From d0abcf2a63cb682f99a93981aff72f30962feb1c Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:55:53 +0900 Subject: [PATCH 01/28] =?UTF-8?q?docs:=20=EC=88=98=EA=B0=95=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4371c7e59..f2b4d34ea 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ # 학습 관리 시스템(Learning Management System) + ## 진행 방법 + * 학습 관리 시스템의 수강신청 요구사항을 파악한다. * 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. * 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. * 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 + * [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) --- @@ -14,10 +17,46 @@ ### 질문 삭제하기 -- 질문자 = 로그인 사용자 아니면 삭제 불가 -- 답변 중 다른 사람이 쓴 답변이 있으면 삭제 불가 -- 답변이 없으면 삭제 가능 -- 질문자와 답변자가 모두 동일하면 삭제 가능 -- 삭제되면 질문 상태 변경 -- 삭제되면 모든 답변 상태 변경 -- 삭제 이력 생성됨 \ No newline at end of file +- 질문자 = 로그인 사용자 아니면 삭제 불가 +- 답변 중 다른 사람이 쓴 답변이 있으면 삭제 불가 +- 답변이 없으면 삭제 가능 +- 질문자와 답변자가 모두 동일하면 삭제 가능 +- 삭제되면 질문 상태 변경 +- 삭제되면 모든 답변 상태 변경 +- 삭제 이력 생성됨 + +### 수강 신청 기능 + +#### Course + +- 기수 단위로 운영 +- 여러 개의 Session(강의)을 가질 수 있음 + +#### Session (강의) + +##### 속성 + +- 시작일(start_date), 종료일(end_date) +- 커버 이미지 + - 최대 1MB + - 타입: gif, jpg/jpeg, png, svg + - 최소 사이즈: width 300px, height 200px + - 비율: 3:2 +- 무료/유료 구분 +- 최대 수강 인원 (유료 강의만 적용) +- 상태: 준비중, 모집중, 종료 + +##### 규칙 + +- 강의 상태가 `모집중`일 때만 수강 신청 가능 +- 무료 강의는 수강 인원 제한 없음 +- 유료 강의 + - 최대 수강 인원을 초과할 수 없음 + - 결제 금액과 수강료가 일치해야 수강 신청 가능 + - 결제 정보는 `payments` 모듈의 Payment 객체를 통해 관리 + +#### Payment (결제) + +- 유료 강의 결제 정보 관리 +- Payment 객체 반환 +- 결제 완료 여부 확인 후 수강 신청 가능 From ec941aa543c7af25db11f47da3a1b6292fdf4b89 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:38:41 +0900 Subject: [PATCH 02/28] =?UTF-8?q?feat:=20startDate/endDate=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Session.java | 24 +++++++++++++++++++ .../nextstep/sessions/domain/SessionTest.java | 16 +++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/Session.java create mode 100644 src/test/java/nextstep/sessions/domain/SessionTest.java diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java new file mode 100644 index 000000000..ed27478a1 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -0,0 +1,24 @@ +package nextstep.sessions.domain; + +import java.time.LocalDate; + +public class Session { + + static final String ERROR_INVALID_DATE = "시작일이 종료일보다 빨라야 합니다"; + + private final LocalDate startDate; + + private final LocalDate endDate; + + public Session(LocalDate startDate, LocalDate endDate) { + validateDate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validateDate(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException(ERROR_INVALID_DATE); + } + } +} diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java new file mode 100644 index 000000000..64cadf494 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -0,0 +1,16 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import org.junit.jupiter.api.Test; + +class SessionTest { + + @Test + void startDateMustBeBeforeEndDate() { + assertThatThrownBy(() -> new Session(LocalDate.of(2025, 12, 18), LocalDate.of(2025, 11, 3))).isInstanceOf( + IllegalArgumentException.class).hasMessageContaining("시작일이 종료일보다"); + } + +} \ No newline at end of file From 5f3554851e97436fc61ba3567e1973d1d010305e Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:56:56 +0900 Subject: [PATCH 03/28] =?UTF-8?q?feat:=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=83=81=ED=83=9C=20PREPARING=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/sessions/domain/Session.java | 9 ++++++++- .../nextstep/sessions/domain/SessionStatus.java | 7 +++++++ .../java/nextstep/sessions/domain/SessionTest.java | 13 ++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/sessions/domain/SessionStatus.java diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index ed27478a1..ec76fa633 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -9,11 +9,18 @@ public class Session { private final LocalDate startDate; private final LocalDate endDate; - + + private final SessionStatus status; + public Session(LocalDate startDate, LocalDate endDate) { validateDate(startDate, endDate); this.startDate = startDate; this.endDate = endDate; + this.status = SessionStatus.PREPARING; + } + + public SessionStatus status() { + return status; } private void validateDate(LocalDate startDate, LocalDate endDate) { diff --git a/src/main/java/nextstep/sessions/domain/SessionStatus.java b/src/main/java/nextstep/sessions/domain/SessionStatus.java new file mode 100644 index 000000000..c55baf5d9 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionStatus.java @@ -0,0 +1,7 @@ +package nextstep.sessions.domain; + +public enum SessionStatus { + PREPARING, + OPEN, + CLOSED +} diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index 64cadf494..0f6a2a9e8 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -1,5 +1,6 @@ package nextstep.sessions.domain; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.LocalDate; @@ -7,10 +8,20 @@ class SessionTest { + static final LocalDate START_DATE = LocalDate.of(2025, 11, 3); + static final LocalDate END_DATE = LocalDate.of(2025, 12, 18); + @Test void startDateMustBeBeforeEndDate() { - assertThatThrownBy(() -> new Session(LocalDate.of(2025, 12, 18), LocalDate.of(2025, 11, 3))).isInstanceOf( + assertThatThrownBy(() -> new Session(END_DATE, START_DATE)).isInstanceOf( IllegalArgumentException.class).hasMessageContaining("시작일이 종료일보다"); } + @Test + void sessionStatusIsPreparingOnCreation() { + Session session = new Session(START_DATE, END_DATE); + assertThat(session.status()).isEqualTo(SessionStatus.PREPARING); + } + + } \ No newline at end of file From b4b63b88cd076ea539aaf21fbae3bfc6191e2e16 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Fri, 12 Dec 2025 23:33:12 +0900 Subject: [PATCH 04/28] =?UTF-8?q?feat:=20=EC=9C=A0=EB=A3=8C/=EB=AC=B4?= =?UTF-8?q?=EB=A3=8C=20=EA=B5=AC=EB=B6=84=20=EB=B0=8F=20=EC=B5=9C=EB=8C=80?= =?UTF-8?q?=20=EC=88=98=EA=B0=95=EC=9D=B8=EC=9B=90=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Session.java | 30 +++++++++++++++++ .../nextstep/sessions/domain/SessionTest.java | 32 ++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index ec76fa633..a43283cb3 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -12,20 +12,50 @@ public class Session { private final SessionStatus status; + private final boolean isPaid; + + private final Integer maxCapacity; + public Session(LocalDate startDate, LocalDate endDate) { + this(startDate, endDate, false, null); + } + + public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity) { validateDate(startDate, endDate); + validateCapacity(isPaid, maxCapacity); this.startDate = startDate; this.endDate = endDate; this.status = SessionStatus.PREPARING; + this.isPaid = isPaid; + this.maxCapacity = maxCapacity; } public SessionStatus status() { return status; } + public boolean isPaid() { + return isPaid; + } + + public Integer maxCapacity() { + return maxCapacity; + } + private void validateDate(LocalDate startDate, LocalDate endDate) { if (startDate.isAfter(endDate)) { throw new IllegalArgumentException(ERROR_INVALID_DATE); } } + + private void validateCapacity(boolean isPaid, Integer maxCapacity) { + if (isPaid && (maxCapacity == null || maxCapacity <= 0)) { + throw new IllegalArgumentException("유료 강의는 최대 수강인원이 있어야 합니다"); + } + if (!isPaid && maxCapacity != null) { + throw new IllegalArgumentException("무료 강의는 최대 수강인원이 없어야 합니다"); + } + } + + } diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index 0f6a2a9e8..dfe794d0e 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -23,5 +23,35 @@ void sessionStatusIsPreparingOnCreation() { assertThat(session.status()).isEqualTo(SessionStatus.PREPARING); } + @Test + void whenCreatingFreeSession_thenMaxCapacityIsNull() { + Session freeSession = new Session(START_DATE, END_DATE, false, null); + assertThat(freeSession.isPaid()).isFalse(); + assertThat(freeSession.maxCapacity()).isNull(); + } + + @Test + void whenCreatingFreeSessionWithNonNullCapacity_thenThrow() { + assertThatThrownBy(() -> new Session(START_DATE, END_DATE, false, 10)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("무료 강의는 최대 수강인원이 없어야 합니다"); + } -} \ No newline at end of file + @Test + void whenCreatingPaidSession_thenMaxCapacityMustBePositive() { + Session paidSession = new Session(START_DATE, END_DATE, true, 5); + assertThat(paidSession.isPaid()).isTrue(); + assertThat(paidSession.maxCapacity()).isEqualTo(5); + } + + @Test + void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { + assertThatThrownBy(() -> new Session(START_DATE, END_DATE, true, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); + + assertThatThrownBy(() -> new Session(START_DATE, END_DATE, true, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); + } +} From 61d5b303491070e96b58138f49fe2f81332173e6 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:50:21 +0900 Subject: [PATCH 05/28] =?UTF-8?q?feat:=20=EC=9C=A0=EB=A3=8C=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20fee=20=ED=95=84=EB=93=9C=20=EB=B0=8F=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Session.java | 22 ++++++++++++++- .../nextstep/sessions/domain/SessionTest.java | 28 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index a43283cb3..5eb098e8e 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -16,18 +16,26 @@ public class Session { private final Integer maxCapacity; + private final int fee; + public Session(LocalDate startDate, LocalDate endDate) { - this(startDate, endDate, false, null); + this(startDate, endDate, false, null, 0); } public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity) { + this(startDate, endDate, isPaid, maxCapacity, 0); + } + + public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee) { validateDate(startDate, endDate); validateCapacity(isPaid, maxCapacity); + validateFee(isPaid, fee); this.startDate = startDate; this.endDate = endDate; this.status = SessionStatus.PREPARING; this.isPaid = isPaid; this.maxCapacity = maxCapacity; + this.fee = fee; } public SessionStatus status() { @@ -42,6 +50,10 @@ public Integer maxCapacity() { return maxCapacity; } + public int fee() { + return fee; + } + private void validateDate(LocalDate startDate, LocalDate endDate) { if (startDate.isAfter(endDate)) { throw new IllegalArgumentException(ERROR_INVALID_DATE); @@ -57,5 +69,13 @@ private void validateCapacity(boolean isPaid, Integer maxCapacity) { } } + private void validateFee(boolean isPaid, int fee) { + if (isPaid && fee <= 0) { + throw new IllegalArgumentException("유료 강의는 0원 초과 여야 합니다"); + } + if (!isPaid && fee != 0) { + throw new IllegalArgumentException("무료 강의는 0원 이어야 합니다"); + } + } } diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index dfe794d0e..6db5e517b 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -39,7 +39,7 @@ void whenCreatingFreeSessionWithNonNullCapacity_thenThrow() { @Test void whenCreatingPaidSession_thenMaxCapacityMustBePositive() { - Session paidSession = new Session(START_DATE, END_DATE, true, 5); + Session paidSession = new Session(START_DATE, END_DATE, true, 5, 100_000); assertThat(paidSession.isPaid()).isTrue(); assertThat(paidSession.maxCapacity()).isEqualTo(5); } @@ -54,4 +54,30 @@ void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); } + + @Test + void whenCreatingFreeSession_thenFeeIsZero() { + Session freeSession = new Session(START_DATE, END_DATE, false, null, 0); + assertThat(freeSession.fee()).isEqualTo(0); + } + + @Test + void whenCreatingFreeSessionWithInvalidFee_thenThrow() { + assertThatThrownBy(() -> new Session(START_DATE, END_DATE, false, null, 500_000)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("무료 강의는 0원"); + } + + @Test + void whenCreatingPaidSession_thenFeeIsOverZero() { + Session paidSession = new Session(START_DATE, END_DATE, true, 5, 100_000); + assertThat(paidSession.fee()).isEqualTo(100_000); + } + + @Test + void whenCreatingPaidSessionWithInvalidFee_thenThrow() { + assertThatThrownBy(() -> new Session(START_DATE, END_DATE, true, 5, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유료 강의는 0원 초과"); + } } From 0ed117dd542b8b6b0e7f7ae660ff3931a69eced5 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 01:39:03 +0900 Subject: [PATCH 06/28] =?UTF-8?q?feat:=20=EC=83=81=ED=83=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=88=98=EA=B0=95=20=EC=8B=A0=EC=B2=AD=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EC=97=AC=EB=B6=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/sessions/domain/Session.java | 11 +++++++++-- .../java/nextstep/sessions/domain/SessionTest.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 5eb098e8e..6ed65b065 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -10,7 +10,7 @@ public class Session { private final LocalDate endDate; - private final SessionStatus status; + private SessionStatus status; private final boolean isPaid; @@ -54,6 +54,14 @@ public int fee() { return fee; } + public boolean canEnroll() { + return status == SessionStatus.OPEN; + } + + public void startRecruiting() { + this.status = SessionStatus.OPEN; + } + private void validateDate(LocalDate startDate, LocalDate endDate) { if (startDate.isAfter(endDate)) { throw new IllegalArgumentException(ERROR_INVALID_DATE); @@ -77,5 +85,4 @@ private void validateFee(boolean isPaid, int fee) { throw new IllegalArgumentException("무료 강의는 0원 이어야 합니다"); } } - } diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index 6db5e517b..a7b496e8f 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -80,4 +80,18 @@ void whenCreatingPaidSessionWithInvalidFee_thenThrow() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("유료 강의는 0원 초과"); } + + @Test + void whenSessionStatusIsOpen_thenCanEnrollIsTrue() { + Session session = new Session(START_DATE, END_DATE, false, null, 0); + session.startRecruiting(); + assertThat(session.canEnroll()).isTrue(); + } + + @Test + void whenSessionStatusIsNotOpen_thenCanEnrollIsFalse() { + Session session = new Session(START_DATE, END_DATE, false, null, 0); + assertThat(session.canEnroll()).isFalse(); + } + } From 5a09362fbe5d0afc750afd4108caf26dad0a2cc9 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:38:28 +0900 Subject: [PATCH 07/28] =?UTF-8?q?feat:=20=EC=9C=A0=EB=A3=8C=20=EA=B0=95?= =?UTF-8?q?=EC=9D=98=20=EC=B5=9C=EB=8C=80=20=EC=88=98=EA=B0=95=20=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=20=EC=B4=88=EA=B3=BC=20=EC=8B=9C=20=EC=88=98=EA=B0=95?= =?UTF-8?q?=20=EB=B6=88=EA=B0=80=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/sessions/domain/Session.java | 12 ++++++++++++ .../java/nextstep/sessions/domain/SessionTest.java | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 6ed65b065..5c2337de0 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -18,6 +18,8 @@ public class Session { private final int fee; + private int enrollCount; + public Session(LocalDate startDate, LocalDate endDate) { this(startDate, endDate, false, null, 0); } @@ -26,6 +28,11 @@ public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer m this(startDate, endDate, isPaid, maxCapacity, 0); } + Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, int enrollCount) { + this(startDate, endDate, isPaid, maxCapacity, fee); + this.enrollCount = enrollCount; + } + public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee) { validateDate(startDate, endDate); validateCapacity(isPaid, maxCapacity); @@ -36,6 +43,7 @@ public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer m this.isPaid = isPaid; this.maxCapacity = maxCapacity; this.fee = fee; + this.enrollCount = 0; } public SessionStatus status() { @@ -55,6 +63,9 @@ public int fee() { } public boolean canEnroll() { + if (isPaid() && enrollCount >= maxCapacity) { + return false; + } return status == SessionStatus.OPEN; } @@ -85,4 +96,5 @@ private void validateFee(boolean isPaid, int fee) { throw new IllegalArgumentException("무료 강의는 0원 이어야 합니다"); } } + } diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index a7b496e8f..b8315ec01 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -94,4 +94,10 @@ void whenSessionStatusIsNotOpen_thenCanEnrollIsFalse() { assertThat(session.canEnroll()).isFalse(); } + @Test + void whenEnrollCountIsOverMaxCapacity_thenCanEnrollIsFalse() { + Session session = new Session(START_DATE, END_DATE, true, 1, 100_000, 1); + assertThat(session.canEnroll()).isFalse(); + } + } From f67bd432ebc338befa5e9727bca0a61991b10911 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:53:59 +0900 Subject: [PATCH 08/28] =?UTF-8?q?feat:=20=EC=88=98=EA=B0=95=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/sessions/domain/Session.java | 11 +++++++++++ .../nextstep/sessions/domain/SessionTest.java | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 5c2337de0..8876d50bf 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -62,6 +62,10 @@ public int fee() { return fee; } + public int enrollCount() { + return enrollCount; + } + public boolean canEnroll() { if (isPaid() && enrollCount >= maxCapacity) { return false; @@ -73,6 +77,13 @@ public void startRecruiting() { this.status = SessionStatus.OPEN; } + public void enroll() { + if (!canEnroll()) { + throw new IllegalArgumentException("수강 신청을 할 수 없습니다"); + } + enrollCount++; + } + private void validateDate(LocalDate startDate, LocalDate endDate) { if (startDate.isAfter(endDate)) { throw new IllegalArgumentException(ERROR_INVALID_DATE); diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index b8315ec01..325d7dc60 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -100,4 +100,22 @@ void whenEnrollCountIsOverMaxCapacity_thenCanEnrollIsFalse() { assertThat(session.canEnroll()).isFalse(); } + @Test + void whenEnroll_thenEnrollCountIncrease() { + Session session = new Session(START_DATE, END_DATE, true, 1, 100_000); + session.startRecruiting(); + int before = session.enrollCount(); + session.enroll(); + int after = session.enrollCount(); + assertThat(after - before).isEqualTo(1); + } + + @Test + void whenEnrollImpossible_thenThrow() { + Session session = new Session(START_DATE, END_DATE, true, 1, 100_000, 1); + assertThatThrownBy(session::enroll) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("수강 신청을 할 수 없습니다"); + } + } From 5901d8fa6a33076bdbc850a50e6ad0bc9c1607ac Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 03:31:22 +0900 Subject: [PATCH 09/28] =?UTF-8?q?feat:=20SessionImage=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=ED=95=84=EC=88=98=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/ImageType.java | 18 +++++ .../sessions/domain/SessionImage.java | 67 +++++++++++++++++++ .../sessions/domain/ImageTypeTest.java | 28 ++++++++ .../sessions/domain/SessionImageTest.java | 55 +++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/ImageType.java create mode 100644 src/main/java/nextstep/sessions/domain/SessionImage.java create mode 100644 src/test/java/nextstep/sessions/domain/ImageTypeTest.java create mode 100644 src/test/java/nextstep/sessions/domain/SessionImageTest.java diff --git a/src/main/java/nextstep/sessions/domain/ImageType.java b/src/main/java/nextstep/sessions/domain/ImageType.java new file mode 100644 index 000000000..5e212a280 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/ImageType.java @@ -0,0 +1,18 @@ +package nextstep.sessions.domain; + +public enum ImageType { + GIF, JPG, JPEG, PNG, SVG; + + public static ImageType from(String ext) { + if (ext == null || ext.trim().isEmpty()) { + throw new IllegalArgumentException("확장자가 유효하지 않습니다"); + } + for (ImageType type : ImageType.values()) { + if (type.name().equalsIgnoreCase(ext)) { + return type; + } + } + throw new IllegalArgumentException("지원하지 않는 이미지 형식입니다: " + ext); + } +} + diff --git a/src/main/java/nextstep/sessions/domain/SessionImage.java b/src/main/java/nextstep/sessions/domain/SessionImage.java new file mode 100644 index 000000000..f88d6a92c --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionImage.java @@ -0,0 +1,67 @@ +package nextstep.sessions.domain; + +public class SessionImage { + + private static final long MAX_SIZE = 1_000_000; // 1MB + private static final int MIN_WIDTH = 300; + private static final int MIN_HEIGHT = 200; + private static final double RATIO = 3.0 / 2.0; + + private final String fileName; + private final long size; + private final int width; + private final int height; + private final ImageType type; + + public SessionImage(String fileName, long size, int width, int height) { + validateFileName(fileName); + validateSize(size); + validateDimension(width, height); + validateRatio(width, height); + + this.fileName = fileName; + this.size = size; + this.width = width; + this.height = height; + this.type = extractType(fileName); + } + + public String fileName() { + return fileName; + } + + public long size() { + return size; + } + + private void validateFileName(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) { + throw new IllegalArgumentException("파일명은 빈 값일 수 없습니다"); + } + } + + private void validateSize(long size) { + if (size <= 0 || size > MAX_SIZE) { + throw new IllegalArgumentException("이미지 용량은 1MB 이하여야 합니다"); + } + } + + private void validateDimension(int width, int height) { + if (width < MIN_WIDTH || height < MIN_HEIGHT) { + throw new IllegalArgumentException("이미지 크기가 최소 조건을 만족하지 않습니다"); + } + } + + private void validateRatio(int width, int height) { + double ratio = (double) width / height; + if (Math.abs(ratio - RATIO) > 0.0001) { + throw new IllegalArgumentException("이미지 비율은 3:2여야 합니다"); + } + } + + private ImageType extractType(String fileName) { + String ext = fileName.substring(fileName.lastIndexOf('.') + 1); + return ImageType.from(ext); + } + +} diff --git a/src/test/java/nextstep/sessions/domain/ImageTypeTest.java b/src/test/java/nextstep/sessions/domain/ImageTypeTest.java new file mode 100644 index 000000000..cb065564f --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/ImageTypeTest.java @@ -0,0 +1,28 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class ImageTypeTest { + + @Test + void whenValidExtension_thenReturnsImageType() { + assertThat(ImageType.from("jpg")).isEqualTo(ImageType.JPG); + assertThat(ImageType.from("JPEG")).isEqualTo(ImageType.JPEG); + assertThat(ImageType.from("png")).isEqualTo(ImageType.PNG); + } + + @Test + void whenInvalidExtension_thenThrows() { + assertThatThrownBy(() -> ImageType.from("exe")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void whenBlank_thenThrows() { + assertThatThrownBy(() -> ImageType.from("")) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/sessions/domain/SessionImageTest.java b/src/test/java/nextstep/sessions/domain/SessionImageTest.java new file mode 100644 index 000000000..f4ab2b224 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/SessionImageTest.java @@ -0,0 +1,55 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +class SessionImageTest { + + @Test + void createImage_success() { + SessionImage image = new SessionImage( + "cover.png", + 500_000, + 300, + 200 + ); + assertThat(image.fileName()).isEqualTo("cover.png"); + } + + @Test + void whenImageSizeExceeds1MB_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SessionImage("cover.png", 1_048_577, 300, 200) + ).withMessageContaining("1MB"); + } + + @Test + void whenImageWidthIsTooSmall_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SessionImage("cover.png", 100_000, 299, 200) + ).withMessageContaining("이미지 크기"); + } + + @Test + void whenImageHeightIsTooSmall_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SessionImage("cover.png", 100_000, 300, 199) + ).withMessageContaining("이미지 크기"); + } + + @Test + void whenImageRatioIsNot3To2_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SessionImage("cover.png", 100_000, 310, 200) + ).withMessageContaining("3:2"); + } + + @Test + void whenFileNameIsEmpty_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new SessionImage("", 100_000, 300, 200) + ).withMessageContaining("파일명"); + } +} From bce1fbd2411ec9ccf1ef04f861b3b8ded781dd01 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 05:19:56 +0900 Subject: [PATCH 10/28] =?UTF-8?q?test:=20SessionTestBuilder=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/SessionTest.java | 42 +++++++++------- .../sessions/domain/SessionTestBuilder.java | 50 +++++++++++++++++++ 2 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 src/test/java/nextstep/sessions/domain/SessionTestBuilder.java diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index 325d7dc60..d7c7799dc 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -13,96 +13,100 @@ class SessionTest { @Test void startDateMustBeBeforeEndDate() { - assertThatThrownBy(() -> new Session(END_DATE, START_DATE)).isInstanceOf( - IllegalArgumentException.class).hasMessageContaining("시작일이 종료일보다"); + assertThatThrownBy(() -> new Session(END_DATE, START_DATE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시작일이 종료일보다"); } @Test void sessionStatusIsPreparingOnCreation() { - Session session = new Session(START_DATE, END_DATE); + Session session = new SessionTestBuilder().free().build(); assertThat(session.status()).isEqualTo(SessionStatus.PREPARING); } @Test void whenCreatingFreeSession_thenMaxCapacityIsNull() { - Session freeSession = new Session(START_DATE, END_DATE, false, null); + Session freeSession = new SessionTestBuilder().free().build(); assertThat(freeSession.isPaid()).isFalse(); assertThat(freeSession.maxCapacity()).isNull(); } @Test void whenCreatingFreeSessionWithNonNullCapacity_thenThrow() { - assertThatThrownBy(() -> new Session(START_DATE, END_DATE, false, 10)) + assertThatThrownBy(() -> new SessionTestBuilder().free().maxCapacity(10).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("무료 강의는 최대 수강인원이 없어야 합니다"); } @Test void whenCreatingPaidSession_thenMaxCapacityMustBePositive() { - Session paidSession = new Session(START_DATE, END_DATE, true, 5, 100_000); + Session paidSession = new SessionTestBuilder().paid(5, 100_000).build(); assertThat(paidSession.isPaid()).isTrue(); assertThat(paidSession.maxCapacity()).isEqualTo(5); } @Test void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { - assertThatThrownBy(() -> new Session(START_DATE, END_DATE, true, null)) + assertThatThrownBy(() -> new SessionTestBuilder().paid(null, 100_000).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); - assertThatThrownBy(() -> new Session(START_DATE, END_DATE, true, 0)) + assertThatThrownBy(() -> new SessionTestBuilder().paid(0, 100_000).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); } @Test void whenCreatingFreeSession_thenFeeIsZero() { - Session freeSession = new Session(START_DATE, END_DATE, false, null, 0); + Session freeSession = new SessionTestBuilder().free().build(); assertThat(freeSession.fee()).isEqualTo(0); } @Test void whenCreatingFreeSessionWithInvalidFee_thenThrow() { - assertThatThrownBy(() -> new Session(START_DATE, END_DATE, false, null, 500_000)) + assertThatThrownBy(() -> new SessionTestBuilder().free().fee(500_000).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("무료 강의는 0원"); } @Test void whenCreatingPaidSession_thenFeeIsOverZero() { - Session paidSession = new Session(START_DATE, END_DATE, true, 5, 100_000); + Session paidSession = new SessionTestBuilder().paid(5, 100_000).build(); assertThat(paidSession.fee()).isEqualTo(100_000); } @Test void whenCreatingPaidSessionWithInvalidFee_thenThrow() { - assertThatThrownBy(() -> new Session(START_DATE, END_DATE, true, 5, 0)) + assertThatThrownBy(() -> new SessionTestBuilder().paid(5, 0).build()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("유료 강의는 0원 초과"); } @Test void whenSessionStatusIsOpen_thenCanEnrollIsTrue() { - Session session = new Session(START_DATE, END_DATE, false, null, 0); + Session session = new SessionTestBuilder().free().build(); session.startRecruiting(); assertThat(session.canEnroll()).isTrue(); } @Test void whenSessionStatusIsNotOpen_thenCanEnrollIsFalse() { - Session session = new Session(START_DATE, END_DATE, false, null, 0); + Session session = new SessionTestBuilder().free().build(); assertThat(session.canEnroll()).isFalse(); } @Test void whenEnrollCountIsOverMaxCapacity_thenCanEnrollIsFalse() { - Session session = new Session(START_DATE, END_DATE, true, 1, 100_000, 1); + Session session = new SessionTestBuilder() + .paid(1, 100_000) + .enrollCount(1) + .build(); assertThat(session.canEnroll()).isFalse(); } @Test void whenEnroll_thenEnrollCountIncrease() { - Session session = new Session(START_DATE, END_DATE, true, 1, 100_000); + Session session = new SessionTestBuilder().paid(1, 100_000).build(); session.startRecruiting(); int before = session.enrollCount(); session.enroll(); @@ -112,10 +116,12 @@ void whenEnroll_thenEnrollCountIncrease() { @Test void whenEnrollImpossible_thenThrow() { - Session session = new Session(START_DATE, END_DATE, true, 1, 100_000, 1); + Session session = new SessionTestBuilder() + .paid(1, 100_000) + .enrollCount(1) + .build(); assertThatThrownBy(session::enroll) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("수강 신청을 할 수 없습니다"); } - } diff --git a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java new file mode 100644 index 000000000..19fc7a901 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java @@ -0,0 +1,50 @@ +package nextstep.sessions.domain; + +import java.time.LocalDate; + +class SessionTestBuilder { + + private LocalDate startDate = SessionTest.START_DATE; + private LocalDate endDate = SessionTest.END_DATE; + private boolean paid = false; + private Integer maxCapacity = null; + private int fee = 0; + private int enrollCount = 0; + + public SessionTestBuilder paid(Integer maxCapacity, int fee) { + this.paid = true; + this.maxCapacity = maxCapacity; + this.fee = fee; + return this; + } + + public SessionTestBuilder free() { + this.paid = false; + this.maxCapacity = null; + this.fee = 0; + return this; + } + + public SessionTestBuilder maxCapacity(Integer maxCapacity) { + this.maxCapacity = maxCapacity; + return this; + } + + public SessionTestBuilder fee(int fee) { + this.fee = fee; + return this; + } + + public SessionTestBuilder enrollCount(int count) { + this.enrollCount = count; + return this; + } + + public Session build() { + if (paid) { + return new Session(startDate, endDate, true, maxCapacity, fee, enrollCount); + } else { + return new Session(startDate, endDate, false, maxCapacity, fee); + } + } +} From 3d04dca658a3f84c6a3f0e86137ed91869396d57 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 05:39:38 +0900 Subject: [PATCH 11/28] =?UTF-8?q?feat:=20Session=EC=97=90=20SessionImage?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9E=90=20=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Session.java | 23 +++++++++++-------- .../sessions/domain/SessionImageTest.java | 7 ++++++ .../nextstep/sessions/domain/SessionTest.java | 2 +- .../sessions/domain/SessionTestBuilder.java | 6 ++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 8876d50bf..9d4f7b811 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -20,23 +20,20 @@ public class Session { private int enrollCount; - public Session(LocalDate startDate, LocalDate endDate) { - this(startDate, endDate, false, null, 0); - } - - public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity) { - this(startDate, endDate, isPaid, maxCapacity, 0); - } + private SessionImage image; - Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, int enrollCount) { - this(startDate, endDate, isPaid, maxCapacity, fee); + Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, int enrollCount, + SessionImage image) { + this(startDate, endDate, isPaid, maxCapacity, fee, image); this.enrollCount = enrollCount; } - public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee) { + public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, + SessionImage image) { validateDate(startDate, endDate); validateCapacity(isPaid, maxCapacity); validateFee(isPaid, fee); + validateImage(image); this.startDate = startDate; this.endDate = endDate; this.status = SessionStatus.PREPARING; @@ -108,4 +105,10 @@ private void validateFee(boolean isPaid, int fee) { } } + private static void validateImage(SessionImage image) { + if (image == null) { + throw new IllegalArgumentException("강의 커버 이미지는 필수입니다."); + } + } + } diff --git a/src/test/java/nextstep/sessions/domain/SessionImageTest.java b/src/test/java/nextstep/sessions/domain/SessionImageTest.java index f4ab2b224..8a9752711 100644 --- a/src/test/java/nextstep/sessions/domain/SessionImageTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionImageTest.java @@ -7,6 +7,13 @@ class SessionImageTest { + public static final SessionImage IMAGE = new SessionImage( + "cover.png", + 500_000, + 300, + 200 + ); + @Test void createImage_success() { SessionImage image = new SessionImage( diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index d7c7799dc..ac37685c5 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -13,7 +13,7 @@ class SessionTest { @Test void startDateMustBeBeforeEndDate() { - assertThatThrownBy(() -> new Session(END_DATE, START_DATE)) + assertThatThrownBy(() -> new Session(END_DATE, START_DATE, false, null, 0, SessionImageTest.IMAGE)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("시작일이 종료일보다"); } diff --git a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java index 19fc7a901..23167b30b 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java +++ b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java @@ -10,6 +10,7 @@ class SessionTestBuilder { private Integer maxCapacity = null; private int fee = 0; private int enrollCount = 0; + private SessionImage image = SessionImageTest.IMAGE; public SessionTestBuilder paid(Integer maxCapacity, int fee) { this.paid = true; @@ -42,9 +43,8 @@ public SessionTestBuilder enrollCount(int count) { public Session build() { if (paid) { - return new Session(startDate, endDate, true, maxCapacity, fee, enrollCount); - } else { - return new Session(startDate, endDate, false, maxCapacity, fee); + return new Session(startDate, endDate, true, maxCapacity, fee, enrollCount, image); } + return new Session(startDate, endDate, false, maxCapacity, fee, image); } } From 3d8f3a0caa028cd694a4df9a86ba9d01deaa097d Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sat, 13 Dec 2025 06:48:12 +0900 Subject: [PATCH 12/28] =?UTF-8?q?feat:=20=EC=88=98=EA=B0=95=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84,=20=EC=88=98=EA=B0=95=20=EC=A0=95=EC=9B=90,=20?= =?UTF-8?q?=EC=88=98=EA=B0=95=EB=A3=8C=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=B4=EB=8A=94=20VO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Capacity.java | 64 +++++++++++++++++++ .../java/nextstep/sessions/domain/Period.java | 25 ++++++++ .../sessions/domain/SessionPricing.java | 35 ++++++++++ .../sessions/domain/CapacityTest.java | 45 +++++++++++++ .../nextstep/sessions/domain/PeriodTest.java | 20 ++++++ .../sessions/domain/SessionPricingTest.java | 35 ++++++++++ 6 files changed, 224 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/Capacity.java create mode 100644 src/main/java/nextstep/sessions/domain/Period.java create mode 100644 src/main/java/nextstep/sessions/domain/SessionPricing.java create mode 100644 src/test/java/nextstep/sessions/domain/CapacityTest.java create mode 100644 src/test/java/nextstep/sessions/domain/PeriodTest.java create mode 100644 src/test/java/nextstep/sessions/domain/SessionPricingTest.java diff --git a/src/main/java/nextstep/sessions/domain/Capacity.java b/src/main/java/nextstep/sessions/domain/Capacity.java new file mode 100644 index 000000000..00bf8f1e8 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Capacity.java @@ -0,0 +1,64 @@ +package nextstep.sessions.domain; + +public class Capacity { + + private static final String ERROR_MAX_CAPACITY_REQUIRED = "유료 강의는 최대 수강인원이 있어야 합니다"; + private static final String ERROR_ENROLL_COUNT_NEGATIVE = "수강 인원은 0 이상이어야 합니다"; + private static final String ERROR_ENROLL_COUNT_EXCEED = "수강 인원은 수강 정원을 초과할 수 없습니다"; + private static final String ERROR_CANNOT_ENROLL = "수강 신청을 할 수 없습니다"; + + private static final int DEFAULT_ENROLL_COUNT = 0; + + private final Integer maxCapacity; + private final int enrollCount; + + public Capacity(Integer maxCapacity) { + this(maxCapacity, DEFAULT_ENROLL_COUNT); + } + + Capacity(Integer maxCapacity, int enrollCount) { + validateMaxCapacity(maxCapacity); + validateEnrollCount(enrollCount); + validateEnrollCountWithinCapacity(maxCapacity, enrollCount); + + this.maxCapacity = maxCapacity; + this.enrollCount = enrollCount; + } + + public Integer maxCapacity() { + return maxCapacity; + } + + public int enrollCount() { + return enrollCount; + } + + public boolean canEnroll() { + return maxCapacity == null || enrollCount < maxCapacity; + } + + public Capacity increaseEnrollCount() { + if (!canEnroll()) { + throw new IllegalArgumentException(ERROR_CANNOT_ENROLL); + } + return new Capacity(maxCapacity, enrollCount + 1); + } + + private static void validateMaxCapacity(Integer maxCapacity) { + if (maxCapacity != null && maxCapacity <= 0) { + throw new IllegalArgumentException(ERROR_MAX_CAPACITY_REQUIRED); + } + } + + private static void validateEnrollCount(int enrollCount) { + if (enrollCount < 0) { + throw new IllegalArgumentException(ERROR_ENROLL_COUNT_NEGATIVE); + } + } + + private static void validateEnrollCountWithinCapacity(Integer maxCapacity, int enrollCount) { + if (maxCapacity != null && enrollCount > maxCapacity) { + throw new IllegalArgumentException(ERROR_ENROLL_COUNT_EXCEED); + } + } +} diff --git a/src/main/java/nextstep/sessions/domain/Period.java b/src/main/java/nextstep/sessions/domain/Period.java new file mode 100644 index 000000000..0d2fdd06e --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Period.java @@ -0,0 +1,25 @@ +package nextstep.sessions.domain; + +import java.time.LocalDate; + +public class Period { + + private static final String ERROR_INVALID_DATE = "시작일이 종료일보다 빨라야 합니다"; + + private final LocalDate startDate; + + private final LocalDate endDate; + + public Period(LocalDate startDate, LocalDate endDate) { + validateDate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validateDate(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException(ERROR_INVALID_DATE); + } + } + +} diff --git a/src/main/java/nextstep/sessions/domain/SessionPricing.java b/src/main/java/nextstep/sessions/domain/SessionPricing.java new file mode 100644 index 000000000..78470970c --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionPricing.java @@ -0,0 +1,35 @@ +package nextstep.sessions.domain; + +public class SessionPricing { + + private static final String ERROR_PAID_FEE = "유료 강의는 0원 초과 여야 합니다"; + private static final String ERROR_FREE_FEE = "무료 강의는 0원 이어야 합니다"; + + private final boolean isPaid; + private final int fee; + + public SessionPricing(boolean isPaid, int fee) { + validateFee(isPaid, fee); + this.isPaid = isPaid; + this.fee = fee; + } + + public boolean isPaid() { + return isPaid; + } + + public int fee() { + return fee; + } + + private void validateFee(boolean isPaid, int fee) { + if (isPaid && fee <= 0) { + throw new IllegalArgumentException(ERROR_PAID_FEE); + } + if (!isPaid && fee != 0) { + throw new IllegalArgumentException(ERROR_FREE_FEE); + } + } + +} + diff --git a/src/test/java/nextstep/sessions/domain/CapacityTest.java b/src/test/java/nextstep/sessions/domain/CapacityTest.java new file mode 100644 index 000000000..126f25bab --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/CapacityTest.java @@ -0,0 +1,45 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class CapacityTest { + + @Test + void freeCapacity_maxCapacityIsNull() { + Capacity freeCapacity = new Capacity(null); + assertThat(freeCapacity.maxCapacity()).isNull(); + assertThat(freeCapacity.canEnroll()).isTrue(); + } + + @Test + void paidCapacity_maxCapacityMustBePositive() { + Capacity paidCapacity = new Capacity(5, 0); + assertThat(paidCapacity.maxCapacity()).isEqualTo(5); + assertThat(paidCapacity.canEnroll()).isTrue(); + } + + @Test + void paidCapacity_invalidMaxCapacity_throwsException() { + assertThatThrownBy(() -> new Capacity(0, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); + } + + @Test + void enrollCountExceedsMax_throwsException() { + assertThatThrownBy(() -> new Capacity(1, 2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("수강 인원은 수강 정원을 초과할 수 없습니다"); + } + + @Test + void increaseEnrollCount_incrementsEnrollCountByOne() { + Capacity capacity = new Capacity(null); + Capacity afterIncrease = capacity.increaseEnrollCount(); + assertThat(afterIncrease.enrollCount() - capacity.enrollCount()).isEqualTo(1); + } + +} diff --git a/src/test/java/nextstep/sessions/domain/PeriodTest.java b/src/test/java/nextstep/sessions/domain/PeriodTest.java new file mode 100644 index 000000000..ff8028225 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/PeriodTest.java @@ -0,0 +1,20 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDate; +import org.junit.jupiter.api.Test; + +class PeriodTest { + + static final LocalDate START_DATE = LocalDate.of(2025, 11, 3); + static final LocalDate END_DATE = LocalDate.of(2025, 12, 18); + + @Test + void startDateMustBeBeforeEndDate() { + assertThatThrownBy(() -> new Period(END_DATE, START_DATE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("시작일이 종료일보다"); + } + +} \ No newline at end of file diff --git a/src/test/java/nextstep/sessions/domain/SessionPricingTest.java b/src/test/java/nextstep/sessions/domain/SessionPricingTest.java new file mode 100644 index 000000000..f162af12d --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/SessionPricingTest.java @@ -0,0 +1,35 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class SessionPricingTest { + + @Test + void freeSession_feeIsZero() { + SessionPricing freeSessionPricing = new SessionPricing(false, 0); + assertThat(freeSessionPricing.fee()).isEqualTo(0); + } + + @Test + void freeSession_invalidFee_throwsException() { + assertThatThrownBy(() -> new SessionPricing(false, 500_000)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("무료 강의는 0원 이어야 합니다"); + } + + @Test + void paidSession_feeIsPositive() { + SessionPricing paidSessionPricing = new SessionPricing(true, 100_000); + assertThat(paidSessionPricing.fee()).isEqualTo(100_000); + } + + @Test + void paidSession_invalidFee_throwsException() { + assertThatThrownBy(() -> new SessionPricing(true, 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("유료 강의는 0원 초과 여야 합니다"); + } +} From 1de7c1019335c68efb971ad84555494832ab3938 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:04:07 +0900 Subject: [PATCH 13/28] =?UTF-8?q?refactor:=20Session=EC=97=90=20Period,=20?= =?UTF-8?q?Capacity,=20SessionPricing=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Capacity.java | 4 + .../nextstep/sessions/domain/Session.java | 76 +++++------------- .../nextstep/sessions/domain/SessionTest.java | 77 ------------------- .../sessions/domain/SessionTestBuilder.java | 4 +- 4 files changed, 25 insertions(+), 136 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Capacity.java b/src/main/java/nextstep/sessions/domain/Capacity.java index 00bf8f1e8..58b709e54 100644 --- a/src/main/java/nextstep/sessions/domain/Capacity.java +++ b/src/main/java/nextstep/sessions/domain/Capacity.java @@ -37,6 +37,10 @@ public boolean canEnroll() { return maxCapacity == null || enrollCount < maxCapacity; } + public boolean isUnlimited() { + return maxCapacity == null; + } + public Capacity increaseEnrollCount() { if (!canEnroll()) { throw new IllegalArgumentException(ERROR_CANNOT_ENROLL); diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 9d4f7b811..71ea51804 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -4,67 +4,43 @@ public class Session { - static final String ERROR_INVALID_DATE = "시작일이 종료일보다 빨라야 합니다"; - - private final LocalDate startDate; - - private final LocalDate endDate; + private final Period period; private SessionStatus status; - private final boolean isPaid; - - private final Integer maxCapacity; + private final SessionPricing pricing; - private final int fee; - - private int enrollCount; + private Capacity capacity; private SessionImage image; Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, int enrollCount, SessionImage image) { this(startDate, endDate, isPaid, maxCapacity, fee, image); - this.enrollCount = enrollCount; } public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, SessionImage image) { - validateDate(startDate, endDate); - validateCapacity(isPaid, maxCapacity); - validateFee(isPaid, fee); + this(new Period(startDate, endDate), new SessionPricing(isPaid, fee), new Capacity(maxCapacity), image); + } + + public Session(Period period, SessionPricing pricing, Capacity capacity, + SessionImage image) { + validatePricingAndCapacity(pricing, capacity); validateImage(image); - this.startDate = startDate; - this.endDate = endDate; + this.period = period; this.status = SessionStatus.PREPARING; - this.isPaid = isPaid; - this.maxCapacity = maxCapacity; - this.fee = fee; - this.enrollCount = 0; + this.pricing = pricing; + this.capacity = capacity; + this.image = image; } public SessionStatus status() { return status; } - public boolean isPaid() { - return isPaid; - } - - public Integer maxCapacity() { - return maxCapacity; - } - - public int fee() { - return fee; - } - - public int enrollCount() { - return enrollCount; - } - public boolean canEnroll() { - if (isPaid() && enrollCount >= maxCapacity) { + if (!capacity.canEnroll()) { return false; } return status == SessionStatus.OPEN; @@ -78,30 +54,16 @@ public void enroll() { if (!canEnroll()) { throw new IllegalArgumentException("수강 신청을 할 수 없습니다"); } - enrollCount++; - } - - private void validateDate(LocalDate startDate, LocalDate endDate) { - if (startDate.isAfter(endDate)) { - throw new IllegalArgumentException(ERROR_INVALID_DATE); - } + this.capacity = capacity.increaseEnrollCount(); } - private void validateCapacity(boolean isPaid, Integer maxCapacity) { - if (isPaid && (maxCapacity == null || maxCapacity <= 0)) { + private void validatePricingAndCapacity(SessionPricing pricing, Capacity capacity) { + if (pricing.isPaid() && capacity.isUnlimited()) { throw new IllegalArgumentException("유료 강의는 최대 수강인원이 있어야 합니다"); } - if (!isPaid && maxCapacity != null) { - throw new IllegalArgumentException("무료 강의는 최대 수강인원이 없어야 합니다"); - } - } - private void validateFee(boolean isPaid, int fee) { - if (isPaid && fee <= 0) { - throw new IllegalArgumentException("유료 강의는 0원 초과 여야 합니다"); - } - if (!isPaid && fee != 0) { - throw new IllegalArgumentException("무료 강의는 0원 이어야 합니다"); + if (!pricing.isPaid() && !capacity.isUnlimited()) { + throw new IllegalArgumentException("무료 강의는 최대 수강 인원이 없어야 합니다"); } } diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index ac37685c5..dc9dae719 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -3,48 +3,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.time.LocalDate; import org.junit.jupiter.api.Test; class SessionTest { - static final LocalDate START_DATE = LocalDate.of(2025, 11, 3); - static final LocalDate END_DATE = LocalDate.of(2025, 12, 18); - - @Test - void startDateMustBeBeforeEndDate() { - assertThatThrownBy(() -> new Session(END_DATE, START_DATE, false, null, 0, SessionImageTest.IMAGE)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("시작일이 종료일보다"); - } - @Test void sessionStatusIsPreparingOnCreation() { Session session = new SessionTestBuilder().free().build(); assertThat(session.status()).isEqualTo(SessionStatus.PREPARING); } - @Test - void whenCreatingFreeSession_thenMaxCapacityIsNull() { - Session freeSession = new SessionTestBuilder().free().build(); - assertThat(freeSession.isPaid()).isFalse(); - assertThat(freeSession.maxCapacity()).isNull(); - } - - @Test - void whenCreatingFreeSessionWithNonNullCapacity_thenThrow() { - assertThatThrownBy(() -> new SessionTestBuilder().free().maxCapacity(10).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("무료 강의는 최대 수강인원이 없어야 합니다"); - } - - @Test - void whenCreatingPaidSession_thenMaxCapacityMustBePositive() { - Session paidSession = new SessionTestBuilder().paid(5, 100_000).build(); - assertThat(paidSession.isPaid()).isTrue(); - assertThat(paidSession.maxCapacity()).isEqualTo(5); - } - @Test void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { assertThatThrownBy(() -> new SessionTestBuilder().paid(null, 100_000).build()) @@ -56,32 +24,6 @@ void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); } - @Test - void whenCreatingFreeSession_thenFeeIsZero() { - Session freeSession = new SessionTestBuilder().free().build(); - assertThat(freeSession.fee()).isEqualTo(0); - } - - @Test - void whenCreatingFreeSessionWithInvalidFee_thenThrow() { - assertThatThrownBy(() -> new SessionTestBuilder().free().fee(500_000).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("무료 강의는 0원"); - } - - @Test - void whenCreatingPaidSession_thenFeeIsOverZero() { - Session paidSession = new SessionTestBuilder().paid(5, 100_000).build(); - assertThat(paidSession.fee()).isEqualTo(100_000); - } - - @Test - void whenCreatingPaidSessionWithInvalidFee_thenThrow() { - assertThatThrownBy(() -> new SessionTestBuilder().paid(5, 0).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("유료 강의는 0원 초과"); - } - @Test void whenSessionStatusIsOpen_thenCanEnrollIsTrue() { Session session = new SessionTestBuilder().free().build(); @@ -95,25 +37,6 @@ void whenSessionStatusIsNotOpen_thenCanEnrollIsFalse() { assertThat(session.canEnroll()).isFalse(); } - @Test - void whenEnrollCountIsOverMaxCapacity_thenCanEnrollIsFalse() { - Session session = new SessionTestBuilder() - .paid(1, 100_000) - .enrollCount(1) - .build(); - assertThat(session.canEnroll()).isFalse(); - } - - @Test - void whenEnroll_thenEnrollCountIncrease() { - Session session = new SessionTestBuilder().paid(1, 100_000).build(); - session.startRecruiting(); - int before = session.enrollCount(); - session.enroll(); - int after = session.enrollCount(); - assertThat(after - before).isEqualTo(1); - } - @Test void whenEnrollImpossible_thenThrow() { Session session = new SessionTestBuilder() diff --git a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java index 23167b30b..00bd426f3 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java +++ b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java @@ -4,8 +4,8 @@ class SessionTestBuilder { - private LocalDate startDate = SessionTest.START_DATE; - private LocalDate endDate = SessionTest.END_DATE; + private LocalDate startDate = PeriodTest.START_DATE; + private LocalDate endDate = PeriodTest.END_DATE; private boolean paid = false; private Integer maxCapacity = null; private int fee = 0; From 3151975cf6a2843743962ccfb4a35bc4d1481112 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:24:45 +0900 Subject: [PATCH 14/28] =?UTF-8?q?feat:=20Session=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=B6=88=EB=B3=80=20=EC=A0=95=EB=B3=B4=20SessionInfo=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/SessionInfo.java | 30 +++++++++++++++++++ .../nextstep/sessions/domain/PeriodTest.java | 1 + .../sessions/domain/SessionInfoTest.java | 15 ++++++++++ .../sessions/domain/SessionPricingTest.java | 3 ++ 4 files changed, 49 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/SessionInfo.java create mode 100644 src/test/java/nextstep/sessions/domain/SessionInfoTest.java diff --git a/src/main/java/nextstep/sessions/domain/SessionInfo.java b/src/main/java/nextstep/sessions/domain/SessionInfo.java new file mode 100644 index 000000000..736a7a558 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionInfo.java @@ -0,0 +1,30 @@ +package nextstep.sessions.domain; + +public class SessionInfo { + + static final String ERROR_COVER_IMAGE_REQUIRED = "강의 커버 이미지는 필수입니다"; + + private final Period period; + + private final SessionPricing pricing; + + private SessionImage image; + + public SessionInfo(Period period, SessionPricing pricing, SessionImage image) { + validateImage(image); + this.period = period; + this.pricing = pricing; + this.image = image; + } + + public SessionPricing pricing() { + return pricing; + } + + private static void validateImage(SessionImage image) { + if (image == null) { + throw new IllegalArgumentException(ERROR_COVER_IMAGE_REQUIRED); + } + } + +} diff --git a/src/test/java/nextstep/sessions/domain/PeriodTest.java b/src/test/java/nextstep/sessions/domain/PeriodTest.java index ff8028225..4fd0b9d25 100644 --- a/src/test/java/nextstep/sessions/domain/PeriodTest.java +++ b/src/test/java/nextstep/sessions/domain/PeriodTest.java @@ -9,6 +9,7 @@ class PeriodTest { static final LocalDate START_DATE = LocalDate.of(2025, 11, 3); static final LocalDate END_DATE = LocalDate.of(2025, 12, 18); + public static final Period P1 = new Period(START_DATE, END_DATE); @Test void startDateMustBeBeforeEndDate() { diff --git a/src/test/java/nextstep/sessions/domain/SessionInfoTest.java b/src/test/java/nextstep/sessions/domain/SessionInfoTest.java new file mode 100644 index 000000000..1dedd0ae9 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/SessionInfoTest.java @@ -0,0 +1,15 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class SessionInfoTest { + + @Test + void imageNull_throwsExcepiton() { + assertThatThrownBy(() -> new SessionInfo(PeriodTest.P1, SessionPricingTest.FREE_SP, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("강의 커버 이미지는 필수입니다"); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/sessions/domain/SessionPricingTest.java b/src/test/java/nextstep/sessions/domain/SessionPricingTest.java index f162af12d..85b9da69d 100644 --- a/src/test/java/nextstep/sessions/domain/SessionPricingTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionPricingTest.java @@ -7,6 +7,9 @@ class SessionPricingTest { + public static final SessionPricing FREE_SP = new SessionPricing(false, 0); + public static final SessionPricing PAID_SP = new SessionPricing(true, 100_000); + @Test void freeSession_feeIsZero() { SessionPricing freeSessionPricing = new SessionPricing(false, 0); From 35191a74b6f57c4dfc4a67e5e799ec72f96a735a Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:47:27 +0900 Subject: [PATCH 15/28] =?UTF-8?q?feat:=20EnrollmentPolicy=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=B2=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/payments/domain/Payment.java | 4 +++ .../nextstep/sessions/domain/Capacity.java | 8 +++++ .../sessions/domain/EnrollmentPolicy.java | 10 ++++++ .../sessions/domain/FreeEnrollmentPolicy.java | 20 ++++++++++++ .../sessions/domain/PaidEnrollmentPolicy.java | 25 +++++++++++++++ .../nextstep/payments/domain/PaymentTest.java | 31 +++++++++++++++++++ .../sessions/domain/CapacityTest.java | 12 +++++++ .../domain/FreeEnrollmentPolicyTest.java | 26 ++++++++++++++++ .../domain/PaidEnrollmentPolicyTest.java | 26 ++++++++++++++++ 9 files changed, 162 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java create mode 100644 src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java create mode 100644 src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java create mode 100644 src/test/java/nextstep/payments/domain/PaymentTest.java create mode 100644 src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java create mode 100644 src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 57d833f85..bf7f7aad1 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -26,4 +26,8 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.amount = amount; this.createdAt = LocalDateTime.now(); } + + public boolean isPaidFor(int fee) { + return this.amount == fee; + } } diff --git a/src/main/java/nextstep/sessions/domain/Capacity.java b/src/main/java/nextstep/sessions/domain/Capacity.java index 58b709e54..d2a13be39 100644 --- a/src/main/java/nextstep/sessions/domain/Capacity.java +++ b/src/main/java/nextstep/sessions/domain/Capacity.java @@ -41,6 +41,14 @@ public boolean isUnlimited() { return maxCapacity == null; } + public boolean isFull() { + return enrollCount >= maxCapacity; + } + + public boolean hasAvailableSeat() { + return !isFull(); + } + public Capacity increaseEnrollCount() { if (!canEnroll()) { throw new IllegalArgumentException(ERROR_CANNOT_ENROLL); diff --git a/src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java b/src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java new file mode 100644 index 000000000..6b9e0629c --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java @@ -0,0 +1,10 @@ +package nextstep.sessions.domain; + +import nextstep.payments.domain.Payment; + +public interface EnrollmentPolicy { + + boolean canEnroll(Capacity capacity, Payment payment); + + void validate(Capacity capacity); +} diff --git a/src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java b/src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java new file mode 100644 index 000000000..b23cc87b5 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java @@ -0,0 +1,20 @@ +package nextstep.sessions.domain; + +import nextstep.payments.domain.Payment; + +public class FreeEnrollmentPolicy implements EnrollmentPolicy { + + static final String ERROR_FREE_COURSE_CAPACITY_MUST_BE_UNLIMITED = "무료 강의는 최대 수강 인원이 없어야 합니다"; + + @Override + public boolean canEnroll(Capacity capacity, Payment payment) { + return true; + } + + @Override + public void validate(Capacity capacity) { + if (!capacity.isUnlimited()) { + throw new IllegalArgumentException(ERROR_FREE_COURSE_CAPACITY_MUST_BE_UNLIMITED); + } + } +} diff --git a/src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java b/src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java new file mode 100644 index 000000000..65599897e --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java @@ -0,0 +1,25 @@ +package nextstep.sessions.domain; + +import nextstep.payments.domain.Payment; + +public class PaidEnrollmentPolicy implements EnrollmentPolicy { + + static final String ERROR_PAID_SESSION_CAPACITY_REQUIRED = "유료 강의는 최대 수강 인원이 있어야 합니다"; + private final int fee; + + public PaidEnrollmentPolicy(int fee) { + this.fee = fee; + } + + @Override + public boolean canEnroll(Capacity capacity, Payment payment) { + return capacity.hasAvailableSeat() && payment != null && payment.isPaidFor(fee); + } + + @Override + public void validate(Capacity capacity) { + if (capacity.isUnlimited()) { + throw new IllegalArgumentException(ERROR_PAID_SESSION_CAPACITY_REQUIRED); + } + } +} diff --git a/src/test/java/nextstep/payments/domain/PaymentTest.java b/src/test/java/nextstep/payments/domain/PaymentTest.java new file mode 100644 index 000000000..45909a652 --- /dev/null +++ b/src/test/java/nextstep/payments/domain/PaymentTest.java @@ -0,0 +1,31 @@ +package nextstep.payments.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class PaymentTest { + private static final Payment PAYMENT_1000 = new Payment("p1", 1L, 1L, 1000L); + + @Test + void isPaidFor_returnsTrue_whenAmountMatchesFee() { + assertThat(PAYMENT_1000.isPaidFor(1000)).isTrue(); + } + + @Test + void isPaidFor_returnsFalse_whenAmountDoesNotMatchFee() { + assertThat(PAYMENT_1000.isPaidFor(1500)).isFalse(); + } + + @Test + void isPaidFor_returnsFalse_whenFeeIsZero() { + assertThat(PAYMENT_1000.isPaidFor(0)).isFalse(); + } + + @Test + void isPaidFor_returnsTrue_forLargeAmounts() { + Payment largePayment = new Payment("p1", 1L, 1L, 10_000_000L); + assertThat(largePayment.isPaidFor(10_000_000)).isTrue(); + } + +} diff --git a/src/test/java/nextstep/sessions/domain/CapacityTest.java b/src/test/java/nextstep/sessions/domain/CapacityTest.java index 126f25bab..567044af5 100644 --- a/src/test/java/nextstep/sessions/domain/CapacityTest.java +++ b/src/test/java/nextstep/sessions/domain/CapacityTest.java @@ -35,6 +35,18 @@ void enrollCountExceedsMax_throwsException() { .hasMessageContaining("수강 인원은 수강 정원을 초과할 수 없습니다"); } + @Test + void enrollEqualsMax_isFullReturnsTrue() { + Capacity capacity = new Capacity(3, 3); + assertThat(capacity.isFull()).isTrue(); + } + + @Test + void enrollLessThanMax_isFullReturnsFalse() { + Capacity capacity = new Capacity(3, 2); + assertThat(capacity.isFull()).isFalse(); + } + @Test void increaseEnrollCount_incrementsEnrollCountByOne() { Capacity capacity = new Capacity(null); diff --git a/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java b/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java new file mode 100644 index 000000000..8da4b3d7c --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java @@ -0,0 +1,26 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class FreeEnrollmentPolicyTest { + + @Test + void validate_throwsException_whenCapacityIsLimited() { + Capacity limitedCapacity = new Capacity(3); + FreeEnrollmentPolicy policy = new FreeEnrollmentPolicy(); + assertThatThrownBy(() -> policy.validate(limitedCapacity)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(FreeEnrollmentPolicy.ERROR_FREE_COURSE_CAPACITY_MUST_BE_UNLIMITED); + } + + @Test + void validate_passes_whenCapacityIsUnlimited() { + Capacity unlimitedCapacity = new Capacity(null); + FreeEnrollmentPolicy policy = new FreeEnrollmentPolicy(); + assertThatCode(() -> policy.validate(unlimitedCapacity)) + .doesNotThrowAnyException(); + } +} diff --git a/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java b/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java new file mode 100644 index 000000000..c140b4cae --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java @@ -0,0 +1,26 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class PaidEnrollmentPolicyTest { + + @Test + void validate_throwsException_whenCapacityIsUnlimited() { + Capacity unlimitedCapacity = new Capacity(null); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(1000); + assertThatThrownBy(() -> policy.validate(unlimitedCapacity)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(PaidEnrollmentPolicy.ERROR_PAID_SESSION_CAPACITY_REQUIRED); + } + + @Test + void validate_passes_whenCapacityIsLimited() { + Capacity limitedCapacity = new Capacity(3); + PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(1000); + assertThatCode(() -> policy.validate(limitedCapacity)) + .doesNotThrowAnyException(); + } +} From 8340a2c03c51f12e83d0096db55fb8868fdeb622 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:14:10 +0900 Subject: [PATCH 16/28] =?UTF-8?q?feat:=20SessionPricing=EC=9D=84=20Session?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20and=20SessionInfo=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=9D=BC=EB=B6=80=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C,=20EnrollmentPolicy=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Session.java | 38 ++++++++++--------- .../nextstep/sessions/domain/SessionInfo.java | 9 +---- .../nextstep/payments/domain/PaymentTest.java | 4 +- .../sessions/domain/SessionInfoTest.java | 2 +- .../nextstep/sessions/domain/SessionTest.java | 7 ++-- 5 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 71ea51804..029745abb 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -1,10 +1,11 @@ package nextstep.sessions.domain; import java.time.LocalDate; +import nextstep.payments.domain.Payment; public class Session { - private final Period period; + private SessionInfo sessionInfo; private SessionStatus status; @@ -12,7 +13,7 @@ public class Session { private Capacity capacity; - private SessionImage image; + private EnrollmentPolicy enrollmentPolicy; Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, int enrollCount, SessionImage image) { @@ -21,37 +22,33 @@ public class Session { public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, SessionImage image) { - this(new Period(startDate, endDate), new SessionPricing(isPaid, fee), new Capacity(maxCapacity), image); + this(new SessionInfo(new Period(startDate, endDate), image), + new SessionPricing(isPaid, fee), new Capacity(maxCapacity)); } - public Session(Period period, SessionPricing pricing, Capacity capacity, - SessionImage image) { + public Session(SessionInfo sessionInfo, SessionPricing pricing, Capacity capacity) { validatePricingAndCapacity(pricing, capacity); - validateImage(image); - this.period = period; + this.sessionInfo = sessionInfo; this.status = SessionStatus.PREPARING; this.pricing = pricing; this.capacity = capacity; - this.image = image; + this.enrollmentPolicy = createEnrollmentPolicy(pricing); } public SessionStatus status() { return status; } - public boolean canEnroll() { - if (!capacity.canEnroll()) { - return false; - } - return status == SessionStatus.OPEN; + public boolean canEnroll(Payment payment) { + return enrollmentPolicy.canEnroll(capacity, payment) && isOpen(); } public void startRecruiting() { this.status = SessionStatus.OPEN; } - public void enroll() { - if (!canEnroll()) { + public void enroll(Payment payment) { + if (!canEnroll(payment)) { throw new IllegalArgumentException("수강 신청을 할 수 없습니다"); } this.capacity = capacity.increaseEnrollCount(); @@ -67,10 +64,15 @@ private void validatePricingAndCapacity(SessionPricing pricing, Capacity capacit } } - private static void validateImage(SessionImage image) { - if (image == null) { - throw new IllegalArgumentException("강의 커버 이미지는 필수입니다."); + private EnrollmentPolicy createEnrollmentPolicy(SessionPricing pricing) { + if (pricing.isPaid()) { + return new PaidEnrollmentPolicy(pricing.fee()); } + return new FreeEnrollmentPolicy(); + } + + private boolean isOpen() { + return status == SessionStatus.OPEN; } } diff --git a/src/main/java/nextstep/sessions/domain/SessionInfo.java b/src/main/java/nextstep/sessions/domain/SessionInfo.java index 736a7a558..ebf0ea8ca 100644 --- a/src/main/java/nextstep/sessions/domain/SessionInfo.java +++ b/src/main/java/nextstep/sessions/domain/SessionInfo.java @@ -6,21 +6,14 @@ public class SessionInfo { private final Period period; - private final SessionPricing pricing; - private SessionImage image; - public SessionInfo(Period period, SessionPricing pricing, SessionImage image) { + public SessionInfo(Period period, SessionImage image) { validateImage(image); this.period = period; - this.pricing = pricing; this.image = image; } - public SessionPricing pricing() { - return pricing; - } - private static void validateImage(SessionImage image) { if (image == null) { throw new IllegalArgumentException(ERROR_COVER_IMAGE_REQUIRED); diff --git a/src/test/java/nextstep/payments/domain/PaymentTest.java b/src/test/java/nextstep/payments/domain/PaymentTest.java index 45909a652..107073b19 100644 --- a/src/test/java/nextstep/payments/domain/PaymentTest.java +++ b/src/test/java/nextstep/payments/domain/PaymentTest.java @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test; -class PaymentTest { - private static final Payment PAYMENT_1000 = new Payment("p1", 1L, 1L, 1000L); +public class PaymentTest { + public static final Payment PAYMENT_1000 = new Payment("p1", 1L, 1L, 1000L); @Test void isPaidFor_returnsTrue_whenAmountMatchesFee() { diff --git a/src/test/java/nextstep/sessions/domain/SessionInfoTest.java b/src/test/java/nextstep/sessions/domain/SessionInfoTest.java index 1dedd0ae9..6463ae57e 100644 --- a/src/test/java/nextstep/sessions/domain/SessionInfoTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionInfoTest.java @@ -8,7 +8,7 @@ class SessionInfoTest { @Test void imageNull_throwsExcepiton() { - assertThatThrownBy(() -> new SessionInfo(PeriodTest.P1, SessionPricingTest.FREE_SP, null)) + assertThatThrownBy(() -> new SessionInfo(PeriodTest.P1, null)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("강의 커버 이미지는 필수입니다"); } diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index dc9dae719..8a654ffb5 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import nextstep.payments.domain.PaymentTest; import org.junit.jupiter.api.Test; class SessionTest { @@ -28,13 +29,13 @@ void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { void whenSessionStatusIsOpen_thenCanEnrollIsTrue() { Session session = new SessionTestBuilder().free().build(); session.startRecruiting(); - assertThat(session.canEnroll()).isTrue(); + assertThat(session.canEnroll(PaymentTest.PAYMENT_1000)).isTrue(); } @Test void whenSessionStatusIsNotOpen_thenCanEnrollIsFalse() { Session session = new SessionTestBuilder().free().build(); - assertThat(session.canEnroll()).isFalse(); + assertThat(session.canEnroll(PaymentTest.PAYMENT_1000)).isFalse(); } @Test @@ -43,7 +44,7 @@ void whenEnrollImpossible_thenThrow() { .paid(1, 100_000) .enrollCount(1) .build(); - assertThatThrownBy(session::enroll) + assertThatThrownBy(() -> session.enroll(PaymentTest.PAYMENT_1000)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("수강 신청을 할 수 없습니다"); } From e834e6dc5ffcee37fca38dd662871831d29b2338 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:26:34 +0900 Subject: [PATCH 17/28] =?UTF-8?q?feat:=20ImageDimension=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sessions/domain/ImageDimension.java | 29 ++++++++++++++++++ .../sessions/domain/ImageDimensionTest.java | 30 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/ImageDimension.java create mode 100644 src/test/java/nextstep/sessions/domain/ImageDimensionTest.java diff --git a/src/main/java/nextstep/sessions/domain/ImageDimension.java b/src/main/java/nextstep/sessions/domain/ImageDimension.java new file mode 100644 index 000000000..fd5b033df --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/ImageDimension.java @@ -0,0 +1,29 @@ +package nextstep.sessions.domain; + +public class ImageDimension { + + private static final int MIN_WIDTH = 300; + private static final int MIN_HEIGHT = 200; + + private static final String ERROR_MIN_SIZE = "이미지 크기가 최소 조건을 만족하지 않습니다"; + private static final String ERROR_RATIO = "이미지 비율은 3:2여야 합니다"; + + private final int width; + private final int height; + + public ImageDimension(int width, int height) { + validate(width, height); + this.width = width; + this.height = height; + } + + private void validate(int width, int height) { + if (width < MIN_WIDTH || height < MIN_HEIGHT) { + throw new IllegalArgumentException(ERROR_MIN_SIZE); + } + if (width * 2 != height * 3) { + throw new IllegalArgumentException(ERROR_RATIO); + } + } + +} diff --git a/src/test/java/nextstep/sessions/domain/ImageDimensionTest.java b/src/test/java/nextstep/sessions/domain/ImageDimensionTest.java new file mode 100644 index 000000000..fef521c01 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/ImageDimensionTest.java @@ -0,0 +1,30 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +class ImageDimensionTest { + + @Test + void whenImageWidthIsTooSmall_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ImageDimension(299, 200) + ).withMessageContaining("이미지 크기"); + } + + @Test + void whenImageHeightIsTooSmall_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ImageDimension(300, 199) + ).withMessageContaining("이미지 크기"); + } + + @Test + void whenImageRatioIsNot3To2_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ImageDimension(310, 200) + ).withMessageContaining("3:2"); + } + +} \ No newline at end of file From bbbc5b7d38ec60d615a0f7b02a71302a83c03fce Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:32:27 +0900 Subject: [PATCH 18/28] =?UTF-8?q?feat:=20ImageSize=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/ImageSize.java | 24 +++++++++++++++++++ .../sessions/domain/ImageSizeTest.java | 16 +++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/ImageSize.java create mode 100644 src/test/java/nextstep/sessions/domain/ImageSizeTest.java diff --git a/src/main/java/nextstep/sessions/domain/ImageSize.java b/src/main/java/nextstep/sessions/domain/ImageSize.java new file mode 100644 index 000000000..f6380b617 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/ImageSize.java @@ -0,0 +1,24 @@ +package nextstep.sessions.domain; + +public class ImageSize { + + static final String ERROR_IMAGE_SIZE = "이미지 용량은 1MB 이하여야 합니다"; + private static final long MAX_SIZE = 1_000_000; + + private final long size; + + public ImageSize(long size) { + validate(size); + this.size = size; + } + + private static void validate(long size) { + if (size <= 0 || size > MAX_SIZE) { + throw new IllegalArgumentException(ERROR_IMAGE_SIZE); + } + } + + public long value() { + return size; + } +} diff --git a/src/test/java/nextstep/sessions/domain/ImageSizeTest.java b/src/test/java/nextstep/sessions/domain/ImageSizeTest.java new file mode 100644 index 000000000..a13e6948c --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/ImageSizeTest.java @@ -0,0 +1,16 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +class ImageSizeTest { + + @Test + void whenImageSizeExceeds1MB_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new ImageSize(1_048_577) + ).withMessageContaining("1MB"); + } + +} \ No newline at end of file From 23c29f029c3e6c74bd0409afc0f064dc5943a765 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:36:32 +0900 Subject: [PATCH 19/28] =?UTF-8?q?feat:=20ImageSize,=20ImageDimension=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sessions/domain/SessionImage.java | 46 ++++--------------- .../sessions/domain/SessionImageTest.java | 28 ----------- 2 files changed, 10 insertions(+), 64 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/SessionImage.java b/src/main/java/nextstep/sessions/domain/SessionImage.java index f88d6a92c..cb4becabe 100644 --- a/src/main/java/nextstep/sessions/domain/SessionImage.java +++ b/src/main/java/nextstep/sessions/domain/SessionImage.java @@ -2,27 +2,20 @@ public class SessionImage { - private static final long MAX_SIZE = 1_000_000; // 1MB - private static final int MIN_WIDTH = 300; - private static final int MIN_HEIGHT = 200; - private static final double RATIO = 3.0 / 2.0; - private final String fileName; - private final long size; - private final int width; - private final int height; + private final ImageSize imageSize; + private final ImageDimension imageDimension; private final ImageType type; - public SessionImage(String fileName, long size, int width, int height) { - validateFileName(fileName); - validateSize(size); - validateDimension(width, height); - validateRatio(width, height); + SessionImage(String fileName, long size, int width, int height) { + this(fileName, new ImageSize(size), new ImageDimension(width, height)); + } + public SessionImage(String fileName, ImageSize imageSize, ImageDimension imageDimension) { + validateFileName(fileName); this.fileName = fileName; - this.size = size; - this.width = width; - this.height = height; + this.imageSize = imageSize; + this.imageDimension = imageDimension; this.type = extractType(fileName); } @@ -31,7 +24,7 @@ public String fileName() { } public long size() { - return size; + return imageSize.value(); } private void validateFileName(String fileName) { @@ -40,25 +33,6 @@ private void validateFileName(String fileName) { } } - private void validateSize(long size) { - if (size <= 0 || size > MAX_SIZE) { - throw new IllegalArgumentException("이미지 용량은 1MB 이하여야 합니다"); - } - } - - private void validateDimension(int width, int height) { - if (width < MIN_WIDTH || height < MIN_HEIGHT) { - throw new IllegalArgumentException("이미지 크기가 최소 조건을 만족하지 않습니다"); - } - } - - private void validateRatio(int width, int height) { - double ratio = (double) width / height; - if (Math.abs(ratio - RATIO) > 0.0001) { - throw new IllegalArgumentException("이미지 비율은 3:2여야 합니다"); - } - } - private ImageType extractType(String fileName) { String ext = fileName.substring(fileName.lastIndexOf('.') + 1); return ImageType.from(ext); diff --git a/src/test/java/nextstep/sessions/domain/SessionImageTest.java b/src/test/java/nextstep/sessions/domain/SessionImageTest.java index 8a9752711..c48811f41 100644 --- a/src/test/java/nextstep/sessions/domain/SessionImageTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionImageTest.java @@ -25,34 +25,6 @@ void createImage_success() { assertThat(image.fileName()).isEqualTo("cover.png"); } - @Test - void whenImageSizeExceeds1MB_thenThrow() { - assertThatIllegalArgumentException().isThrownBy(() -> - new SessionImage("cover.png", 1_048_577, 300, 200) - ).withMessageContaining("1MB"); - } - - @Test - void whenImageWidthIsTooSmall_thenThrow() { - assertThatIllegalArgumentException().isThrownBy(() -> - new SessionImage("cover.png", 100_000, 299, 200) - ).withMessageContaining("이미지 크기"); - } - - @Test - void whenImageHeightIsTooSmall_thenThrow() { - assertThatIllegalArgumentException().isThrownBy(() -> - new SessionImage("cover.png", 100_000, 300, 199) - ).withMessageContaining("이미지 크기"); - } - - @Test - void whenImageRatioIsNot3To2_thenThrow() { - assertThatIllegalArgumentException().isThrownBy(() -> - new SessionImage("cover.png", 100_000, 310, 200) - ).withMessageContaining("3:2"); - } - @Test void whenFileNameIsEmpty_thenThrow() { assertThatIllegalArgumentException().isThrownBy(() -> From 61fd7b1ef85dc833b6e2a829d95cb9c80b621e07 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:47:25 +0900 Subject: [PATCH 20/28] =?UTF-8?q?feat:=20FileName=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/FileName.java | 23 +++++++++++++++++++ .../sessions/domain/FileNameTest.java | 21 +++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/FileName.java create mode 100644 src/test/java/nextstep/sessions/domain/FileNameTest.java diff --git a/src/main/java/nextstep/sessions/domain/FileName.java b/src/main/java/nextstep/sessions/domain/FileName.java new file mode 100644 index 000000000..c518a295b --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/FileName.java @@ -0,0 +1,23 @@ +package nextstep.sessions.domain; + +public class FileName { + + static final String ERROR_FILENAME_NOT_EMPTY = "파일명은 빈 값일 수 없습니다"; + private final String value; + + public FileName(String value) { + validate(value); + this.value = value; + } + + public String extension() { + return value.substring(value.lastIndexOf('.') + 1); + } + + private void validate(String fileName) { + if (fileName == null || fileName.trim().isEmpty()) { + throw new IllegalArgumentException(ERROR_FILENAME_NOT_EMPTY); + } + } + +} diff --git a/src/test/java/nextstep/sessions/domain/FileNameTest.java b/src/test/java/nextstep/sessions/domain/FileNameTest.java new file mode 100644 index 000000000..9b2777d7b --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/FileNameTest.java @@ -0,0 +1,21 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +class FileNameTest { + + @Test + void whenFileNameIsEmpty_thenThrow() { + assertThatIllegalArgumentException().isThrownBy(() -> + new FileName("") + ).withMessageContaining("파일명"); + } + + @Test + void extension() { + assertThat(new FileName("testImage.jpg").extension()).isEqualTo("jpg"); + } +} \ No newline at end of file From cc4028c55fac66f13ad315cc349644e102a89bcf Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:49:44 +0900 Subject: [PATCH 21/28] =?UTF-8?q?feat:=20FileName=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/FileName.java | 4 ++++ .../sessions/domain/SessionImage.java | 22 +++++-------------- .../sessions/domain/SessionImageTest.java | 8 ------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/FileName.java b/src/main/java/nextstep/sessions/domain/FileName.java index c518a295b..b7098bd63 100644 --- a/src/main/java/nextstep/sessions/domain/FileName.java +++ b/src/main/java/nextstep/sessions/domain/FileName.java @@ -10,6 +10,10 @@ public FileName(String value) { this.value = value; } + public String value() { + return value; + } + public String extension() { return value.substring(value.lastIndexOf('.') + 1); } diff --git a/src/main/java/nextstep/sessions/domain/SessionImage.java b/src/main/java/nextstep/sessions/domain/SessionImage.java index cb4becabe..0523fa56f 100644 --- a/src/main/java/nextstep/sessions/domain/SessionImage.java +++ b/src/main/java/nextstep/sessions/domain/SessionImage.java @@ -2,40 +2,28 @@ public class SessionImage { - private final String fileName; + private final FileName fileName; private final ImageSize imageSize; private final ImageDimension imageDimension; private final ImageType type; SessionImage(String fileName, long size, int width, int height) { - this(fileName, new ImageSize(size), new ImageDimension(width, height)); + this(new FileName(fileName), new ImageSize(size), new ImageDimension(width, height)); } - public SessionImage(String fileName, ImageSize imageSize, ImageDimension imageDimension) { - validateFileName(fileName); + public SessionImage(FileName fileName, ImageSize imageSize, ImageDimension imageDimension) { this.fileName = fileName; this.imageSize = imageSize; this.imageDimension = imageDimension; - this.type = extractType(fileName); + this.type = ImageType.from(fileName.extension()); } public String fileName() { - return fileName; + return fileName.value(); } public long size() { return imageSize.value(); } - private void validateFileName(String fileName) { - if (fileName == null || fileName.trim().isEmpty()) { - throw new IllegalArgumentException("파일명은 빈 값일 수 없습니다"); - } - } - - private ImageType extractType(String fileName) { - String ext = fileName.substring(fileName.lastIndexOf('.') + 1); - return ImageType.from(ext); - } - } diff --git a/src/test/java/nextstep/sessions/domain/SessionImageTest.java b/src/test/java/nextstep/sessions/domain/SessionImageTest.java index c48811f41..8e4e39bcb 100644 --- a/src/test/java/nextstep/sessions/domain/SessionImageTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionImageTest.java @@ -1,7 +1,6 @@ package nextstep.sessions.domain; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import org.junit.jupiter.api.Test; @@ -24,11 +23,4 @@ void createImage_success() { ); assertThat(image.fileName()).isEqualTo("cover.png"); } - - @Test - void whenFileNameIsEmpty_thenThrow() { - assertThatIllegalArgumentException().isThrownBy(() -> - new SessionImage("", 100_000, 300, 200) - ).withMessageContaining("파일명"); - } } From 3d841cf1558230d1a70ae6ce10d2abca69afb80b Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 05:32:05 +0900 Subject: [PATCH 22/28] =?UTF-8?q?feat:=20Enrollment=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/payments/domain/Payment.java | 9 ++++++ .../nextstep/sessions/domain/Enrollment.java | 29 +++++++++++++++++++ .../nextstep/payments/domain/PaymentTest.java | 11 +++++++ .../sessions/domain/EnrollmentTest.java | 18 ++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/Enrollment.java create mode 100644 src/test/java/nextstep/sessions/domain/EnrollmentTest.java diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index bf7f7aad1..61d504037 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -1,6 +1,7 @@ package nextstep.payments.domain; import java.time.LocalDateTime; +import nextstep.users.domain.NsUser; public class Payment { private String id; @@ -27,6 +28,14 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.createdAt = LocalDateTime.now(); } + public Long nsUserId() { + return nsUserId; + } + + public boolean isPaidBy(NsUser user) { + return user.getId().equals(this.nsUserId); + } + public boolean isPaidFor(int fee) { return this.amount == fee; } diff --git a/src/main/java/nextstep/sessions/domain/Enrollment.java b/src/main/java/nextstep/sessions/domain/Enrollment.java new file mode 100644 index 000000000..49551d104 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Enrollment.java @@ -0,0 +1,29 @@ +package nextstep.sessions.domain; + +import java.time.LocalDateTime; +import nextstep.payments.domain.Payment; +import nextstep.users.domain.NsUser; + +public class Enrollment { + + static final String ERROR_USER_PAYMENT_MISMATCH = "결제한 사용자와 신청자가 일치하지 않습니다"; + + private final NsUser user; + private final Payment payment; + private final LocalDateTime enrolledAt; + + public Enrollment(NsUser user, Payment payment) { + validateUserPayMatch(user, payment); + this.user = user; + this.payment = payment; + this.enrolledAt = LocalDateTime.now(); + } + + private static void validateUserPayMatch(NsUser user, Payment payment) { + if (payment.isPaidBy(user)) { + return; + } + throw new IllegalArgumentException(ERROR_USER_PAYMENT_MISMATCH); + } + +} diff --git a/src/test/java/nextstep/payments/domain/PaymentTest.java b/src/test/java/nextstep/payments/domain/PaymentTest.java index 107073b19..e5029cce2 100644 --- a/src/test/java/nextstep/payments/domain/PaymentTest.java +++ b/src/test/java/nextstep/payments/domain/PaymentTest.java @@ -2,11 +2,22 @@ import static org.assertj.core.api.Assertions.assertThat; +import nextstep.users.domain.NsUserTest; import org.junit.jupiter.api.Test; public class PaymentTest { public static final Payment PAYMENT_1000 = new Payment("p1", 1L, 1L, 1000L); + @Test + void isPaidBy_returnsTrue_whenMatch() { + assertThat(PAYMENT_1000.isPaidBy(NsUserTest.JAVAJIGI)).isTrue(); + } + + @Test + void isPaidBy_returnsFalse_whenMisMatch() { + assertThat(PAYMENT_1000.isPaidBy(NsUserTest.SANJIGI)).isFalse(); + } + @Test void isPaidFor_returnsTrue_whenAmountMatchesFee() { assertThat(PAYMENT_1000.isPaidFor(1000)).isTrue(); diff --git a/src/test/java/nextstep/sessions/domain/EnrollmentTest.java b/src/test/java/nextstep/sessions/domain/EnrollmentTest.java new file mode 100644 index 000000000..8e5f89696 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/EnrollmentTest.java @@ -0,0 +1,18 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.payments.domain.PaymentTest; +import nextstep.users.domain.NsUserTest; +import org.junit.jupiter.api.Test; + +class EnrollmentTest { + + @Test + void validateUserAndPayment() { + assertThatThrownBy(() -> new Enrollment(NsUserTest.SANJIGI, PaymentTest.PAYMENT_1000)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining(Enrollment.ERROR_USER_PAYMENT_MISMATCH); + } + +} \ No newline at end of file From 9b6e44029b1d2a8397c7ab56650e8439340fa33b Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 07:02:38 +0900 Subject: [PATCH 23/28] =?UTF-8?q?feat:=20Enrollment,=20Enrollments=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/payments/domain/Payment.java | 20 ++++++++++++ .../nextstep/sessions/domain/Enrollment.java | 23 ++++++++++++++ .../nextstep/sessions/domain/Enrollments.java | 23 ++++++++++++++ .../java/nextstep/users/domain/NsUser.java | 26 ++++++++++++++-- .../sessions/domain/EnrollmentTest.java | 2 ++ .../sessions/domain/EnrollmentsTest.java | 31 +++++++++++++++++++ 6 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 src/main/java/nextstep/sessions/domain/Enrollments.java create mode 100644 src/test/java/nextstep/sessions/domain/EnrollmentsTest.java diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 61d504037..552d28ac5 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -1,6 +1,7 @@ package nextstep.payments.domain; import java.time.LocalDateTime; +import java.util.Objects; import nextstep.users.domain.NsUser; public class Payment { @@ -39,4 +40,23 @@ public boolean isPaidBy(NsUser user) { public boolean isPaidFor(int fee) { return this.amount == fee; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Payment payment = (Payment) o; + return Objects.equals(id, payment.id) && Objects.equals(sessionId, payment.sessionId) + && Objects.equals(nsUserId, payment.nsUserId) && Objects.equals(amount, payment.amount) + && Objects.equals(createdAt, payment.createdAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, sessionId, nsUserId, amount, createdAt); + } } diff --git a/src/main/java/nextstep/sessions/domain/Enrollment.java b/src/main/java/nextstep/sessions/domain/Enrollment.java index 49551d104..2ed4575e9 100644 --- a/src/main/java/nextstep/sessions/domain/Enrollment.java +++ b/src/main/java/nextstep/sessions/domain/Enrollment.java @@ -1,6 +1,7 @@ package nextstep.sessions.domain; import java.time.LocalDateTime; +import java.util.Objects; import nextstep.payments.domain.Payment; import nextstep.users.domain.NsUser; @@ -19,6 +20,28 @@ public Enrollment(NsUser user, Payment payment) { this.enrolledAt = LocalDateTime.now(); } + public Payment payment() { + return payment; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Enrollment that = (Enrollment) o; + return Objects.equals(user, that.user) && Objects.equals(payment, that.payment) + && Objects.equals(enrolledAt, that.enrolledAt); + } + + @Override + public int hashCode() { + return Objects.hash(user, payment, enrolledAt); + } + private static void validateUserPayMatch(NsUser user, Payment payment) { if (payment.isPaidBy(user)) { return; diff --git a/src/main/java/nextstep/sessions/domain/Enrollments.java b/src/main/java/nextstep/sessions/domain/Enrollments.java new file mode 100644 index 000000000..2bd0760df --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Enrollments.java @@ -0,0 +1,23 @@ +package nextstep.sessions.domain; + +import java.util.HashSet; +import java.util.Set; + +public class Enrollments { + + static final String ERROR_ALREADY_ENROLLED = "이미 등록된 사용자입니다."; + + private final Set enrollments = new HashSet<>(); + + public void add(Enrollment enrollment) { + if (enrollments.contains(enrollment)) { + throw new IllegalArgumentException(ERROR_ALREADY_ENROLLED); + } + enrollments.add(enrollment); + } + + public int size() { + return enrollments.size(); + } +} + diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java index 62ec5138c..b83d4bae6 100755 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -1,9 +1,8 @@ package nextstep.users.domain; -import nextstep.qna.UnAuthorizedException; - import java.time.LocalDateTime; import java.util.Objects; +import nextstep.qna.UnAuthorizedException; public class NsUser { public static final GuestNsUser GUEST_USER = new GuestNsUser(); @@ -29,7 +28,8 @@ public NsUser(Long id, String userId, String password, String name, String email this(id, userId, password, name, email, LocalDateTime.now(), null); } - public NsUser(Long id, String userId, String password, String name, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { + public NsUser(Long id, String userId, String password, String name, String email, LocalDateTime createdAt, + LocalDateTime updatedAt) { this.id = id; this.userId = userId; this.password = password; @@ -124,6 +124,26 @@ public boolean isGuestUser() { } } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NsUser nsUser = (NsUser) o; + return Objects.equals(id, nsUser.id) && Objects.equals(userId, nsUser.userId) + && Objects.equals(password, nsUser.password) && Objects.equals(name, nsUser.name) + && Objects.equals(email, nsUser.email) && Objects.equals(createdAt, nsUser.createdAt) + && Objects.equals(updatedAt, nsUser.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, userId, password, name, email, createdAt, updatedAt); + } + @Override public String toString() { return "NsUser{" + diff --git a/src/test/java/nextstep/sessions/domain/EnrollmentTest.java b/src/test/java/nextstep/sessions/domain/EnrollmentTest.java index 8e5f89696..4d19cbd0d 100644 --- a/src/test/java/nextstep/sessions/domain/EnrollmentTest.java +++ b/src/test/java/nextstep/sessions/domain/EnrollmentTest.java @@ -8,6 +8,8 @@ class EnrollmentTest { + public static Enrollment E1 = new Enrollment(NsUserTest.JAVAJIGI, PaymentTest.PAYMENT_1000); + @Test void validateUserAndPayment() { assertThatThrownBy(() -> new Enrollment(NsUserTest.SANJIGI, PaymentTest.PAYMENT_1000)) diff --git a/src/test/java/nextstep/sessions/domain/EnrollmentsTest.java b/src/test/java/nextstep/sessions/domain/EnrollmentsTest.java new file mode 100644 index 000000000..2679e455c --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/EnrollmentsTest.java @@ -0,0 +1,31 @@ +package nextstep.sessions.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class EnrollmentsTest { + + private Enrollments enrollments; + + @BeforeEach + void setUp() { + enrollments = new Enrollments(); + } + + @Test + void addEnrollment() { + enrollments.add(EnrollmentTest.E1); + assertThat(enrollments.size()).isEqualTo(1); + } + + @Test + void whenDuplicateEnrollment_thenThrows() { + enrollments.add(EnrollmentTest.E1); + assertThatThrownBy(() -> enrollments.add(EnrollmentTest.E1)) + .hasMessageContaining(Enrollments.ERROR_ALREADY_ENROLLED); + } + +} \ No newline at end of file From 7766878d4099c90d03cb40f6b572d8dbf085ab64 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:46:28 +0900 Subject: [PATCH 24/28] =?UTF-8?q?feat:=20Enrollment,=20Enrollments=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9,=20Capacity=20unlimited=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Capacity.java | 19 +++--- .../nextstep/sessions/domain/Session.java | 64 +++++++++++-------- .../sessions/domain/CapacityTest.java | 28 ++++---- .../domain/FreeEnrollmentPolicyTest.java | 4 +- .../domain/PaidEnrollmentPolicyTest.java | 4 +- .../nextstep/sessions/domain/SessionTest.java | 34 ++++++---- .../sessions/domain/SessionTestBuilder.java | 18 ++++-- 7 files changed, 101 insertions(+), 70 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Capacity.java b/src/main/java/nextstep/sessions/domain/Capacity.java index d2a13be39..ebeebc862 100644 --- a/src/main/java/nextstep/sessions/domain/Capacity.java +++ b/src/main/java/nextstep/sessions/domain/Capacity.java @@ -10,18 +10,21 @@ public class Capacity { private static final int DEFAULT_ENROLL_COUNT = 0; private final Integer maxCapacity; + private final boolean unlimited; private final int enrollCount; - public Capacity(Integer maxCapacity) { - this(maxCapacity, DEFAULT_ENROLL_COUNT); + + public Capacity(Integer maxCapacity, boolean unlimited) { + this(maxCapacity, unlimited, DEFAULT_ENROLL_COUNT); } - Capacity(Integer maxCapacity, int enrollCount) { + public Capacity(Integer maxCapacity, boolean unlimited, int enrollCount) { validateMaxCapacity(maxCapacity); validateEnrollCount(enrollCount); validateEnrollCountWithinCapacity(maxCapacity, enrollCount); this.maxCapacity = maxCapacity; + this.unlimited = unlimited; this.enrollCount = enrollCount; } @@ -34,11 +37,11 @@ public int enrollCount() { } public boolean canEnroll() { - return maxCapacity == null || enrollCount < maxCapacity; + return unlimited || enrollCount < maxCapacity; } public boolean isUnlimited() { - return maxCapacity == null; + return unlimited; } public boolean isFull() { @@ -53,11 +56,11 @@ public Capacity increaseEnrollCount() { if (!canEnroll()) { throw new IllegalArgumentException(ERROR_CANNOT_ENROLL); } - return new Capacity(maxCapacity, enrollCount + 1); + return new Capacity(maxCapacity, unlimited, enrollCount + 1); } - private static void validateMaxCapacity(Integer maxCapacity) { - if (maxCapacity != null && maxCapacity <= 0) { + private void validateMaxCapacity(Integer maxCapacity) { + if (!unlimited && (maxCapacity == null || maxCapacity <= 0)) { throw new IllegalArgumentException(ERROR_MAX_CAPACITY_REQUIRED); } } diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 029745abb..975459a63 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -1,10 +1,15 @@ package nextstep.sessions.domain; import java.time.LocalDate; -import nextstep.payments.domain.Payment; public class Session { + public static final String ERROR_SESSION_NOT_OPEN = "모집중인 강의만 수강 신청 가능합니다."; + public static final String ERROR_CAPACITY_EXCEEDED = "강의 정원이 초과되어 수강 신청할 수 없습니다."; + public static final String ERROR_PAYMENT_AMOUNT_MISMATCH = "결제 금액이 강의 수강료와 일치하지 않습니다."; + + private Long id; + private SessionInfo sessionInfo; private SessionStatus status; @@ -13,64 +18,67 @@ public class Session { private Capacity capacity; - private EnrollmentPolicy enrollmentPolicy; + private final Enrollments enrollments = new Enrollments(); - Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, int enrollCount, - SessionImage image) { - this(startDate, endDate, isPaid, maxCapacity, fee, image); - } - - public Session(LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, int fee, + public Session(Long id, LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, + boolean unlimited, int fee, int enrollCount, SessionImage image) { - this(new SessionInfo(new Period(startDate, endDate), image), - new SessionPricing(isPaid, fee), new Capacity(maxCapacity)); + this(id, new SessionInfo(new Period(startDate, endDate), image), + new SessionPricing(isPaid, fee), new Capacity(maxCapacity, unlimited, enrollCount)); } - public Session(SessionInfo sessionInfo, SessionPricing pricing, Capacity capacity) { + public Session(Long id, SessionInfo sessionInfo, SessionPricing pricing, Capacity capacity) { validatePricingAndCapacity(pricing, capacity); + this.id = id; this.sessionInfo = sessionInfo; this.status = SessionStatus.PREPARING; this.pricing = pricing; this.capacity = capacity; - this.enrollmentPolicy = createEnrollmentPolicy(pricing); } public SessionStatus status() { return status; } - public boolean canEnroll(Payment payment) { - return enrollmentPolicy.canEnroll(capacity, payment) && isOpen(); - } - public void startRecruiting() { this.status = SessionStatus.OPEN; } - public void enroll(Payment payment) { - if (!canEnroll(payment)) { - throw new IllegalArgumentException("수강 신청을 할 수 없습니다"); - } + public void enroll(Enrollment enrollment) { + validateOpen(); + validateCapacity(); + validatePaymentAmount(enrollment); + enrollments.add(enrollment); this.capacity = capacity.increaseEnrollCount(); } + private void validateOpen() { + if (!isOpen()) { + throw new IllegalStateException(ERROR_SESSION_NOT_OPEN); + } + } + + private void validateCapacity() { + if (capacity.isFull()) { + throw new IllegalStateException(Session.ERROR_CAPACITY_EXCEEDED); + } + } + + private void validatePaymentAmount(Enrollment enrollment) { + if (pricing.isPaid() && !enrollment.payment().isPaidFor(pricing.fee())) { + throw new IllegalArgumentException(ERROR_PAYMENT_AMOUNT_MISMATCH); + } + } + private void validatePricingAndCapacity(SessionPricing pricing, Capacity capacity) { if (pricing.isPaid() && capacity.isUnlimited()) { throw new IllegalArgumentException("유료 강의는 최대 수강인원이 있어야 합니다"); } - if (!pricing.isPaid() && !capacity.isUnlimited()) { throw new IllegalArgumentException("무료 강의는 최대 수강 인원이 없어야 합니다"); } } - private EnrollmentPolicy createEnrollmentPolicy(SessionPricing pricing) { - if (pricing.isPaid()) { - return new PaidEnrollmentPolicy(pricing.fee()); - } - return new FreeEnrollmentPolicy(); - } - private boolean isOpen() { return status == SessionStatus.OPEN; } diff --git a/src/test/java/nextstep/sessions/domain/CapacityTest.java b/src/test/java/nextstep/sessions/domain/CapacityTest.java index 567044af5..a3c17fcdf 100644 --- a/src/test/java/nextstep/sessions/domain/CapacityTest.java +++ b/src/test/java/nextstep/sessions/domain/CapacityTest.java @@ -7,51 +7,51 @@ class CapacityTest { + public static final Capacity FREE_CAPACITY = new Capacity(Integer.MAX_VALUE, true); + public static final Capacity PAID_CAPACITY = new Capacity(10, true); + @Test - void freeCapacity_maxCapacityIsNull() { - Capacity freeCapacity = new Capacity(null); - assertThat(freeCapacity.maxCapacity()).isNull(); - assertThat(freeCapacity.canEnroll()).isTrue(); + void freeCapacity_isUnlimitedIsTrue() { + assertThat(FREE_CAPACITY.isUnlimited()).isTrue(); + assertThat(FREE_CAPACITY.canEnroll()).isTrue(); } @Test void paidCapacity_maxCapacityMustBePositive() { - Capacity paidCapacity = new Capacity(5, 0); - assertThat(paidCapacity.maxCapacity()).isEqualTo(5); - assertThat(paidCapacity.canEnroll()).isTrue(); + assertThat(PAID_CAPACITY.maxCapacity()).isEqualTo(10); + assertThat(PAID_CAPACITY.canEnroll()).isTrue(); } @Test void paidCapacity_invalidMaxCapacity_throwsException() { - assertThatThrownBy(() -> new Capacity(0, 0)) + assertThatThrownBy(() -> new Capacity(0, false, 0)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); } @Test void enrollCountExceedsMax_throwsException() { - assertThatThrownBy(() -> new Capacity(1, 2)) + assertThatThrownBy(() -> new Capacity(1, false, 2)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("수강 인원은 수강 정원을 초과할 수 없습니다"); } @Test void enrollEqualsMax_isFullReturnsTrue() { - Capacity capacity = new Capacity(3, 3); + Capacity capacity = new Capacity(3, false, 3); assertThat(capacity.isFull()).isTrue(); } @Test void enrollLessThanMax_isFullReturnsFalse() { - Capacity capacity = new Capacity(3, 2); + Capacity capacity = new Capacity(3, false, 2); assertThat(capacity.isFull()).isFalse(); } @Test void increaseEnrollCount_incrementsEnrollCountByOne() { - Capacity capacity = new Capacity(null); - Capacity afterIncrease = capacity.increaseEnrollCount(); - assertThat(afterIncrease.enrollCount() - capacity.enrollCount()).isEqualTo(1); + Capacity afterIncrease = FREE_CAPACITY.increaseEnrollCount(); + assertThat(afterIncrease.enrollCount() - FREE_CAPACITY.enrollCount()).isEqualTo(1); } } diff --git a/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java b/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java index 8da4b3d7c..2343d9421 100644 --- a/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java +++ b/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java @@ -9,7 +9,7 @@ class FreeEnrollmentPolicyTest { @Test void validate_throwsException_whenCapacityIsLimited() { - Capacity limitedCapacity = new Capacity(3); + Capacity limitedCapacity = new Capacity(3, false); FreeEnrollmentPolicy policy = new FreeEnrollmentPolicy(); assertThatThrownBy(() -> policy.validate(limitedCapacity)) .isInstanceOf(IllegalArgumentException.class) @@ -18,7 +18,7 @@ void validate_throwsException_whenCapacityIsLimited() { @Test void validate_passes_whenCapacityIsUnlimited() { - Capacity unlimitedCapacity = new Capacity(null); + Capacity unlimitedCapacity = CapacityTest.FREE_CAPACITY; FreeEnrollmentPolicy policy = new FreeEnrollmentPolicy(); assertThatCode(() -> policy.validate(unlimitedCapacity)) .doesNotThrowAnyException(); diff --git a/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java b/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java index c140b4cae..7a545f8da 100644 --- a/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java +++ b/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java @@ -9,7 +9,7 @@ class PaidEnrollmentPolicyTest { @Test void validate_throwsException_whenCapacityIsUnlimited() { - Capacity unlimitedCapacity = new Capacity(null); + Capacity unlimitedCapacity = CapacityTest.FREE_CAPACITY; PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(1000); assertThatThrownBy(() -> policy.validate(unlimitedCapacity)) .isInstanceOf(IllegalArgumentException.class) @@ -18,7 +18,7 @@ void validate_throwsException_whenCapacityIsUnlimited() { @Test void validate_passes_whenCapacityIsLimited() { - Capacity limitedCapacity = new Capacity(3); + Capacity limitedCapacity = new Capacity(3, false); PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(1000); assertThatCode(() -> policy.validate(limitedCapacity)) .doesNotThrowAnyException(); diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index 8a654ffb5..6ccbb8c03 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import nextstep.payments.domain.PaymentTest; import org.junit.jupiter.api.Test; class SessionTest { @@ -14,6 +13,7 @@ void sessionStatusIsPreparingOnCreation() { assertThat(session.status()).isEqualTo(SessionStatus.PREPARING); } + @Test void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { assertThatThrownBy(() -> new SessionTestBuilder().paid(null, 100_000).build()) @@ -26,26 +26,36 @@ void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { } @Test - void whenSessionStatusIsOpen_thenCanEnrollIsTrue() { + void whenStartRecruiting_StatusIsOpen() { Session session = new SessionTestBuilder().free().build(); session.startRecruiting(); - assertThat(session.canEnroll(PaymentTest.PAYMENT_1000)).isTrue(); + assertThat(session.status()).isEqualTo(SessionStatus.OPEN); } @Test - void whenSessionStatusIsNotOpen_thenCanEnrollIsFalse() { + void whenSessionStatusIsNotOpen_thenThrows() { Session session = new SessionTestBuilder().free().build(); - assertThat(session.canEnroll(PaymentTest.PAYMENT_1000)).isFalse(); + assertThatThrownBy(() -> session.enroll(EnrollmentTest.E1)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(Session.ERROR_SESSION_NOT_OPEN); } @Test - void whenEnrollImpossible_thenThrow() { - Session session = new SessionTestBuilder() - .paid(1, 100_000) - .enrollCount(1) - .build(); - assertThatThrownBy(() -> session.enroll(PaymentTest.PAYMENT_1000)) + void whenCapacityFull_thenThrows() { + Session session = new SessionTestBuilder().paid(5, 1000).id(1L).enrollCount(5).build(); + session.startRecruiting(); + assertThatThrownBy(() -> session.enroll(EnrollmentTest.E1)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining(Session.ERROR_CAPACITY_EXCEEDED); + } + + @Test + void whenPaymentDifferent_thenThrows() { + Session session = new SessionTestBuilder().paid(5, 100_000).id(1L).build(); + session.startRecruiting(); + assertThatThrownBy(() -> session.enroll(EnrollmentTest.E1)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("수강 신청을 할 수 없습니다"); + .hasMessageContaining(Session.ERROR_PAYMENT_AMOUNT_MISMATCH); } + } diff --git a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java index 00bd426f3..b2056a0b4 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java +++ b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java @@ -4,10 +4,13 @@ class SessionTestBuilder { + private Long id = 1L; private LocalDate startDate = PeriodTest.START_DATE; private LocalDate endDate = PeriodTest.END_DATE; + private SessionStatus status = SessionStatus.PREPARING; private boolean paid = false; - private Integer maxCapacity = null; + private Integer maxCapacity = Integer.MAX_VALUE; + private boolean unlimited = true; private int fee = 0; private int enrollCount = 0; private SessionImage image = SessionImageTest.IMAGE; @@ -15,17 +18,23 @@ class SessionTestBuilder { public SessionTestBuilder paid(Integer maxCapacity, int fee) { this.paid = true; this.maxCapacity = maxCapacity; + this.unlimited = false; this.fee = fee; return this; } public SessionTestBuilder free() { this.paid = false; - this.maxCapacity = null; + this.maxCapacity = Integer.MAX_VALUE; this.fee = 0; return this; } + public SessionTestBuilder id(Long id) { + this.id = id; + return this; + } + public SessionTestBuilder maxCapacity(Integer maxCapacity) { this.maxCapacity = maxCapacity; return this; @@ -43,8 +52,9 @@ public SessionTestBuilder enrollCount(int count) { public Session build() { if (paid) { - return new Session(startDate, endDate, true, maxCapacity, fee, enrollCount, image); + return new Session(id, startDate, endDate, true, maxCapacity, false, fee, enrollCount, image); } - return new Session(startDate, endDate, false, maxCapacity, fee, image); + return new Session(id, startDate, endDate, false, maxCapacity, true, fee, enrollCount, image); } + } From 6e6c5fd3bcc1ff0dabfaf415dc25198f740e47f6 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:54:11 +0900 Subject: [PATCH 25/28] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20EnrollmentPolicy=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sessions/domain/EnrollmentPolicy.java | 10 ------- .../sessions/domain/FreeEnrollmentPolicy.java | 20 -------------- .../sessions/domain/PaidEnrollmentPolicy.java | 25 ------------------ .../domain/FreeEnrollmentPolicyTest.java | 26 ------------------- .../domain/PaidEnrollmentPolicyTest.java | 26 ------------------- 5 files changed, 107 deletions(-) delete mode 100644 src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java delete mode 100644 src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java delete mode 100644 src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java delete mode 100644 src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java delete mode 100644 src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java diff --git a/src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java b/src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java deleted file mode 100644 index 6b9e0629c..000000000 --- a/src/main/java/nextstep/sessions/domain/EnrollmentPolicy.java +++ /dev/null @@ -1,10 +0,0 @@ -package nextstep.sessions.domain; - -import nextstep.payments.domain.Payment; - -public interface EnrollmentPolicy { - - boolean canEnroll(Capacity capacity, Payment payment); - - void validate(Capacity capacity); -} diff --git a/src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java b/src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java deleted file mode 100644 index b23cc87b5..000000000 --- a/src/main/java/nextstep/sessions/domain/FreeEnrollmentPolicy.java +++ /dev/null @@ -1,20 +0,0 @@ -package nextstep.sessions.domain; - -import nextstep.payments.domain.Payment; - -public class FreeEnrollmentPolicy implements EnrollmentPolicy { - - static final String ERROR_FREE_COURSE_CAPACITY_MUST_BE_UNLIMITED = "무료 강의는 최대 수강 인원이 없어야 합니다"; - - @Override - public boolean canEnroll(Capacity capacity, Payment payment) { - return true; - } - - @Override - public void validate(Capacity capacity) { - if (!capacity.isUnlimited()) { - throw new IllegalArgumentException(ERROR_FREE_COURSE_CAPACITY_MUST_BE_UNLIMITED); - } - } -} diff --git a/src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java b/src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java deleted file mode 100644 index 65599897e..000000000 --- a/src/main/java/nextstep/sessions/domain/PaidEnrollmentPolicy.java +++ /dev/null @@ -1,25 +0,0 @@ -package nextstep.sessions.domain; - -import nextstep.payments.domain.Payment; - -public class PaidEnrollmentPolicy implements EnrollmentPolicy { - - static final String ERROR_PAID_SESSION_CAPACITY_REQUIRED = "유료 강의는 최대 수강 인원이 있어야 합니다"; - private final int fee; - - public PaidEnrollmentPolicy(int fee) { - this.fee = fee; - } - - @Override - public boolean canEnroll(Capacity capacity, Payment payment) { - return capacity.hasAvailableSeat() && payment != null && payment.isPaidFor(fee); - } - - @Override - public void validate(Capacity capacity) { - if (capacity.isUnlimited()) { - throw new IllegalArgumentException(ERROR_PAID_SESSION_CAPACITY_REQUIRED); - } - } -} diff --git a/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java b/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java deleted file mode 100644 index 2343d9421..000000000 --- a/src/test/java/nextstep/sessions/domain/FreeEnrollmentPolicyTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package nextstep.sessions.domain; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.Test; - -class FreeEnrollmentPolicyTest { - - @Test - void validate_throwsException_whenCapacityIsLimited() { - Capacity limitedCapacity = new Capacity(3, false); - FreeEnrollmentPolicy policy = new FreeEnrollmentPolicy(); - assertThatThrownBy(() -> policy.validate(limitedCapacity)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(FreeEnrollmentPolicy.ERROR_FREE_COURSE_CAPACITY_MUST_BE_UNLIMITED); - } - - @Test - void validate_passes_whenCapacityIsUnlimited() { - Capacity unlimitedCapacity = CapacityTest.FREE_CAPACITY; - FreeEnrollmentPolicy policy = new FreeEnrollmentPolicy(); - assertThatCode(() -> policy.validate(unlimitedCapacity)) - .doesNotThrowAnyException(); - } -} diff --git a/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java b/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java deleted file mode 100644 index 7a545f8da..000000000 --- a/src/test/java/nextstep/sessions/domain/PaidEnrollmentPolicyTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package nextstep.sessions.domain; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import org.junit.jupiter.api.Test; - -class PaidEnrollmentPolicyTest { - - @Test - void validate_throwsException_whenCapacityIsUnlimited() { - Capacity unlimitedCapacity = CapacityTest.FREE_CAPACITY; - PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(1000); - assertThatThrownBy(() -> policy.validate(unlimitedCapacity)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage(PaidEnrollmentPolicy.ERROR_PAID_SESSION_CAPACITY_REQUIRED); - } - - @Test - void validate_passes_whenCapacityIsLimited() { - Capacity limitedCapacity = new Capacity(3, false); - PaidEnrollmentPolicy policy = new PaidEnrollmentPolicy(1000); - assertThatCode(() -> policy.validate(limitedCapacity)) - .doesNotThrowAnyException(); - } -} From ab7d29efb11f973362d894d0251b04bb0bea1744 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:18:14 +0900 Subject: [PATCH 26/28] =?UTF-8?q?feat:=20=EC=A0=95=EC=A0=81=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=8F=84=EC=9E=85=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=88=EA=B0=80=EB=8A=A5=ED=95=9C=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=81=ED=83=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/Capacity.java | 12 +++++-- .../nextstep/sessions/domain/Session.java | 35 ++++++++----------- .../sessions/domain/SessionPricing.java | 10 +++++- .../sessions/domain/CapacityTest.java | 1 + .../sessions/domain/SessionInfoTest.java | 2 ++ .../nextstep/sessions/domain/SessionTest.java | 15 ++------ .../sessions/domain/SessionTestBuilder.java | 4 +-- 7 files changed, 41 insertions(+), 38 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Capacity.java b/src/main/java/nextstep/sessions/domain/Capacity.java index ebeebc862..2df65bfb9 100644 --- a/src/main/java/nextstep/sessions/domain/Capacity.java +++ b/src/main/java/nextstep/sessions/domain/Capacity.java @@ -14,11 +14,11 @@ public class Capacity { private final int enrollCount; - public Capacity(Integer maxCapacity, boolean unlimited) { + Capacity(Integer maxCapacity, boolean unlimited) { this(maxCapacity, unlimited, DEFAULT_ENROLL_COUNT); } - public Capacity(Integer maxCapacity, boolean unlimited, int enrollCount) { + Capacity(Integer maxCapacity, boolean unlimited, int enrollCount) { validateMaxCapacity(maxCapacity); validateEnrollCount(enrollCount); validateEnrollCountWithinCapacity(maxCapacity, enrollCount); @@ -28,6 +28,14 @@ public Capacity(Integer maxCapacity, boolean unlimited, int enrollCount) { this.enrollCount = enrollCount; } + public static Capacity limited(int maxCapacity) { + return new Capacity(maxCapacity, false, DEFAULT_ENROLL_COUNT); + } + + public static Capacity unlimited() { + return new Capacity(Integer.MAX_VALUE, true, 0); + } + public Integer maxCapacity() { return maxCapacity; } diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 975459a63..5b078ddb2 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -20,15 +20,7 @@ public class Session { private final Enrollments enrollments = new Enrollments(); - public Session(Long id, LocalDate startDate, LocalDate endDate, boolean isPaid, Integer maxCapacity, - boolean unlimited, int fee, int enrollCount, - SessionImage image) { - this(id, new SessionInfo(new Period(startDate, endDate), image), - new SessionPricing(isPaid, fee), new Capacity(maxCapacity, unlimited, enrollCount)); - } - - public Session(Long id, SessionInfo sessionInfo, SessionPricing pricing, Capacity capacity) { - validatePricingAndCapacity(pricing, capacity); + Session(Long id, SessionInfo sessionInfo, SessionPricing pricing, Capacity capacity) { this.id = id; this.sessionInfo = sessionInfo; this.status = SessionStatus.PREPARING; @@ -36,6 +28,17 @@ public Session(Long id, SessionInfo sessionInfo, SessionPricing pricing, Capacit this.capacity = capacity; } + public static Session paidLimited(Long id, LocalDate startDate, LocalDate endDate, int fee, int maxCapacity, + SessionImage image) { + SessionInfo info = new SessionInfo(new Period(startDate, endDate), image); + return new Session(id, info, SessionPricing.paid(fee), Capacity.limited(maxCapacity)); + } + + public static Session freeUnlimited(Long id, LocalDate startDate, LocalDate endDate, SessionImage image) { + SessionInfo info = new SessionInfo(new Period(startDate, endDate), image); + return new Session(id, info, SessionPricing.free(), Capacity.unlimited()); + } + public SessionStatus status() { return status; } @@ -48,8 +51,9 @@ public void enroll(Enrollment enrollment) { validateOpen(); validateCapacity(); validatePaymentAmount(enrollment); + enrollments.add(enrollment); - this.capacity = capacity.increaseEnrollCount(); + capacity = capacity.increaseEnrollCount(); } private void validateOpen() { @@ -60,7 +64,7 @@ private void validateOpen() { private void validateCapacity() { if (capacity.isFull()) { - throw new IllegalStateException(Session.ERROR_CAPACITY_EXCEEDED); + throw new IllegalStateException(ERROR_CAPACITY_EXCEEDED); } } @@ -70,15 +74,6 @@ private void validatePaymentAmount(Enrollment enrollment) { } } - private void validatePricingAndCapacity(SessionPricing pricing, Capacity capacity) { - if (pricing.isPaid() && capacity.isUnlimited()) { - throw new IllegalArgumentException("유료 강의는 최대 수강인원이 있어야 합니다"); - } - if (!pricing.isPaid() && !capacity.isUnlimited()) { - throw new IllegalArgumentException("무료 강의는 최대 수강 인원이 없어야 합니다"); - } - } - private boolean isOpen() { return status == SessionStatus.OPEN; } diff --git a/src/main/java/nextstep/sessions/domain/SessionPricing.java b/src/main/java/nextstep/sessions/domain/SessionPricing.java index 78470970c..3c0c4e07f 100644 --- a/src/main/java/nextstep/sessions/domain/SessionPricing.java +++ b/src/main/java/nextstep/sessions/domain/SessionPricing.java @@ -8,12 +8,20 @@ public class SessionPricing { private final boolean isPaid; private final int fee; - public SessionPricing(boolean isPaid, int fee) { + SessionPricing(boolean isPaid, int fee) { validateFee(isPaid, fee); this.isPaid = isPaid; this.fee = fee; } + public static SessionPricing paid(int fee) { + return new SessionPricing(true, fee); + } + + public static SessionPricing free() { + return new SessionPricing(false, 0); + } + public boolean isPaid() { return isPaid; } diff --git a/src/test/java/nextstep/sessions/domain/CapacityTest.java b/src/test/java/nextstep/sessions/domain/CapacityTest.java index a3c17fcdf..6df007eee 100644 --- a/src/test/java/nextstep/sessions/domain/CapacityTest.java +++ b/src/test/java/nextstep/sessions/domain/CapacityTest.java @@ -9,6 +9,7 @@ class CapacityTest { public static final Capacity FREE_CAPACITY = new Capacity(Integer.MAX_VALUE, true); public static final Capacity PAID_CAPACITY = new Capacity(10, true); + public static final Capacity PAID_CAPACITy_FULL = new Capacity(10, true, 10); @Test void freeCapacity_isUnlimitedIsTrue() { diff --git a/src/test/java/nextstep/sessions/domain/SessionInfoTest.java b/src/test/java/nextstep/sessions/domain/SessionInfoTest.java index 6463ae57e..e34b5e148 100644 --- a/src/test/java/nextstep/sessions/domain/SessionInfoTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionInfoTest.java @@ -6,6 +6,8 @@ class SessionInfoTest { + public static final SessionInfo INFO = new SessionInfo(PeriodTest.P1, SessionImageTest.IMAGE); + @Test void imageNull_throwsExcepiton() { assertThatThrownBy(() -> new SessionInfo(PeriodTest.P1, null)) diff --git a/src/test/java/nextstep/sessions/domain/SessionTest.java b/src/test/java/nextstep/sessions/domain/SessionTest.java index 6ccbb8c03..9004612e3 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionTest.java @@ -13,18 +13,6 @@ void sessionStatusIsPreparingOnCreation() { assertThat(session.status()).isEqualTo(SessionStatus.PREPARING); } - - @Test - void whenCreatingPaidSessionWithInvalidCapacity_thenThrow() { - assertThatThrownBy(() -> new SessionTestBuilder().paid(null, 100_000).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); - - assertThatThrownBy(() -> new SessionTestBuilder().paid(0, 100_000).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("유료 강의는 최대 수강인원이 있어야 합니다"); - } - @Test void whenStartRecruiting_StatusIsOpen() { Session session = new SessionTestBuilder().free().build(); @@ -42,7 +30,8 @@ void whenSessionStatusIsNotOpen_thenThrows() { @Test void whenCapacityFull_thenThrows() { - Session session = new SessionTestBuilder().paid(5, 1000).id(1L).enrollCount(5).build(); + Session session = new Session(1L, SessionInfoTest.INFO, SessionPricingTest.PAID_SP, + CapacityTest.PAID_CAPACITy_FULL); session.startRecruiting(); assertThatThrownBy(() -> session.enroll(EnrollmentTest.E1)) .isInstanceOf(IllegalStateException.class) diff --git a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java index b2056a0b4..153771b2f 100644 --- a/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java +++ b/src/test/java/nextstep/sessions/domain/SessionTestBuilder.java @@ -52,9 +52,9 @@ public SessionTestBuilder enrollCount(int count) { public Session build() { if (paid) { - return new Session(id, startDate, endDate, true, maxCapacity, false, fee, enrollCount, image); + return Session.paidLimited(id, startDate, endDate, fee, maxCapacity, image); } - return new Session(id, startDate, endDate, false, maxCapacity, true, fee, enrollCount, image); + return Session.freeUnlimited(id, startDate, endDate, image); } } From d4b5dd9a5bcdfee85284ff97f4ef3e70c03a6ca6 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:31:44 +0900 Subject: [PATCH 27/28] =?UTF-8?q?feat:=20Pricing=EA=B3=BC=20Payment=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/sessions/domain/Enrollment.java | 4 ++++ src/main/java/nextstep/sessions/domain/Session.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/nextstep/sessions/domain/Enrollment.java b/src/main/java/nextstep/sessions/domain/Enrollment.java index 2ed4575e9..e9f9343d5 100644 --- a/src/main/java/nextstep/sessions/domain/Enrollment.java +++ b/src/main/java/nextstep/sessions/domain/Enrollment.java @@ -24,6 +24,10 @@ public Payment payment() { return payment; } + public boolean canPayFor(SessionPricing pricing) { + return !pricing.isPaid() || payment.isPaidFor(pricing.fee()); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java index 5b078ddb2..e768d461d 100644 --- a/src/main/java/nextstep/sessions/domain/Session.java +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -69,7 +69,7 @@ private void validateCapacity() { } private void validatePaymentAmount(Enrollment enrollment) { - if (pricing.isPaid() && !enrollment.payment().isPaidFor(pricing.fee())) { + if (!enrollment.canPayFor(pricing)) { throw new IllegalArgumentException(ERROR_PAYMENT_AMOUNT_MISMATCH); } } From 61df4208d7b3488c6542ed3a15b0b471044de1a6 Mon Sep 17 00:00:00 2001 From: username0w <163955522+username0w@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:42:36 +0900 Subject: [PATCH 28/28] =?UTF-8?q?refactor:=20Stream=20=EC=82=AC=EC=9A=A9,?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81?= =?UTF-8?q?=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/ImageType.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/ImageType.java b/src/main/java/nextstep/sessions/domain/ImageType.java index 5e212a280..4ef58b549 100644 --- a/src/main/java/nextstep/sessions/domain/ImageType.java +++ b/src/main/java/nextstep/sessions/domain/ImageType.java @@ -1,18 +1,22 @@ package nextstep.sessions.domain; +import java.util.Arrays; + public enum ImageType { GIF, JPG, JPEG, PNG, SVG; + private static final String ERROR_EMPTY_EXT = "확장자가 유효하지 않습니다"; + private static final String ERROR_UNSUPPORTED_EXT = "지원하지 않는 이미지 형식입니다: "; + public static ImageType from(String ext) { if (ext == null || ext.trim().isEmpty()) { - throw new IllegalArgumentException("확장자가 유효하지 않습니다"); + throw new IllegalArgumentException(ERROR_EMPTY_EXT); } - for (ImageType type : ImageType.values()) { - if (type.name().equalsIgnoreCase(ext)) { - return type; - } - } - throw new IllegalArgumentException("지원하지 않는 이미지 형식입니다: " + ext); + + return Arrays.stream(ImageType.values()) + .filter(type -> type.name().equalsIgnoreCase(ext)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(ERROR_UNSUPPORTED_EXT + ext)); } }