Skip to content

Commit c27edbb

Browse files
authored
2단계 - 수강신청(도메인 모델) (#811)
* docs: 수강신청 도메인 모델 문서 추가 및 링크 수정 README.md - 2단계 문서 링크를 `02-lms-domain-model.md`로 올바르게 연결하도록 수정 docs/02-lms-domain-model.md - 수강신청 도메인 모델 요구사항 문서 신규 추가 - LMS 개요 및 기능 요구사항 구조화 - 프로그래밍 요구사항 및 TDD 원칙 명시 * feat: Course에 generation 필드 추가 및 저장/조회 기능 확장 Course.java - generation 필드 추가 및 Getter 구현 - 생성자 오버로드 수정 및 generation 반영 - 기존 필드 포함한 신규 전체 생성자 정의 JdbcCourseRepository.java - insert 쿼리에 generation 컬럼 추가 - findById 조회 시 generation 매핑 추가 및 파라미터 인덱스 조정 schema.sql - course 테이블에 generation 컬럼 추가 (NOT NULL) CourseTest.java - generation 포함 생성자 정상 생성 테스트 추가 CourseRepositoryTest.java - generation 포함한 Course 저장/조회 검증 추가 - createdAt 값 비교 테스트 추가 * feat: 이미지 타입 enum 추가 및 변환 로직 구현 ImageType.java - GIF, JPG, PNG, SVG enum 정의 - jpeg 입력 시 JPG로 매핑하는 from() 메서드 구현 - 미지원 타입은 기존 valueOf에서 예외 발생 ImageTypeTest.java - jpeg 입력 시 JPG 반환 테스트 추가 - 미지원 타입 입력 시 예외 발생 테스트 추가 * refactor: ImageType 대소문자 처리 개선 및 파라미터화 테스트 추가 ImageType.java - 확장자 입력값을 대문자로 변환 후 처리하도록 개선 - JPEG → JPG 매핑 조건을 대문자 기준으로 수정 - valueOf 호출 시 대문자 사용으로 케이스 민감도 문제 해결 ImageTypeTest.java - PNG 입력값에 대해 파라미터화된 변환 성공 테스트 추가(png/PNG) - 기존 JPEG → JPG 테스트 유지 * feat: CoverImage 도메인 추가 및 파일 크기 검증 로직 구현 CoverImage.java - CoverImage 도메인 클래스 신규 추가 - 파일 크기 최대 1MB 제한 상수 정의 - 생성 시 파일 크기 검증 수행 - 검증 실패 시 IllegalArgumentException 발생하도록 구현 CoverImageTest.java - 정상 생성 테스트 추가 - 파일 크기 초과 시 예외 발생 테스트 추가 * feat: CoverImage에 최소 해상도 검증 추가 CoverImage.java - 최소 너비/높이 상수(MIN_WIDTH=300, MIN_HEIGHT=200) 추가 - 생성 시 validateDimension 호출하여 해상도 검증 추가 - 너비/높이 부족 시 상세 메시지 포함 예외 발생 CoverImageTest.java - 너비 부족 시 예외 발생 테스트 추가 - 높이 부족 시 예외 발생 테스트 추가 * feat: CoverImage에 이미지 비율(3:2) 검증 추가 CoverImage.java - WIDTH_RATIO=3, HEIGHT_RATIO=2 상수 추가 - 생성 시 validateAspectRatio 실행하도록 추가 - 비율 불일치 시 상세 메시지 포함 예외 발생 CoverImageTest.java - 3:2 비율이 아닐 경우 예외 발생 테스트 추가 * feat: CoverImage에 확장자 자동 추출 및 검증 로직 추가 CoverImage.java - filename 기반으로 확장자를 추출해 ImageType을 자동 결정하는 생성자 추가 - extractExtension 메서드 추가하여 확장자 누락/잘못된 형식 검증 - 기존 생성자 로직과 동일하게 파일 크기·해상도·비율 검증 수행 CoverImageTest.java - 확장자 미존재 시 예외 발생 테스트 추가 * feat: 이미지 해상도 전용 Value Object ImageDimension 추가 ImageDimension.java - 최소 너비(300), 최소 높이(200), 비율(3:2) 검증 로직 포함한 VO 추가 - 생성 시 해상도 조건 및 비율 조건 유효성 검증 수행 - width, height 불변 값으로 보관 ImageDimensionTest.java - 정상 생성 테스트 추가 - 너비 부족, 높이 부족, 비율 불일치 경우 각각 예외 검증 테스트 추가 * feat: 이미지 파일 전용 Value Object ImageFile 추가 ImageFile.java - 파일 크기 최대 1MB 제한 검증 추가 - 파일명에서 확장자를 추출하여 ImageType 자동 매핑 - 확장자 없음 또는 빈 확장자일 경우 예외 발생 ImageFileTest.java - 정상 생성 테스트 추가 - 파일 크기 초과 시 예외 발생 테스트 추가 - 확장자 누락 시 예외 발생 테스트 추가 * refactor: CoverImage를 ImageFile·ImageDimension VO 조합 구조로 단순화 CoverImage.java - 파일 검증 → ImageFile로 위임 - 해상도·비율 검증 → ImageDimension으로 위임 - CoverImage는 두 VO를 구성요소로 보유하도록 변경 - 기존 개별 검증 로직 및 생성자 삭제 CoverImageTest.java - 파일/해상도/비율 검증 책임이 개별 VO로 이동함에 따라 테스트 제거 * feat: SessionStatus 추가 및 수강신청 가능 여부 판단 로직 구현 SessionStatus.java - PREPARING, RECRUITING, CLOSED 상태 enum 정의 - 상태별 설명 필드 추가 - 모집중(RECRUITING)일 때만 수강신청 가능하도록 canEnroll() 메서드 구현 SessionStatusTest.java - 준비중/종료 상태에서는 신청 불가, 모집중일 때만 신청 가능함을 검증하는 테스트 추가 * feat: SessionPeriod 값 객체 추가 SessionPeriod.java - 시작일·종료일을 표현하는 VO 추가 - 종료일이 시작일보다 이전일 경우 예외 발생하도록 검증 로직 구현 SessionPeriodTest.java - 정상 생성 테스트 추가 - 종료일 < 시작일일 때 예외 발생 테스트 추가 * feat: 금액 값 객체 Money 추가 Money.java - 음수 금액을 허용하지 않는 Money VO 추가 - 0 이상인지 검증하는 validate 로직 구현 MoneyTest.java - 0 및 양수 입력 시 정상 생성 테스트 추가 - 음수 입력 시 예외 발생 테스트 추가 * feat: Money 비교 기능 추가 Money.java - 필드명을 value → amount로 변경하여 의미 명확화 - 동일 금액 비교를 위한 isSameAs 메서드 추가 MoneyTest.java - isSameAs에 대한 비교 테스트(CsvSource 기반) 추가 - 동일 금액/상이한 금액에 대한 기대 결과 검증 * refactor: 테스트 메서드 명을 생성자/기능 중심 네이밍으로 정리 ImageDimensionTest.java - 테스트 메서드명을 `생성자_*` 형태로 일관되게 변경 ImageFileTest.java - 테스트 메서드명을 `생성자_*` 형태로 일관되게 변경 SessionStatusTest.java - canEnroll 검증 메서드명을 `canEnroll_*` 형태로 명확하게 수정 * feat: Session 도메인 추가 Session.java - Session 엔티티 신규 추가 - 기본 생성 시 상태를 PREPARING으로 설정 - 수강신청 가능 여부를 SessionStatus에 위임하여 canEnroll 구현 SessionTest.java - 정상 생성 테스트 추가 - 기본 상태가 PREPARING인지 검증 * feat: 무료/유료 세션 도메인 추가 FreeSession.java - Session을 상속한 무료 세션 도메인 추가 - 별도 제약 없이 기본 생성자만 제공 PaidSession.java - Session 확장하여 유료 세션 구현 - 최대 수강 인원(maxEnrollment) 및 수강료(fee) 보유 - maxEnrollment는 1명 이상이어야 하도록 검증 로직 추가 PaidSessionTest.java - 정상 생성 테스트 추가 - 최대 수강 인원이 0명 이하일 경우 예외 발생 테스트 추가 * refactor: Session을 추상 클래스로 전환하고 테스트 정리 Session.java - Session 클래스를 abstract로 변경하여 FreeSession/PaidSession만 생성되도록 구조 개선 FreeSessionTest.java - 기존 SessionTest를 FreeSessionTest로 변경 - FreeSession 생성 테스트로 수정 - 기본 상태가 PREPARING인지 검증 PaidSessionTest.java - 정상 생성 테스트에서 객체 생성 후 기본 상태 PREPARING 검증하도록 수정 * feat: 수강신청 엔티티 및 컬렉션 도메인 추가 Enrollment.java - 수강신청 정보(id, studentId, createdAt)를 표현하는 엔티티 추가 - 기본 생성 시 id=0L로 초기화하는 생성자 제공 Enrollments.java - Enrollment 리스트를 관리하는 도메인 컬렉션 추가 - 불변 리스트 복사 후 add 메서드로 수강신청 추가 기능 제공 * feat: Session에 수강신청 기능 추가 및 Enrollment에 sessionId 반영 Enrollment.java - sessionId 필드 추가 - 생성자에 sessionId 포함하도록 변경 Session.java - Enrollments 컬렉션 필드 추가 - enroll(enrollment) 메서드 도입 - 모집 상태가 아닐 경우 예외 발생하도록 validateEnrollment 구현 - 기본 생성자는 빈 Enrollments로 초기화하도록 변경 SessionTest.java - 기본 생성 시 상태가 PREPARING인지 검증 - 모집중이 아닐 때 enroll 호출 시 예외 발생 테스트 추가 * feat: 세션 수강신청 기능 확장 및 FreeSession 생성자 추가 Enrollments.java - 내부 리스트명을 values로 변경 - count() 메서드 추가로 총 신청 인원 조회 가능 FreeSession.java - 상태를 지정해 생성할 수 있는 생성자 추가 Session.java - 상태 지정 생성자 추가 - enrollmentCount()로 신청 인원 조회 기능 제공 - enroll 시 모집중 상태가 아니면 예외 발생하도록 유지 SessionTest.java - 모집중(enrolling)일 때 수강신청 성공 테스트 추가 - PREPARING/CLOSED 상태에서 예외 발생하는지 파라미터화 테스트로 검증 * feat: 유료 세션 결제 검증 및 상태 검증 로직 강화 PaidSession.java - 상태 지정 생성자 추가 - enroll(enrollment, payment) 메서드로 결제 금액 일치 여부 검증 후 부모 enroll 호출 - 결제 불일치 시 예외 발생하도록 validatePayment 추가 Session.java - enroll 시 상태 검증 메서드명을 validateStatus로 변경하여 의미 명확화 FreeSessionTest.java - FreeSession 정상 생성 테스트 명확화 PaidSessionTest.java - 공통 객체 setUp으로 정리 - 정상 enroll 테스트 추가 - 결제 금액 불일치 시 예외 발생 테스트 추가 * feat: PaidSession 수강 인원 제한 검증 추가 PaidSession.java - enroll 시 결제 검증 후 정원 초과 여부 validateCapacity 추가 - 정원 도달 시 IllegalStateException 발생 PaidSessionTest.java - 최대 정원 초과 시 예외 발생 테스트 추가 * refactor: Course에서 generation 제거 및 관련 코드 정리 Course.java - generation 필드 제거 - generation을 받던 생성자 삭제 및 기존 생성자 시그니처 정리 - findById 매핑 로직에서 generation 관련 코드 제거 JdbcCourseRepository.java - insert 쿼리에서 generation 제거 - select 쿼리 컬럼 목록에서 generation 제거 - RowMapper 매핑 인덱스 수정 schema.sql - course 테이블에서 generation 컬럼 삭제 CourseTest.java - 생성자 변경에 맞게 테스트 파라미터 수정 CourseRepositoryTest.java - generation 비교 제거 - 변경된 생성자 사용하도록 수정 * feat: Course에 Sessions 추가 및 생성자/매핑 구조 확장 Course.java - Sessions 컬렉션 필드 추가 - 기본 생성자 제거 및 sessions 초기화 포함한 생성자 구조로 변경 - 신규 생성자에서 Sessions를 주입받도록 확장 - 기존 필드 초기화 로직 정리 Sessions.java - Session 리스트를 보관하는 컬렉션 VO 추가 - add(), count() 기능 제공 - 기본 생성 시 빈 리스트로 초기화 JdbcCourseRepository.java - RowMapper에서 sessions는 아직 영속화 대상이 아니므로 null로 매핑 처리 * feat: Course에 Session 관리 기능 추가 및 세션 컬렉션 안정화 Course.java - addSession() 메서드 추가로 코스에 세션 등록 기능 제공 - sessionCount() 메서드로 세션 수 조회 기능 추가 PaidSession.java - 상태 지정 생성자에서 maxEnrollment 유효성 검증 누락 문제를 수정하여 validateMaxEnrollment 호출 추가 Sessions.java - 내부 리스트를 방어적 복사(new ArrayList<>(values))로 초기화해 불변성 강화 * refactor: PaidSession 필드명 의미 명확화 및 용어 일관성 개선 PaidSession.java - maxEnrollment → capacity로 필드명 변경 - fee → price로 필드명 변경 - 생성자 파라미터 및 필드 초기화 로직을 새로운 용어에 맞게 수정 - 결제 검증 로직에서 fee → price로 업데이트 - 정원 검증 로직에서 maxEnrollment → capacity로 일관성 유지 * feat: 수강 인원 값 객체 Capacity 추가 Capacity.java - 최대 수강 인원을 표현하는 VO 추가 - 1명 이상인지 검증하는 로직 포함 - 0 이하 입력 시 IllegalArgumentException 발생 CapacityTest.java - 정상 생성 테스트 추가 - 0 이하 입력 시 예외 발생 테스트 추가 * feat: Capacity에 정원 초과 여부 판단 기능 추가 Capacity.java - isOver(currentCount) 메서드 추가로 현재 인원 대비 정원 초과 여부 판단 기능 제공 CapacityTest.java - isOver에 대한 파라미터화 테스트(CsvSource) 추가 - 정원 == 현재 인원 → 초과(true), 현재 인원 < 정원 → 미초과(false) 검증 * refactor: PaidSession에서 Capacity·Money 값 객체 적용 및 테스트 정리 Capacity.java - getValue() 추가하여 정원 출력 가능하도록 확장 PaidSession.java - 정원(capacity)와 수강료(price)를 VO(Capacity, Money)로 교체 - 기본 생성자에서 int 입력을 받아 VO로 변환하도록 변경 - 상태지정 생성자 역시 VO 기반 생성자로 연결 - validateCapacity에서 Capacity.isOver 사용하도록 변경 - 오류 메시지에서 capacity.getValue() 활용 PaidSessionTest.java - 생성자 테스트를 VO 적용 후 시그니처 변경에 맞게 수정 - fee 제거 후 price 정수 기반 생성 구조로 변경 - enroll 관련 테스트 모두 업데이트된 시그니처에 맞게 수정 * refactor: Course·Enrollment 불변성 강화 Course.java - createdAt, updatedAt 필드를 final로 변경하여 생성 시점 이후 변경 불가하도록 개선 Enrollment.java - id, sessionId, studentId, createdAt 필드를 final로 변경해 불변 객체로 전환 - 생성자 외에는 상태가 변하지 않도록 구조 명확화 * refactor: Session 불변 필드 적용 및 테스트 주석 정리 Session.java - id, coverImage, period, status 필드를 final로 변경하여 불변성 강화 - 생성 후 변경 불가능한 도메인 모델로 구조 개선 CapacityTest.java - 불필요한 주석 제거로 테스트 코드 간결화 * docs: 도메인 모델 문서에 규칙·체크리스트·구현 목록 추가 - 규칙 7(인스턴스 변수 3개 제한)을 명시적으로 문서에 포함해 설계 기준 강화 - PR 전 점검 섹션 추가 및 체크리스트 파일 링크 연결 - 전체 도메인 기능 구현 목록을 항목별로 정리해 진행 상황을 명확하게 표현 - Course, Session, Value Objects, 일급 컬렉션, Enum 등 모든 구성 요소의 요구 기능을 체크리스트 형태로 문서화 * docs: PR 링크 추가 * feat: 수강 신청 중복 검증 추가 Enrollment.java - studentId 조회를 위한 getter 추가 Enrollments.java - add 시 중복 수강 신청 여부 검증 추가 - 동일 studentId 존재 시 예외 발생 처리 EnrollmentsTest.java - 정상 추가 테스트 작성 - 중복 신청 시 예외 검증 테스트 추가 * refactor: Session 수강신청 템플릿 메서드 패턴 적용 FreeSession.java - 결제/정원 정책 메서드 빈 구현 추가(무료 세션 특성 반영) Money.java - ZERO 상수 추가로 무료 결제 표현 단순화 PaidSession.java - 결제 검증을 validatePaymentPolicy로 이관 - 정원 검증을 validateCapacityPolicy로 이관 Session.java - enroll 메서드 final 처리 및 템플릿 메서드 패턴 적용 - validatePaymentPolicy, validateCapacityPolicy 추상 메서드 정의 SessionTest.java - 무료 세션 enroll 테스트에서 Money.ZERO 사용하도록 수정 - 상태 검증 테스트도 동일하게 Money.ZERO 적용 * test: FreeSession 전용 테스트 확장 및 SessionTest 삭제 FreeSessionTest.java - 공통 픽스처 setUp 추가 - 모집중 수강 신청 성공 테스트 추가 - 상태별(준비중/종료) 수강 신청 실패 테스트 추가 - 기존 생성자 검증 로직 유지 및 보강 SessionTest.java - FreeSessionTest로 통합됨에 따라 클래스 삭제 * test: 생성자 및 수강신청 테스트 간결화 FreeSessionTest - 생성자 검증을 한 줄로 축약하여 가독성 개선 PaidSessionTest - 생성자 검증을 한 줄 표현으로 간결화 - 정상 수강 신청 테스트에서 payment 변수 제거 → 즉시 new Money() 전달 - 모집중이 아닐 때 실패 테스트 추가 (`EnumSource` 활용) * docs: 세션 구조 및 템플릿 메서드 패턴 적용 내용 업데이트 02-lms-domain-model.md - Session을 추상 클래스 + 템플릿 메서드 패턴 적용 방식으로 상세화 - FreeSession/PaidSession의 정책 검증 구조와 구현 방식 설명 추가 - Enrollments의 중복 수강 신청 검증 항목 추가 * refactor: 중복 수강 신청 검증 로직 간소화 Enrollments.java - 중복 검증을 stream anyMatch → values.contains(enrollment) 방식으로 단순화 * feat: 수강 신청 정책 분리 및 무료 수강 정책 기본 구현 추가 EnrollmentPolicy.java - 템플릿 메서드 패턴 기반의 공통 검증 구조 도입 - 결제·정원 검증 메서드를 추상화하여 정책별 구현 가능하도록 설계 FreeEnrollmentPolicy.java - 무료 강의용 정책 구현 - 결제·정원 검증을 비활성화 - FREE 타입 반환하도록 구현 SessionType.java - FREE/PAID 세션 타입 정의 및 설명 필드 추가 FreeEnrollmentPolicyTest.java - 무료 정책 검증 시 항상 성공하는지 테스트 추가 - 정책 타입이 FREE인지 검증하는 테스트 추가 * feat: 유료 수강 정책(PaidEnrollmentPolicy) 추가 및 검증 테스트 구현 PaidEnrollmentPolicy.java - 유료 강의용 EnrollmentPolicy 구현 - 결제 금액 검증 로직 추가 (price.isSameAs) - 정원 초과 검증 로직 추가 (capacity.isOver) - PAID 타입 반환 PaidEnrollmentPolicyTest.java - 정상 결제·정원 입력 시 성공 테스트 추가 - 결제 금액 불일치 시 예외 발생 테스트 추가 - 정원 초과 시 예외 발생 테스트 추가 - PAID 타입 반환 검증 * refactor: Session 구조 단순화 및 정책 기반 구성으로 통합 FreeSession.java / PaidSession.java - Session 하위 구현 제거 - 정책 기반 구조로 대체됨에 따라 클래스 삭제 Session.java - 추상 클래스 → 단일 클래스로 변경 - EnrollmentPolicy 의존성 추가하여 결제·정원 검증을 정책에 위임 - enroll 로직에서 상태 검증 후 정책 검증 호출하도록 통합 - getType() 추가하여 세션 타입 조회 가능 PaidSessionTest.java - 정책 기반 구조로 변경됨에 따라 기존 PaidSession 테스트 삭제 SessionTest.java - FreeSessionTest → SessionTest로 변경 - Session + FreeEnrollmentPolicy 조합으로 생성/수강 신청 테스트하도록 수정 - 모집 상태 검증 로직 유지 * docs: 정책 기반 Session 구조로 문서 내용 업데이트 02-lms-domain-model.md - Session을 추상 클래스 기반 → 정책 위임 방식으로 변경 - EnrollmentPolicy 중심 구조(템플릿 메서드) 설명 추가 - Free/Paid 정책의 역할 및 검증 규칙 명확히 정리 - Session 타입 조회가 Policy 반환값 기반임을 명시
1 parent bae2e6a commit c27edbb

33 files changed

+915
-9
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
본 저장소는 TDD 학습을 위한 단계별 과제를 포함합니다.
44
## 단계별 문서
55
- **[1단계 - 레거시 코드 리팩터링](./docs/01-legacy-code-refactoring.md)**
6-
- **[2단계 - 수강신청(도메인 모델)]()**
6+
- **[2단계 - 수강신청(도메인 모델)](./docs/02-lms-domain-model.md)**
77
- **[3단계 - 수강신청(DB 신청)]()**
88
- **[4단계 - 수강신청(요구사항 변경)]()**
99
## 진행 방법

docs/02-lms-domain-model.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# 2단계 - 수강신청(도메인 모델)
2+
***
3+
## 코드 리뷰
4+
> PR 링크:
5+
> **[https://github.com/next-step/java-lms/pull/811](https://github.com/next-step/java-lms/pull/811)**
6+
## 나의 학습 목표
7+
- TDD 사이클로 구현
8+
- 객체지향 생활 체조 원칙 준수
9+
- 테스트 작성하기 쉬운 구조로 설계
10+
- 자기 점검 체크리스트 준수
11+
- 특히 "규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다."를 지키기 위해 노력한다.
12+
## 학습 관리 시스템(LMS)
13+
- 넥스트스텝은 재직자를 대상으로 소프트웨어 교육을 진행하는 교육 기관이다.
14+
- 2018년 교육 사업을 시작했다.
15+
- 교육 사업을 시작하며 자체적으로 학습 관리 시스템을 개발해 수강생을 모집하고, 컨텐츠를 관리하고 있다.
16+
## 수강 신청 기능 요구사항
17+
- 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다.
18+
- 강의는 시작일과 종료일을 가진다.
19+
- 강의는 강의 커버 이미지 정보를 가진다.
20+
- 이미지 크기는 1MB 이하여야 한다.
21+
- 이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다.
22+
- 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다.
23+
- 강의는 무료 강의와 유료 강의로 나뉜다.
24+
- 무료 강의는 최대 수강 인원 제한이 없다.
25+
- 유료 강의는 강의 최대 수강 인원을 초과할 수 없다.
26+
- 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다.
27+
- 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다.
28+
- 강의 수강신청은 강의 상태가 모집중일 때만 가능하다.
29+
- 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다.
30+
- 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다.
31+
## 프로그래밍 요구사항
32+
- DB 테이블 설꼐 없이 도메인 모델부터 구현한다.
33+
- 도메인 모델은 TDD로 구현한다.
34+
- 단, Service 클래스는 단위 테스트가 없어도 된다.
35+
## PR 전 점검
36+
**[체크리스트 확인하기](checklist.md)**
37+
## 구현 기능 목록
38+
#### Course
39+
- [x] 강의 추가
40+
- [x] 강의 개수 조회
41+
42+
#### Sessions (일급 컬렉션)
43+
- [x] 강의 추가
44+
- [x] 강의 개수 조회
45+
46+
#### Session
47+
- [x] 수강 신청
48+
- [x] 모집중 상태 검증
49+
- [x] Policy에 검증 위임
50+
- [x] 수강 인원 조회
51+
- [x] 강의 타입 조회 (Policy에서 반환)
52+
53+
#### EnrollmentPolicy (추상 클래스, 템플릿 메서드 패턴)
54+
- [x] final validate(payment, currentCount) - 알고리즘 순서 강제
55+
- [x] 결제 검증 (validatePayment) - 하위에서 구현
56+
- [x] 정원 검증 (validateCapacity) - 하위에서 구현
57+
- [x] 강의 타입 반환 (getType) - 하위에서 구현
58+
59+
#### FreeEnrollmentPolicy
60+
- [x] EnrollmentPolicy 상속
61+
- [x] 결제 검증: 빈 구현 (무료)
62+
- [x] 정원 검증: 빈 구현 (제한 없음)
63+
- [x] 타입 반환: FREE
64+
65+
#### PaidEnrollmentPolicy
66+
- [x] EnrollmentPolicy 상속
67+
- [x] 결제 금액 검증 (수강료 일치)
68+
- [x] 수강 인원 검증 (정원 초과)
69+
- [x] 타입 반환: PAID
70+
71+
#### Enrollments (일급 컬렉션)
72+
- [x] 수강 신청 추가
73+
- [x] 수강 인원 조회
74+
- [x] 중복 수강 신청 검증
75+
76+
#### CoverImage (VO)
77+
- [x] 이미지 파일 정보와 크기 정보 조합 생성
78+
79+
#### ImageFile (VO)
80+
- [x] 파일 크기 검증 (1MB 이하)
81+
- [x] 확장자 추출
82+
- [x] 이미지 타입 변환
83+
84+
#### ImageDimension (VO)
85+
- [x] 너비/높이 최소값 검증 (300x200 이상)
86+
- [x] 비율 검증 (3:2)
87+
88+
#### ImageType (Enum)
89+
- [x] 확장자 → ImageType 변환 (from(String))
90+
- [x] JPEG → JPG 변환 지원
91+
92+
#### SessionPeriod (VO)
93+
- [x] 시작일/종료일 검증 (종료일 >= 시작일)
94+
95+
#### SessionStatus (Enum)
96+
- [x] 수강 신청 가능 여부
97+
98+
#### Money (VO)
99+
- [x] 금액 검증 (0 이상)
100+
- [x] 금액 비교
101+
102+
#### Capacity (VO)
103+
- [x] 최대 인원 검증 (1명 이상)
104+
- [x] 초과 여부 확인
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package nextstep.courses.domain;
2+
3+
public class Capacity {
4+
private final int value;
5+
6+
public Capacity(int value) {
7+
if (value <= 0) {
8+
throw new IllegalArgumentException(String.format("최대 수강 인원은 1명 이상이어야 합니다. (입력: %d)", value));
9+
}
10+
this.value = value;
11+
}
12+
13+
public boolean isOver(int currentCount) {
14+
return currentCount >= value;
15+
}
16+
17+
public int getValue() {
18+
return this.value;
19+
}
20+
}

src/main/java/nextstep/courses/domain/Course.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,39 @@ public class Course {
99

1010
private Long creatorId;
1111

12-
private LocalDateTime createdAt;
12+
private final Sessions sessions;
1313

14-
private LocalDateTime updatedAt;
14+
private final LocalDateTime createdAt;
1515

16-
public Course() {}
16+
private final LocalDateTime updatedAt;
1717

18-
public Course(String title, Long creatorId) {
19-
this(0L, title, creatorId, LocalDateTime.now(), null);
18+
public Course(String title, Long creatorId, LocalDateTime now) {
19+
this(0L, title, creatorId, new Sessions(), now, null);
2020
}
2121

22-
public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) {
22+
public Course(
23+
Long id,
24+
String title,
25+
Long creatorId,
26+
Sessions sessions,
27+
LocalDateTime createdAt,
28+
LocalDateTime updatedAt) {
2329
this.id = id;
2430
this.title = title;
2531
this.creatorId = creatorId;
32+
this.sessions = sessions;
2633
this.createdAt = createdAt;
2734
this.updatedAt = updatedAt;
2835
}
2936

37+
public void addSession(Session session) {
38+
sessions.add(session);
39+
}
40+
41+
public int sessionCount() {
42+
return sessions.count();
43+
}
44+
3045
public String getTitle() {
3146
return title;
3247
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package nextstep.courses.domain;
2+
3+
public class CoverImage {
4+
private final ImageFile imageFile;
5+
private final ImageDimension dimension;
6+
7+
public CoverImage(String filename, long fileSize, int width, int height) {
8+
this.imageFile = new ImageFile(filename, fileSize);
9+
this.dimension = new ImageDimension(width, height);
10+
}
11+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package nextstep.courses.domain;
2+
3+
import java.time.LocalDateTime;
4+
5+
public class Enrollment {
6+
private final Long id;
7+
private final Long sessionId;
8+
private final Long studentId;
9+
private final LocalDateTime createdAt;
10+
11+
public Enrollment(Long sessionId, Long studentId, LocalDateTime now) {
12+
this(0L, sessionId, studentId, now);
13+
}
14+
15+
public Enrollment(Long id, Long sessionId, Long studentId, LocalDateTime createdAt) {
16+
this.id = id;
17+
this.sessionId = sessionId;
18+
this.studentId = studentId;
19+
this.createdAt = createdAt;
20+
}
21+
22+
public Long getStudentId() {
23+
return studentId;
24+
}
25+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package nextstep.courses.domain;
2+
3+
public abstract class EnrollmentPolicy {
4+
public final void validate(Money payment, int currentCount) {
5+
validatePayment(payment);
6+
validateCapacity(currentCount);
7+
}
8+
9+
public abstract SessionType getType();
10+
11+
protected abstract void validatePayment(Money payment);
12+
13+
protected abstract void validateCapacity(int currentCount);
14+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package nextstep.courses.domain;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
6+
public class Enrollments {
7+
private final List<Enrollment> values;
8+
9+
public Enrollments() {
10+
this(new ArrayList<>());
11+
}
12+
13+
public Enrollments(List<Enrollment> values) {
14+
this.values = new ArrayList<>(values);
15+
}
16+
17+
public void add(Enrollment enrollment) {
18+
validateNotDuplicate(enrollment);
19+
this.values.add(enrollment);
20+
}
21+
22+
private void validateNotDuplicate(Enrollment enrollment) {
23+
if (values.contains(enrollment)) {
24+
throw new IllegalArgumentException("이미 수강 신청한 강의입니다.");
25+
}
26+
}
27+
28+
public int count() {
29+
return this.values.size();
30+
}
31+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package nextstep.courses.domain;
2+
3+
public class FreeEnrollmentPolicy extends EnrollmentPolicy {
4+
5+
@Override
6+
public SessionType getType() {
7+
return SessionType.FREE;
8+
}
9+
10+
@Override
11+
protected void validatePayment(Money payment) {
12+
// 무료는 검증 X
13+
}
14+
15+
@Override
16+
protected void validateCapacity(int currentCount) {
17+
// 무료는 검증 X
18+
}
19+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package nextstep.courses.domain;
2+
3+
public class ImageDimension {
4+
private static final int MIN_WIDTH = 300;
5+
private static final int MIN_HEIGHT = 200;
6+
private static final int WIDTH_RATIO = 3;
7+
private static final int HEIGHT_RATIO = 2;
8+
9+
private final int width;
10+
private final int height;
11+
12+
public ImageDimension(int width, int height) {
13+
validateDimension(width, height);
14+
validateAspectRatio(width, height);
15+
this.width = width;
16+
this.height = height;
17+
}
18+
19+
private void validateDimension(int width, int height) {
20+
if (width < MIN_WIDTH) {
21+
throw new IllegalArgumentException(String.format("이미지 너비는 %d픽셀 이상이어야 합니다. (입력: %d)", MIN_WIDTH, width));
22+
}
23+
if (height < MIN_HEIGHT) {
24+
throw new IllegalArgumentException(String.format("이미지 높이는 %d픽셀 이상이어야 합니다. (입력: %d)", MIN_HEIGHT, height));
25+
}
26+
}
27+
28+
private void validateAspectRatio(int width, int height) {
29+
if (width * HEIGHT_RATIO != height * WIDTH_RATIO) {
30+
throw new IllegalArgumentException(String.format("이미지 비율은 3:2여야 합니다. (입력: %d:%d)", width, height));
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)