From 94f59200d43ccce07f2e33b2a3f5a8f1f767ee96 Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:44:56 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[refactor]=20Course=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/courses/domain/{ => course}/Course.java | 2 +- .../courses/domain/{ => course}/CourseRepository.java | 2 +- .../nextstep/courses/infrastructure/JdbcCourseRepository.java | 4 ++-- .../nextstep/courses/infrastructure/CourseRepositoryTest.java | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/nextstep/courses/domain/{ => course}/Course.java (96%) rename src/main/java/nextstep/courses/domain/{ => course}/CourseRepository.java (71%) diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/course/Course.java similarity index 96% rename from src/main/java/nextstep/courses/domain/Course.java rename to src/main/java/nextstep/courses/domain/course/Course.java index 0f6971604..1c935f2fc 100644 --- a/src/main/java/nextstep/courses/domain/Course.java +++ b/src/main/java/nextstep/courses/domain/course/Course.java @@ -1,4 +1,4 @@ -package nextstep.courses.domain; +package nextstep.courses.domain.course; import java.time.LocalDateTime; diff --git a/src/main/java/nextstep/courses/domain/CourseRepository.java b/src/main/java/nextstep/courses/domain/course/CourseRepository.java similarity index 71% rename from src/main/java/nextstep/courses/domain/CourseRepository.java rename to src/main/java/nextstep/courses/domain/course/CourseRepository.java index 6aaeb638d..28180d25e 100644 --- a/src/main/java/nextstep/courses/domain/CourseRepository.java +++ b/src/main/java/nextstep/courses/domain/course/CourseRepository.java @@ -1,4 +1,4 @@ -package nextstep.courses.domain; +package nextstep.courses.domain.course; public interface CourseRepository { int save(Course course); diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java index f9122cbe3..7489afc31 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java @@ -1,7 +1,7 @@ package nextstep.courses.infrastructure; -import nextstep.courses.domain.Course; -import nextstep.courses.domain.CourseRepository; +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.course.CourseRepository; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; diff --git a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java index f087fc0ad..530375d3e 100644 --- a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java +++ b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java @@ -1,7 +1,7 @@ package nextstep.courses.infrastructure; -import nextstep.courses.domain.Course; -import nextstep.courses.domain.CourseRepository; +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.course.CourseRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; From 2dadcc41b760820ac5ad077ae0d6008742c59bcf Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:45:31 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[feature]=20Image=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=8D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../courses/InvalidImageFileException.java | 7 ++++ .../nextstep/courses/domain/image/Image.java | 13 +++++++ .../courses/domain/image/ImageDimension.java | 28 ++++++++++++++ .../domain/image/ImageFileExtension.java | 37 +++++++++++++++++++ .../courses/domain/image/ImageSize.java | 19 ++++++++++ .../domain/image/ImageDimensionTest.java | 19 ++++++++++ .../domain/image/ImageFileExtensionTest.java | 14 +++++++ .../courses/domain/image/ImageSizeTest.java | 14 +++++++ 8 files changed, 151 insertions(+) create mode 100644 src/main/java/nextstep/courses/InvalidImageFileException.java create mode 100644 src/main/java/nextstep/courses/domain/image/Image.java create mode 100644 src/main/java/nextstep/courses/domain/image/ImageDimension.java create mode 100644 src/main/java/nextstep/courses/domain/image/ImageFileExtension.java create mode 100644 src/main/java/nextstep/courses/domain/image/ImageSize.java create mode 100644 src/test/java/nextstep/courses/domain/image/ImageDimensionTest.java create mode 100644 src/test/java/nextstep/courses/domain/image/ImageFileExtensionTest.java create mode 100644 src/test/java/nextstep/courses/domain/image/ImageSizeTest.java diff --git a/src/main/java/nextstep/courses/InvalidImageFileException.java b/src/main/java/nextstep/courses/InvalidImageFileException.java new file mode 100644 index 000000000..152ded6b0 --- /dev/null +++ b/src/main/java/nextstep/courses/InvalidImageFileException.java @@ -0,0 +1,7 @@ +package nextstep.courses; + +public class InvalidImageFileException extends RuntimeException { + public InvalidImageFileException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/courses/domain/image/Image.java b/src/main/java/nextstep/courses/domain/image/Image.java new file mode 100644 index 000000000..126591c60 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/Image.java @@ -0,0 +1,13 @@ +package nextstep.courses.domain.image; + +public class Image { + private final ImageSize size; + private final ImageDimension imageDimension; + private final ImageFileExtension fileExtension; + + public Image(int size, int width, int height, String fileExtension) { + this.size = new ImageSize(size); + this.imageDimension = new ImageDimension(width, height); + this.fileExtension = ImageFileExtension.from(fileExtension); + } +} diff --git a/src/main/java/nextstep/courses/domain/image/ImageDimension.java b/src/main/java/nextstep/courses/domain/image/ImageDimension.java new file mode 100644 index 000000000..64fbeaf93 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/ImageDimension.java @@ -0,0 +1,28 @@ +package nextstep.courses.domain.image; + +import nextstep.courses.InvalidImageFileException; + +public class ImageDimension { + private final static int MIN_WIDTH = 300; // 300 px + private final static int MIN_HEIGHT = 200; // 200 px + private final static double ASPECT_RATIO = 1.5; // width : height = 3 : 2 + + private final int width; + private final int height; + + public ImageDimension(int width, int height) { + if (!isValidWidthAndHeight(width, height)) { + throw new InvalidImageFileException(String.format("이미지는 가로 %spx 이상, 세로 %spx 이상, 가로 세로 비율이 %s:1 이어야 합니다.", MIN_WIDTH, MIN_HEIGHT, ASPECT_RATIO)); + } + this.width = width; + this.height = height; + } + + private boolean isValidWidthAndHeight(int width, int height) { + return width >= MIN_WIDTH && height >= MIN_HEIGHT && isValidRatio(width, height); + } + + private boolean isValidRatio(int width, int height) { + return (double) width / height == ASPECT_RATIO; + } +} diff --git a/src/main/java/nextstep/courses/domain/image/ImageFileExtension.java b/src/main/java/nextstep/courses/domain/image/ImageFileExtension.java new file mode 100644 index 000000000..99b1edc20 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/ImageFileExtension.java @@ -0,0 +1,37 @@ +package nextstep.courses.domain.image; + +import nextstep.courses.InvalidImageFileException; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public enum ImageFileExtension { + GIF("gif"), + JPG("jpg"), + JPEG("jpeg"), + PNG("png"), + SVG("svg"); + + private final String value; + + ImageFileExtension(String value) { + this.value = value; + } + + private String value() { + return value; + } + + static ImageFileExtension from(String value) { + return Arrays.stream(ImageFileExtension.values()) + .filter(fileExtension -> fileExtension.value.equals(value)) + .findFirst() + .orElseThrow(() -> new InvalidImageFileException(String.format("이미지 파일은 %s 중에 하나의 형태여야 합니다.", possibleFileExtensions()))); + } + + private static String possibleFileExtensions() { + return Arrays.stream(ImageFileExtension.values()) + .map(ImageFileExtension::value) + .collect(Collectors.joining(", ")); + } +} diff --git a/src/main/java/nextstep/courses/domain/image/ImageSize.java b/src/main/java/nextstep/courses/domain/image/ImageSize.java new file mode 100644 index 000000000..7688e5e54 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/ImageSize.java @@ -0,0 +1,19 @@ +package nextstep.courses.domain.image; + +import nextstep.courses.InvalidImageFileException; + +class ImageSize { + private final static int MAX_SIZE = 1_024; // 1024 Byte + private final int size; + + ImageSize(int size) { + if (!isValidSize(size)) { + throw new InvalidImageFileException(String.format("이미지 사이즈는 %sB 이하여야 합니다.", MAX_SIZE)); + } + this.size = size; + } + + private boolean isValidSize(int size) { + return size <= MAX_SIZE; + } +} diff --git a/src/test/java/nextstep/courses/domain/image/ImageDimensionTest.java b/src/test/java/nextstep/courses/domain/image/ImageDimensionTest.java new file mode 100644 index 000000000..50c07ebd2 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/image/ImageDimensionTest.java @@ -0,0 +1,19 @@ +package nextstep.courses.domain.image; + +import nextstep.courses.InvalidImageFileException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ImageDimensionTest { + @ParameterizedTest + @CsvSource(value = {"90,60", "300,300"}) + void 이미지_너비와_높이와_비율에_제한이_있다(int width, int height) { + assertThatThrownBy(() -> new ImageDimension(90, 60)) + .isInstanceOf(InvalidImageFileException.class); + + assertThatThrownBy(() -> new ImageDimension(300, 300)) + .isInstanceOf(InvalidImageFileException.class); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/image/ImageFileExtensionTest.java b/src/test/java/nextstep/courses/domain/image/ImageFileExtensionTest.java new file mode 100644 index 000000000..6390c7c8b --- /dev/null +++ b/src/test/java/nextstep/courses/domain/image/ImageFileExtensionTest.java @@ -0,0 +1,14 @@ +package nextstep.courses.domain.image; + +import nextstep.courses.InvalidImageFileException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ImageFileExtensionTest { + @Test + void 이미지_확장자는_gif_jpg_png_svg만_가능하다() { + assertThatThrownBy(() -> ImageFileExtension.from("txt")) + .isInstanceOf(InvalidImageFileException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/image/ImageSizeTest.java b/src/test/java/nextstep/courses/domain/image/ImageSizeTest.java new file mode 100644 index 000000000..c7afd3a72 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/image/ImageSizeTest.java @@ -0,0 +1,14 @@ +package nextstep.courses.domain.image; + +import nextstep.courses.InvalidImageFileException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ImageSizeTest { + @Test + void 이미지의_크기는_1MB_이하이다() { + assertThatThrownBy(() -> new ImageSize(1025)) + .isInstanceOf(InvalidImageFileException.class); + } +} From c1cff29c0811bbb9e4c11fd2bcae4a7b0bea3e92 Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:46:01 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[feature]=20=EC=9C=A0/=EB=AC=B4=EB=A3=8C?= =?UTF-8?q?=20SessionType=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../courses/domain/session/type/Free.java | 18 ++++++++++++ .../courses/domain/session/type/Paid.java | 23 +++++++++++++++ .../domain/session/type/SessionType.java | 9 ++++++ .../courses/domain/session/type/PaidTest.java | 29 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/session/type/Free.java create mode 100644 src/main/java/nextstep/courses/domain/session/type/Paid.java create mode 100644 src/main/java/nextstep/courses/domain/session/type/SessionType.java create mode 100644 src/test/java/nextstep/courses/domain/session/type/PaidTest.java diff --git a/src/main/java/nextstep/courses/domain/session/type/Free.java b/src/main/java/nextstep/courses/domain/session/type/Free.java new file mode 100644 index 000000000..4051edc9c --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/type/Free.java @@ -0,0 +1,18 @@ +package nextstep.courses.domain.session.type; + +import nextstep.courses.domain.session.EnrollmentCondition; + +public class Free implements SessionType { + public Free() { + } + + @Override + public boolean canEnroll(EnrollmentCondition request) { + return true; + } + + @Override + public String toHumanReadableTypeName() { + return "무료"; + } +} diff --git a/src/main/java/nextstep/courses/domain/session/type/Paid.java b/src/main/java/nextstep/courses/domain/session/type/Paid.java new file mode 100644 index 000000000..5147cbfa3 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/type/Paid.java @@ -0,0 +1,23 @@ +package nextstep.courses.domain.session.type; + +import nextstep.courses.domain.session.EnrollmentCondition; + +public class Paid implements SessionType { + private final long sessionId; + private final long price; + + public Paid(long sessionId, long price) { + this.sessionId = sessionId; + this.price = price; + } + + @Override + public boolean canEnroll(EnrollmentCondition condition) { + return condition.hasPaid(this.sessionId, price); + } + + @Override + public String toHumanReadableTypeName() { + return "유료"; + } +} diff --git a/src/main/java/nextstep/courses/domain/session/type/SessionType.java b/src/main/java/nextstep/courses/domain/session/type/SessionType.java new file mode 100644 index 000000000..1dd7bd61c --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/type/SessionType.java @@ -0,0 +1,9 @@ +package nextstep.courses.domain.session.type; + +import nextstep.courses.domain.session.EnrollmentCondition; + +public interface SessionType { + boolean canEnroll(EnrollmentCondition request); + + String toHumanReadableTypeName(); +} diff --git a/src/test/java/nextstep/courses/domain/session/type/PaidTest.java b/src/test/java/nextstep/courses/domain/session/type/PaidTest.java new file mode 100644 index 000000000..8ad34e966 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/type/PaidTest.java @@ -0,0 +1,29 @@ +package nextstep.courses.domain.session.type; + +import nextstep.courses.domain.session.EnrollmentCondition; +import nextstep.courses.domain.session.EnrollmentConditionTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PaidTest { + private final static Long SESSION_ID = 123L; + + @Test + void 유료_강의는_수강생이_결제한_금액과_수강료가_일치할_때_수강_신청이_가능하다() { + Paid paidSession = new Paid(SESSION_ID, 30_000L); + assertThat(paidSession.canEnroll(EnrollmentConditionTest.JAVAJIGI_ENROLLMENT)).isTrue(); + } + + @Test + void 유료_강의는_수강생이_결제한_금액과_수강료가_불일치하면_수강_신청이_불가하다() { + Paid paidSession = new Paid(SESSION_ID, 20_000L); + assertThat(paidSession.canEnroll(EnrollmentConditionTest.JAVAJIGI_ENROLLMENT)).isFalse(); + } + + @Test + void 유료_강의는_수강생이_결제한_강의와_신청한_강의가_불일치하면_수강_신청이_불가하다() { + Paid paidSession = new Paid(124, 20_000L); + assertThat(paidSession.canEnroll(EnrollmentConditionTest.JAVAJIGI_ENROLLMENT)).isFalse(); + } +} \ No newline at end of file From 00ac3bdf58be3f00a20854cccb767a41ff9db2a9 Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:46:41 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[refactor]=20NsUsers=20=EC=9D=BC=EA=B8=89?= =?UTF-8?q?=20=EC=BB=AC=EB=A0=89=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/users/domain/NsUser.java | 80 +++---------------- .../java/nextstep/users/domain/NsUsers.java | 24 ++++++ 2 files changed, 34 insertions(+), 70 deletions(-) create mode 100644 src/main/java/nextstep/users/domain/NsUsers.java diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java index 62ec5138c..13aa0591c 100755 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -1,7 +1,5 @@ package nextstep.users.domain; -import nextstep.qna.UnAuthorizedException; - import java.time.LocalDateTime; import java.util.Objects; @@ -43,78 +41,20 @@ public Long getId() { return id; } - public String getUserId() { - return userId; - } - - public NsUser setUserId(String userId) { - this.userId = userId; - return this; - } - - public String getPassword() { - return password; - } - - public NsUser setPassword(String password) { - this.password = password; - return this; - } - - public String getName() { - return name; - } - - public NsUser setName(String name) { - this.name = name; - return this; - } - - public String getEmail() { - return email; - } - - public NsUser setEmail(String email) { - this.email = email; - return this; - } - - public void update(NsUser loginUser, NsUser target) { - if (!matchUserId(loginUser.getUserId())) { - throw new UnAuthorizedException(); - } - - if (!matchPassword(target.getPassword())) { - throw new UnAuthorizedException(); - } - - this.name = target.name; - this.email = target.email; - } - - public boolean matchUser(NsUser target) { - return matchUserId(target.getUserId()); - } - - private boolean matchUserId(String userId) { - return this.userId.equals(userId); - } - - public boolean matchPassword(String targetPassword) { - return password.equals(targetPassword); + public boolean isGuestUser() { + return false; } - public boolean equalsNameAndEmail(NsUser target) { - if (Objects.isNull(target)) { - return false; - } - - return name.equals(target.name) && - email.equals(target.email); + @Override + public boolean equals(Object o) { + 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(name, nsUser.name) && Objects.equals(email, nsUser.email); } - public boolean isGuestUser() { - return false; + @Override + public int hashCode() { + return Objects.hash(id, userId, name, email); } private static class GuestNsUser extends NsUser { diff --git a/src/main/java/nextstep/users/domain/NsUsers.java b/src/main/java/nextstep/users/domain/NsUsers.java new file mode 100644 index 000000000..ffd2186ea --- /dev/null +++ b/src/main/java/nextstep/users/domain/NsUsers.java @@ -0,0 +1,24 @@ +package nextstep.users.domain; + +import java.util.HashSet; +import java.util.Set; + +public class NsUsers { + private final Set users; + + public NsUsers() { + this(new HashSet<>()); + } + + public NsUsers(Set users) { + this.users = users; + } + + public void addUser(NsUser user) { + this.users.add(user); + } + + public boolean hasLessThan(int size) { + return this.users.size() < size; + } +} From 48e786b2c8888ec85d5bc573fd1fd701964243df Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:49:08 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feature]=20Session=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Session 도메인을 위한 하위 도메인도 추가 - BaseEntity - Period - SessionEnrollment - SessionStatus --- .../SessionUnregistrableException.java | 10 ++++ .../nextstep/courses/domain/BaseEntity.java | 21 +++++++ .../courses/domain/session/Period.java | 13 +++++ .../courses/domain/session/Session.java | 57 +++++++++++++++++++ .../domain/session/SessionEnrollment.java | 40 +++++++++++++ .../courses/domain/session/SessionStatus.java | 5 ++ .../domain/session/SessionEnrollmentTest.java | 18 ++++++ .../courses/domain/session/SessionTest.java | 45 +++++++++++++++ 8 files changed, 209 insertions(+) create mode 100644 src/main/java/nextstep/courses/SessionUnregistrableException.java create mode 100644 src/main/java/nextstep/courses/domain/BaseEntity.java create mode 100644 src/main/java/nextstep/courses/domain/session/Period.java create mode 100644 src/main/java/nextstep/courses/domain/session/Session.java create mode 100644 src/main/java/nextstep/courses/domain/session/SessionEnrollment.java create mode 100644 src/main/java/nextstep/courses/domain/session/SessionStatus.java create mode 100644 src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java create mode 100644 src/test/java/nextstep/courses/domain/session/SessionTest.java diff --git a/src/main/java/nextstep/courses/SessionUnregistrableException.java b/src/main/java/nextstep/courses/SessionUnregistrableException.java new file mode 100644 index 000000000..a9c00059d --- /dev/null +++ b/src/main/java/nextstep/courses/SessionUnregistrableException.java @@ -0,0 +1,10 @@ +package nextstep.courses; + +public class SessionUnregistrableException extends RuntimeException{ + public SessionUnregistrableException(){ + } + + public SessionUnregistrableException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/courses/domain/BaseEntity.java b/src/main/java/nextstep/courses/domain/BaseEntity.java new file mode 100644 index 000000000..7d9172da9 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/BaseEntity.java @@ -0,0 +1,21 @@ +package nextstep.courses.domain; + +import nextstep.courses.domain.session.Period; + +import java.time.LocalDate; + +public class BaseEntity { + private final Long id; + private final String title; + private final Period period; + + public BaseEntity(Long id, String title, LocalDate startDate, LocalDate endDate) { + this(id, title, new Period(startDate, endDate)); + } + + public BaseEntity(Long id, String title, Period period) { + this.id = id; + this.title = title; + this.period = period; + } +} diff --git a/src/main/java/nextstep/courses/domain/session/Period.java b/src/main/java/nextstep/courses/domain/session/Period.java new file mode 100644 index 000000000..909bffe84 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/Period.java @@ -0,0 +1,13 @@ +package nextstep.courses.domain.session; + +import java.time.LocalDate; + +public class Period { + private final LocalDate startDate; + private final LocalDate endDate; + + public Period(LocalDate startDate, LocalDate endDate) { + this.startDate = startDate; + this.endDate = endDate; + } +} diff --git a/src/main/java/nextstep/courses/domain/session/Session.java b/src/main/java/nextstep/courses/domain/session/Session.java new file mode 100644 index 000000000..e1d2d1949 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/Session.java @@ -0,0 +1,57 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.SessionUnregistrableException; +import nextstep.courses.domain.*; +import nextstep.courses.domain.image.Image; +import nextstep.courses.domain.session.type.Free; +import nextstep.courses.domain.session.type.Paid; +import nextstep.courses.domain.session.type.SessionType; + +import java.time.LocalDate; + +public class Session { + private final BaseEntity baseEntity; + private final Image image; + private SessionStatus status; + private final SessionType type; + private final SessionEnrollment enrollment; + + public Session(Long id, String title, Image image, SessionStatus status, SessionType type, SessionEnrollment enrollment, LocalDate startDate, LocalDate endDate) { + this.baseEntity = new BaseEntity(id, title, new Period(startDate, endDate)); + this.image = image; + this.status = status; + this.type = type; + this.enrollment = enrollment; + } + + public void openRecruiting() { + this.status = SessionStatus.RECRUITING; + } + + public void enroll(EnrollmentCondition request) { + checkEnrollable(request); + + this.enrollment.enroll(request.getUser()); + } + + private void checkEnrollable(EnrollmentCondition condition) { + if (!isRecruiting()) { + throw new SessionUnregistrableException(String.format("%s 상태인 강의는 수강신청할 수 없습니다.", this.status.name())); + } + if (!this.type.canEnroll(condition)) { + throw new SessionUnregistrableException(String.format("%s 강의 수강 신청 조건 미달로 신청할 수 없습니다.", this.type.toHumanReadableTypeName())); + } + } + + private boolean isRecruiting() { + return this.status == SessionStatus.RECRUITING; + } + + public static Session createFreeSession(String title, Image image) { + return new Session(null, title, image, SessionStatus.PREPARING, new Free(), new SessionEnrollment(), LocalDate.now(), LocalDate.MAX); + } + + public static Session createPaidSession(long id, String title, Image image, int maxCapacity, long price) { + return new Session(null, title, image, SessionStatus.PREPARING, new Paid(id, price), new SessionEnrollment(maxCapacity), LocalDate.now(), LocalDate.MAX); + } +} diff --git a/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java b/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java new file mode 100644 index 000000000..f6fd115a2 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java @@ -0,0 +1,40 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.SessionUnregistrableException; +import nextstep.users.domain.NsUser; +import nextstep.users.domain.NsUsers; + +/** + * 강의 정원 및 현재 등록 인원을 관리하는 도메인 + */ +public class SessionEnrollment { + private final int maxCapacity; + private final NsUsers enrolledUsers; + + public SessionEnrollment() { + this(-1, new NsUsers()); + } + + public SessionEnrollment(int maxCapacity) { + this(maxCapacity, new NsUsers()); + } + + public SessionEnrollment(int maxCapacity, NsUsers enrolledUsers) { + this.maxCapacity = maxCapacity; + this.enrolledUsers = enrolledUsers; + } + + public void enroll(NsUser user) { + if (!hasCapacity()) { + throw new SessionUnregistrableException("정원 초과로 수강신청할 수 없습니다."); + } + this.enrolledUsers.addUser(user); + } + + private boolean hasCapacity() { + if (maxCapacity == -1) { + return true; + } + return enrolledUsers.hasLessThan(maxCapacity); + } +} diff --git a/src/main/java/nextstep/courses/domain/session/SessionStatus.java b/src/main/java/nextstep/courses/domain/session/SessionStatus.java new file mode 100644 index 000000000..d565d88b1 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/SessionStatus.java @@ -0,0 +1,5 @@ +package nextstep.courses.domain.session; + +public enum SessionStatus { + PREPARING, RECRUITING, COMPLETED +} diff --git a/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java b/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java new file mode 100644 index 000000000..2e8e1b701 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java @@ -0,0 +1,18 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.SessionUnregistrableException; +import nextstep.users.domain.NsUserTest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class SessionEnrollmentTest { + @Test + void 최대_수강_인원을_초과하면_등록이_실패한다() { + SessionEnrollment enrollment = new SessionEnrollment(1); + enrollment.enroll(NsUserTest.JAVAJIGI); + assertThatThrownBy(() -> enrollment.enroll(NsUserTest.SANJIGI)) + .isInstanceOf(SessionUnregistrableException.class); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/SessionTest.java b/src/test/java/nextstep/courses/domain/session/SessionTest.java new file mode 100644 index 000000000..7a2d92f04 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/SessionTest.java @@ -0,0 +1,45 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.SessionUnregistrableException; +import nextstep.courses.domain.image.Image; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SessionTest { + private static final Image IMAGE = new Image(1_024, 300, 200, "png"); + private static final EnrollmentCondition JAVAJIGI = EnrollmentConditionTest.JAVAJIGI_ENROLLMENT; + private static final EnrollmentCondition SANJIGI = EnrollmentConditionTest.SANJIGI_ENROLLMENT; + + @Test + void 강의는_모집중_상태가_아니면_신청할_수_없다() { + Session freeSession = Session.createFreeSession("free", IMAGE); + assertThatThrownBy(() -> freeSession.enroll(JAVAJIGI)) + .isInstanceOf(SessionUnregistrableException.class); + } + + @Test + void 무료_강의는_인원수_제한없이_들을_수_있다() { + Session freeSession = Session.createFreeSession("free", IMAGE); + freeSession.openRecruiting(); + freeSession.enroll(JAVAJIGI); + freeSession.enroll(SANJIGI); + } + + @Test + void 유료_강의는_인원수_제한이_있다() { + Session paidSession = Session.createPaidSession(123L, "paid", IMAGE, 1, 30_000L); + paidSession.openRecruiting(); + paidSession.enroll(JAVAJIGI); + assertThatThrownBy(() -> paidSession.enroll(SANJIGI)) + .isInstanceOf(SessionUnregistrableException.class); + } + + @Test + void 유료_강의는_지불한_금액이_맞아야_한다() { + Session paidSession = Session.createPaidSession(123L, "paid", IMAGE, 1, 20_000L); + paidSession.openRecruiting(); + assertThatThrownBy(() -> paidSession.enroll(JAVAJIGI)) + .isInstanceOf(SessionUnregistrableException.class); + } +} \ No newline at end of file From 89f625d5b35f2dbd21235ef39fc023b5676ed2a0 Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:53:33 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[feature]=20EnrollmentCondition=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개인별 수강 신청 가능 조건 값을 관리하는 도메인 추가 - 개인별로 확인해야 할 조건이 추가될 경우 해당 도메인을 이용 --- .../domain/session/EnrollmentCondition.java | 32 +++++++++++++++++++ .../nextstep/payments/domain/Payment.java | 16 ++++++++++ .../session/EnrollmentConditionTest.java | 10 ++++++ .../courses/domain/session/SessionTest.java | 21 ++++++------ .../courses/domain/session/type/PaidTest.java | 14 ++++---- 5 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java create mode 100644 src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java diff --git a/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java b/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java new file mode 100644 index 000000000..640ed5740 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java @@ -0,0 +1,32 @@ +package nextstep.courses.domain.session; + +import nextstep.payments.domain.Payment; +import nextstep.users.domain.NsUser; + +/** + * 개인별 수강 신청 가능 조건 값을 관리하는 도메인 + */ +public class EnrollmentCondition { + private final NsUser user; + private final Payment payment; + + public EnrollmentCondition(NsUser user, Payment payment) { + if (!paidBySameUser(user, payment)) { + throw new IllegalArgumentException("잘못된 수강 신청 조건입니다. "); + } + this.user = user; + this.payment = payment; + } + + private boolean paidBySameUser(NsUser user, Payment payment) { + return payment.isPaidBy(user.getId()); + } + + public NsUser getUser() { + return user; + } + + public boolean hasPaid(Long sessionId, long price) { + return payment.isPaidFor(sessionId) && payment.isSameAmount(price); + } +} diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 57d833f85..b7b482109 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -19,6 +19,10 @@ public class Payment { public Payment() { } + public Payment(Long sessionId, Long nsUserId, Long amount) { + this(null, sessionId, nsUserId, amount); + } + public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.id = id; this.sessionId = sessionId; @@ -26,4 +30,16 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.amount = amount; this.createdAt = LocalDateTime.now(); } + + public boolean isSameAmount(long amount) { + return this.amount == amount; + } + + public boolean isPaidFor(Long sessionId) { + return sessionId.equals(this.sessionId); + } + + public boolean isPaidBy(Long userId) { + return userId.equals(this.nsUserId); + } } diff --git a/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java b/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java new file mode 100644 index 000000000..368258ca9 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java @@ -0,0 +1,10 @@ +package nextstep.courses.domain.session; + +import nextstep.payments.domain.Payment; +import nextstep.users.domain.NsUserTest; + +public class EnrollmentConditionTest { + public static final Long SESSION_ID = 123L; + public static final EnrollmentCondition JAVAJIGI_ENROLLMENT = new EnrollmentCondition(NsUserTest.JAVAJIGI, new Payment(SESSION_ID, NsUserTest.JAVAJIGI.getId(), 30_000L)); + public static final EnrollmentCondition SANJIGI_ENROLLMENT = new EnrollmentCondition(NsUserTest.SANJIGI, new Payment(SESSION_ID, NsUserTest.SANJIGI.getId(), 30_000L)); +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/SessionTest.java b/src/test/java/nextstep/courses/domain/session/SessionTest.java index 7a2d92f04..a8da91913 100644 --- a/src/test/java/nextstep/courses/domain/session/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/session/SessionTest.java @@ -5,16 +5,17 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static nextstep.courses.domain.session.EnrollmentConditionTest.SESSION_ID; +import static nextstep.courses.domain.session.EnrollmentConditionTest.JAVAJIGI_ENROLLMENT; +import static nextstep.courses.domain.session.EnrollmentConditionTest.SANJIGI_ENROLLMENT; class SessionTest { private static final Image IMAGE = new Image(1_024, 300, 200, "png"); - private static final EnrollmentCondition JAVAJIGI = EnrollmentConditionTest.JAVAJIGI_ENROLLMENT; - private static final EnrollmentCondition SANJIGI = EnrollmentConditionTest.SANJIGI_ENROLLMENT; @Test void 강의는_모집중_상태가_아니면_신청할_수_없다() { Session freeSession = Session.createFreeSession("free", IMAGE); - assertThatThrownBy(() -> freeSession.enroll(JAVAJIGI)) + assertThatThrownBy(() -> freeSession.enroll(JAVAJIGI_ENROLLMENT)) .isInstanceOf(SessionUnregistrableException.class); } @@ -22,24 +23,24 @@ class SessionTest { void 무료_강의는_인원수_제한없이_들을_수_있다() { Session freeSession = Session.createFreeSession("free", IMAGE); freeSession.openRecruiting(); - freeSession.enroll(JAVAJIGI); - freeSession.enroll(SANJIGI); + freeSession.enroll(JAVAJIGI_ENROLLMENT); + freeSession.enroll(SANJIGI_ENROLLMENT); } @Test void 유료_강의는_인원수_제한이_있다() { - Session paidSession = Session.createPaidSession(123L, "paid", IMAGE, 1, 30_000L); + Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 1, 30_000L); paidSession.openRecruiting(); - paidSession.enroll(JAVAJIGI); - assertThatThrownBy(() -> paidSession.enroll(SANJIGI)) + paidSession.enroll(JAVAJIGI_ENROLLMENT); + assertThatThrownBy(() -> paidSession.enroll(SANJIGI_ENROLLMENT)) .isInstanceOf(SessionUnregistrableException.class); } @Test void 유료_강의는_지불한_금액이_맞아야_한다() { - Session paidSession = Session.createPaidSession(123L, "paid", IMAGE, 1, 20_000L); + Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 1, 20_000L); paidSession.openRecruiting(); - assertThatThrownBy(() -> paidSession.enroll(JAVAJIGI)) + assertThatThrownBy(() -> paidSession.enroll(JAVAJIGI_ENROLLMENT)) .isInstanceOf(SessionUnregistrableException.class); } } \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/type/PaidTest.java b/src/test/java/nextstep/courses/domain/session/type/PaidTest.java index 8ad34e966..33471565b 100644 --- a/src/test/java/nextstep/courses/domain/session/type/PaidTest.java +++ b/src/test/java/nextstep/courses/domain/session/type/PaidTest.java @@ -1,29 +1,27 @@ package nextstep.courses.domain.session.type; -import nextstep.courses.domain.session.EnrollmentCondition; -import nextstep.courses.domain.session.EnrollmentConditionTest; import org.junit.jupiter.api.Test; +import static nextstep.courses.domain.session.EnrollmentConditionTest.SESSION_ID; +import static nextstep.courses.domain.session.EnrollmentConditionTest.JAVAJIGI_ENROLLMENT; import static org.assertj.core.api.Assertions.assertThat; class PaidTest { - private final static Long SESSION_ID = 123L; - @Test void 유료_강의는_수강생이_결제한_금액과_수강료가_일치할_때_수강_신청이_가능하다() { Paid paidSession = new Paid(SESSION_ID, 30_000L); - assertThat(paidSession.canEnroll(EnrollmentConditionTest.JAVAJIGI_ENROLLMENT)).isTrue(); + assertThat(paidSession.canEnroll(JAVAJIGI_ENROLLMENT)).isTrue(); } @Test void 유료_강의는_수강생이_결제한_금액과_수강료가_불일치하면_수강_신청이_불가하다() { Paid paidSession = new Paid(SESSION_ID, 20_000L); - assertThat(paidSession.canEnroll(EnrollmentConditionTest.JAVAJIGI_ENROLLMENT)).isFalse(); + assertThat(paidSession.canEnroll(JAVAJIGI_ENROLLMENT)).isFalse(); } @Test void 유료_강의는_수강생이_결제한_강의와_신청한_강의가_불일치하면_수강_신청이_불가하다() { - Paid paidSession = new Paid(124, 20_000L); - assertThat(paidSession.canEnroll(EnrollmentConditionTest.JAVAJIGI_ENROLLMENT)).isFalse(); + Paid paidSession = new Paid(SESSION_ID + 1, 20_000L); + assertThat(paidSession.canEnroll(JAVAJIGI_ENROLLMENT)).isFalse(); } } \ No newline at end of file From 1f5d6ad1cab5bd3741fcf9cd5ba7229b41bc5aba Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Tue, 9 Dec 2025 21:54:17 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[docs]=202=EB=8B=A8=EA=B3=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EB=B0=8F=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=98=EB=B0=8D=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 498506166..932e5e215 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,25 @@ * 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) \ No newline at end of file +* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) + +## 수강 신청 기능 요구사항 +- 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다. +- 강의는 시작일과 종료일을 가진다. +- 강의는 강의 커버 이미지 정보를 가진다. +- 이미지 크기는 1MB 이하여야 한다. +- 이미지 타입은 gif, jpg(jpeg 포함), png, svg만 허용한다. +- 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다. +- 강의는 무료 강의와 유료 강의로 나뉜다. +- 무료 강의는 최대 수강 인원 제한이 없다. +- 유료 강의는 강의 최대 수강 인원을 초과할 수 없다. +- 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다. +- 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다. +- 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. +- 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. +- 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다. + +## 프로그래밍 요구사항 +- DB 테이블 설계 없이 도메인 모델부터 구현한다. +- 도메인 모델은 TDD로 구현한다. +- 단, Service 클래스는 단위 테스트가 없어도 된다. From 631f0de0b5dc3bacd3617ecb938b52496a9d337f Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Thu, 11 Dec 2025 00:54:01 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[refactor]=20Capacity=20=EC=9B=90?= =?UTF-8?q?=EC=8B=9C=EA=B0=92=20=ED=8F=AC=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../courses/domain/session/Capacity.java | 22 +++++++++++++++++++ .../java/nextstep/users/domain/NsUsers.java | 6 +++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/session/Capacity.java diff --git a/src/main/java/nextstep/courses/domain/session/Capacity.java b/src/main/java/nextstep/courses/domain/session/Capacity.java new file mode 100644 index 000000000..83d571125 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/Capacity.java @@ -0,0 +1,22 @@ +package nextstep.courses.domain.session; + +public class Capacity { + private final static int UNLIMITED = -1; + private final int capacity; + + public Capacity() { + this(UNLIMITED); + } + + public Capacity(int capacity) { + this.capacity = capacity; + } + + public boolean isUnlimited() { + return capacity == UNLIMITED; + } + + public boolean isGreaterThan(int value) { + return capacity > value; + } +} diff --git a/src/main/java/nextstep/users/domain/NsUsers.java b/src/main/java/nextstep/users/domain/NsUsers.java index ffd2186ea..ae10684d8 100644 --- a/src/main/java/nextstep/users/domain/NsUsers.java +++ b/src/main/java/nextstep/users/domain/NsUsers.java @@ -1,5 +1,7 @@ package nextstep.users.domain; +import nextstep.courses.domain.session.Capacity; + import java.util.HashSet; import java.util.Set; @@ -18,7 +20,7 @@ public void addUser(NsUser user) { this.users.add(user); } - public boolean hasLessThan(int size) { - return this.users.size() < size; + public boolean isLessThan(Capacity capacity) { + return capacity.isGreaterThan(this.users.size()); } } From 928f9d47ac695538870da0bc4bd82f199de5850f Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Thu, 11 Dec 2025 00:55:53 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[refactor]=20=EC=88=98=EA=B0=95=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=A1=B0=EA=B1=B4=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/session/EnrollmentCondition.java | 2 +- .../domain/session/EnrollmentConditionTest.java | 15 ++++++++++++--- .../courses/domain/session/type/PaidTest.java | 10 +++++----- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java b/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java index 640ed5740..ddf60a27b 100644 --- a/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java +++ b/src/main/java/nextstep/courses/domain/session/EnrollmentCondition.java @@ -12,7 +12,7 @@ public class EnrollmentCondition { public EnrollmentCondition(NsUser user, Payment payment) { if (!paidBySameUser(user, payment)) { - throw new IllegalArgumentException("잘못된 수강 신청 조건입니다. "); + throw new IllegalArgumentException("수강 신청자와 결제자 정보가 불일치합니다."); } this.user = user; this.payment = payment; diff --git a/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java b/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java index 368258ca9..a8715a5de 100644 --- a/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java +++ b/src/test/java/nextstep/courses/domain/session/EnrollmentConditionTest.java @@ -2,9 +2,18 @@ import nextstep.payments.domain.Payment; import nextstep.users.domain.NsUserTest; +import org.junit.jupiter.api.Test; + +import static nextstep.courses.domain.session.SessionTest.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class EnrollmentConditionTest { - public static final Long SESSION_ID = 123L; - public static final EnrollmentCondition JAVAJIGI_ENROLLMENT = new EnrollmentCondition(NsUserTest.JAVAJIGI, new Payment(SESSION_ID, NsUserTest.JAVAJIGI.getId(), 30_000L)); - public static final EnrollmentCondition SANJIGI_ENROLLMENT = new EnrollmentCondition(NsUserTest.SANJIGI, new Payment(SESSION_ID, NsUserTest.SANJIGI.getId(), 30_000L)); + public static final EnrollmentCondition JAVAJIGI_CONDITION = new EnrollmentCondition(NsUserTest.JAVAJIGI, new Payment(SESSION_ID, NsUserTest.JAVAJIGI.getId(), 30_000L)); + public static final EnrollmentCondition SANJIGI_CONDITION = new EnrollmentCondition(NsUserTest.SANJIGI, new Payment(SESSION_ID, NsUserTest.SANJIGI.getId(), 30_000L)); + + @Test + void 수강신청자와_결제자가_서로_다르면_수강신청_조건을_충족하지_않는다() { + assertThatThrownBy(() -> new EnrollmentCondition(NsUserTest.JAVAJIGI, new Payment(SESSION_ID, NsUserTest.SANJIGI.getId(), 30_000L))) + .isInstanceOf(IllegalArgumentException.class); + } } \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/type/PaidTest.java b/src/test/java/nextstep/courses/domain/session/type/PaidTest.java index 33471565b..f6bc11278 100644 --- a/src/test/java/nextstep/courses/domain/session/type/PaidTest.java +++ b/src/test/java/nextstep/courses/domain/session/type/PaidTest.java @@ -2,26 +2,26 @@ import org.junit.jupiter.api.Test; -import static nextstep.courses.domain.session.EnrollmentConditionTest.SESSION_ID; -import static nextstep.courses.domain.session.EnrollmentConditionTest.JAVAJIGI_ENROLLMENT; +import static nextstep.courses.domain.session.SessionTest.SESSION_ID; +import static nextstep.courses.domain.session.EnrollmentConditionTest.JAVAJIGI_CONDITION; import static org.assertj.core.api.Assertions.assertThat; class PaidTest { @Test void 유료_강의는_수강생이_결제한_금액과_수강료가_일치할_때_수강_신청이_가능하다() { Paid paidSession = new Paid(SESSION_ID, 30_000L); - assertThat(paidSession.canEnroll(JAVAJIGI_ENROLLMENT)).isTrue(); + assertThat(paidSession.canEnroll(JAVAJIGI_CONDITION)).isTrue(); } @Test void 유료_강의는_수강생이_결제한_금액과_수강료가_불일치하면_수강_신청이_불가하다() { Paid paidSession = new Paid(SESSION_ID, 20_000L); - assertThat(paidSession.canEnroll(JAVAJIGI_ENROLLMENT)).isFalse(); + assertThat(paidSession.canEnroll(JAVAJIGI_CONDITION)).isFalse(); } @Test void 유료_강의는_수강생이_결제한_강의와_신청한_강의가_불일치하면_수강_신청이_불가하다() { Paid paidSession = new Paid(SESSION_ID + 1, 20_000L); - assertThat(paidSession.canEnroll(JAVAJIGI_ENROLLMENT)).isFalse(); + assertThat(paidSession.canEnroll(JAVAJIGI_CONDITION)).isFalse(); } } \ No newline at end of file From ed462e2816beeb594c54119e45a3822f1fafb5cd Mon Sep 17 00:00:00 2001 From: julee-0430 Date: Thu, 11 Dec 2025 00:57:04 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[refactor]=20SessionEnrollment=20?= =?UTF-8?q?=EC=97=90=20Session=20=EC=9D=84=20=EC=A1=B0=ED=95=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EA=B0=95=EC=9D=98=20=EC=A0=95=EC=9B=90=20=EB=B0=8F?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=EC=9D=B8=EC=9B=90=EC=9D=84=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../courses/domain/session/Session.java | 31 ++++++------- .../domain/session/SessionEnrollment.java | 35 ++++++++++----- .../domain/session/SessionEnrollmentTest.java | 44 ++++++++++++++++--- .../courses/domain/session/SessionTest.java | 42 ++++++------------ 4 files changed, 89 insertions(+), 63 deletions(-) diff --git a/src/main/java/nextstep/courses/domain/session/Session.java b/src/main/java/nextstep/courses/domain/session/Session.java index e1d2d1949..2ad502ad0 100644 --- a/src/main/java/nextstep/courses/domain/session/Session.java +++ b/src/main/java/nextstep/courses/domain/session/Session.java @@ -14,44 +14,39 @@ public class Session { private final Image image; private SessionStatus status; private final SessionType type; - private final SessionEnrollment enrollment; - public Session(Long id, String title, Image image, SessionStatus status, SessionType type, SessionEnrollment enrollment, LocalDate startDate, LocalDate endDate) { + public Session(Long id, String title, Image image, SessionStatus status, SessionType type, LocalDate startDate, LocalDate endDate) { this.baseEntity = new BaseEntity(id, title, new Period(startDate, endDate)); this.image = image; this.status = status; this.type = type; - this.enrollment = enrollment; } public void openRecruiting() { this.status = SessionStatus.RECRUITING; } - public void enroll(EnrollmentCondition request) { - checkEnrollable(request); + public boolean isRecruiting() { + return this.status == SessionStatus.RECRUITING; + } - this.enrollment.enroll(request.getUser()); + public boolean canEnroll(EnrollmentCondition request) { + return this.type.canEnroll(request); } - private void checkEnrollable(EnrollmentCondition condition) { - if (!isRecruiting()) { - throw new SessionUnregistrableException(String.format("%s 상태인 강의는 수강신청할 수 없습니다.", this.status.name())); - } - if (!this.type.canEnroll(condition)) { - throw new SessionUnregistrableException(String.format("%s 강의 수강 신청 조건 미달로 신청할 수 없습니다.", this.type.toHumanReadableTypeName())); - } + public String currentStatusToHumanReadable() { + return this.status.name(); } - private boolean isRecruiting() { - return this.status == SessionStatus.RECRUITING; + public String typeToHumanReadable() { + return this.type.toHumanReadableTypeName(); } public static Session createFreeSession(String title, Image image) { - return new Session(null, title, image, SessionStatus.PREPARING, new Free(), new SessionEnrollment(), LocalDate.now(), LocalDate.MAX); + return new Session(null, title, image, SessionStatus.PREPARING, new Free(), LocalDate.now(), LocalDate.MAX); } - public static Session createPaidSession(long id, String title, Image image, int maxCapacity, long price) { - return new Session(null, title, image, SessionStatus.PREPARING, new Paid(id, price), new SessionEnrollment(maxCapacity), LocalDate.now(), LocalDate.MAX); + public static Session createPaidSession(long id, String title, Image image, long price) { + return new Session(null, title, image, SessionStatus.PREPARING, new Paid(id, price), LocalDate.now(), LocalDate.MAX); } } diff --git a/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java b/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java index f6fd115a2..93bc23997 100644 --- a/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java +++ b/src/main/java/nextstep/courses/domain/session/SessionEnrollment.java @@ -1,40 +1,53 @@ package nextstep.courses.domain.session; import nextstep.courses.SessionUnregistrableException; -import nextstep.users.domain.NsUser; import nextstep.users.domain.NsUsers; /** * 강의 정원 및 현재 등록 인원을 관리하는 도메인 */ public class SessionEnrollment { - private final int maxCapacity; + private final Capacity maxCapacity; private final NsUsers enrolledUsers; + private final Session session; - public SessionEnrollment() { - this(-1, new NsUsers()); + public SessionEnrollment(Session session) { + this(new Capacity(), new NsUsers(), session); } - public SessionEnrollment(int maxCapacity) { - this(maxCapacity, new NsUsers()); + public SessionEnrollment(int maxCapacity, Session session) { + this(new Capacity(maxCapacity), new NsUsers(), session); } - public SessionEnrollment(int maxCapacity, NsUsers enrolledUsers) { + public SessionEnrollment(Capacity maxCapacity, NsUsers enrolledUsers, Session session) { this.maxCapacity = maxCapacity; this.enrolledUsers = enrolledUsers; + this.session = session; } - public void enroll(NsUser user) { + public void enroll(EnrollmentCondition condition) { + checkEnrollable(condition); + this.enrolledUsers.addUser(condition.getUser()); + } + + private void checkEnrollable(EnrollmentCondition condition) { + if (!session.isRecruiting()) { + throw new SessionUnregistrableException(String.format("%s 상태인 강의는 수강신청할 수 없습니다.", session.currentStatusToHumanReadable())); + } + + if (!session.canEnroll(condition)) { + throw new SessionUnregistrableException(String.format("%s 강의 수강 신청 조건 미달로 신청할 수 없습니다.", session.typeToHumanReadable())); + } + if (!hasCapacity()) { throw new SessionUnregistrableException("정원 초과로 수강신청할 수 없습니다."); } - this.enrolledUsers.addUser(user); } private boolean hasCapacity() { - if (maxCapacity == -1) { + if (maxCapacity.isUnlimited()) { return true; } - return enrolledUsers.hasLessThan(maxCapacity); + return enrolledUsers.isLessThan(maxCapacity); } } diff --git a/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java b/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java index 2e8e1b701..182666b2b 100644 --- a/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java +++ b/src/test/java/nextstep/courses/domain/session/SessionEnrollmentTest.java @@ -1,18 +1,50 @@ package nextstep.courses.domain.session; import nextstep.courses.SessionUnregistrableException; -import nextstep.users.domain.NsUserTest; +import nextstep.courses.domain.image.Image; import org.junit.jupiter.api.Test; +import static nextstep.courses.domain.session.EnrollmentConditionTest.JAVAJIGI_CONDITION; +import static nextstep.courses.domain.session.EnrollmentConditionTest.SANJIGI_CONDITION; +import static nextstep.courses.domain.session.SessionTest.SESSION_ID; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; class SessionEnrollmentTest { + private static final Image IMAGE = new Image(1_024, 300, 200, "png"); + + @Test + void 강의는_모집중_상태가_아니면_신청할_수_없다() { + Session freeSession = Session.createFreeSession("free", IMAGE); + SessionEnrollment enrollment = new SessionEnrollment(freeSession); + assertThatThrownBy(() -> enrollment.enroll(JAVAJIGI_CONDITION)) + .isInstanceOf(SessionUnregistrableException.class); + } + + @Test + void 무료_강의는_인원수_제한없이_들을_수_있다() { + Session freeSession = Session.createFreeSession("free", IMAGE); + freeSession.openRecruiting(); + SessionEnrollment enrollment = new SessionEnrollment(freeSession); + enrollment.enroll(JAVAJIGI_CONDITION); + enrollment.enroll(SANJIGI_CONDITION); + } + + @Test + void 유료_강의는_인원수_제한이_있다() { + Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 30_000L); + paidSession.openRecruiting(); + SessionEnrollment enrollment = new SessionEnrollment(1, paidSession); + enrollment.enroll(JAVAJIGI_CONDITION); + assertThatThrownBy(() -> enrollment.enroll(SANJIGI_CONDITION)) + .isInstanceOf(SessionUnregistrableException.class); + } + @Test - void 최대_수강_인원을_초과하면_등록이_실패한다() { - SessionEnrollment enrollment = new SessionEnrollment(1); - enrollment.enroll(NsUserTest.JAVAJIGI); - assertThatThrownBy(() -> enrollment.enroll(NsUserTest.SANJIGI)) + void 유료_강의는_지불한_금액이_맞아야_한다() { + Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 20_000L); + paidSession.openRecruiting(); + SessionEnrollment enrollment = new SessionEnrollment(1, paidSession); + assertThatThrownBy(() -> enrollment.enroll(JAVAJIGI_CONDITION)) .isInstanceOf(SessionUnregistrableException.class); } } \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/SessionTest.java b/src/test/java/nextstep/courses/domain/session/SessionTest.java index a8da91913..44d33dd88 100644 --- a/src/test/java/nextstep/courses/domain/session/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/session/SessionTest.java @@ -1,46 +1,32 @@ package nextstep.courses.domain.session; -import nextstep.courses.SessionUnregistrableException; import nextstep.courses.domain.image.Image; +import nextstep.payments.domain.Payment; +import nextstep.users.domain.NsUserTest; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static nextstep.courses.domain.session.EnrollmentConditionTest.SESSION_ID; -import static nextstep.courses.domain.session.EnrollmentConditionTest.JAVAJIGI_ENROLLMENT; -import static nextstep.courses.domain.session.EnrollmentConditionTest.SANJIGI_ENROLLMENT; +import static org.assertj.core.api.Assertions.assertThat; -class SessionTest { +public class SessionTest { + public static final Long SESSION_ID = 123L; private static final Image IMAGE = new Image(1_024, 300, 200, "png"); @Test - void 강의는_모집중_상태가_아니면_신청할_수_없다() { - Session freeSession = Session.createFreeSession("free", IMAGE); - assertThatThrownBy(() -> freeSession.enroll(JAVAJIGI_ENROLLMENT)) - .isInstanceOf(SessionUnregistrableException.class); - } - - @Test - void 무료_강의는_인원수_제한없이_들을_수_있다() { + void 모집중_상태로_변경하면_정상적으로_상태가_변경된다() { Session freeSession = Session.createFreeSession("free", IMAGE); freeSession.openRecruiting(); - freeSession.enroll(JAVAJIGI_ENROLLMENT); - freeSession.enroll(SANJIGI_ENROLLMENT); + assertThat(freeSession.isRecruiting()).isTrue(); } @Test - void 유료_강의는_인원수_제한이_있다() { - Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 1, 30_000L); + void 강의_수강신청_가능_조건을_입력해_수강신청_가능_여부를_확인한다() { + Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 20_000L); paidSession.openRecruiting(); - paidSession.enroll(JAVAJIGI_ENROLLMENT); - assertThatThrownBy(() -> paidSession.enroll(SANJIGI_ENROLLMENT)) - .isInstanceOf(SessionUnregistrableException.class); - } - @Test - void 유료_강의는_지불한_금액이_맞아야_한다() { - Session paidSession = Session.createPaidSession(SESSION_ID, "paid", IMAGE, 1, 20_000L); - paidSession.openRecruiting(); - assertThatThrownBy(() -> paidSession.enroll(JAVAJIGI_ENROLLMENT)) - .isInstanceOf(SessionUnregistrableException.class); + EnrollmentCondition trueCondition = new EnrollmentCondition(NsUserTest.JAVAJIGI, new Payment(SESSION_ID, NsUserTest.JAVAJIGI.getId(), 20_000L)); + assertThat(paidSession.canEnroll(trueCondition)).isTrue(); + + EnrollmentCondition falseCondition = new EnrollmentCondition(NsUserTest.SANJIGI, new Payment(SESSION_ID, NsUserTest.SANJIGI.getId(), 30_000L)); + assertThat(paidSession.canEnroll(falseCondition)).isFalse(); } } \ No newline at end of file