diff --git a/Step.md b/docs/Step01.md similarity index 100% rename from Step.md rename to docs/Step01.md diff --git a/docs/Step02.md b/docs/Step02.md new file mode 100644 index 0000000000..f00b4718a8 --- /dev/null +++ b/docs/Step02.md @@ -0,0 +1,38 @@ +## 수강 신청 기능 요구사항 +- 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다. +- 강의는 시작일과 종료일을 가진다. +- 강의는 강의 커버 이미지 정보를 가진다. + - 이미지 크기는 1MB 이하여야 한다. + - 이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다. + - 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다. +- 강의는 무료 강의와 유료 강의로 나뉜다. + - 무료 강의는 최대 수강 인원 제한이 없다. + - 유료 강의는 강의 최대 수강 인원을 초과할 수 없다. + - 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다. +- 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다. +- 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. +- 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. + - 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다. + + +## 클래스 +- `과정(Course)` : 1 + - 기수 + +- `강의(Session)` : N + - 금액 : 유료 / 무료 + - 유료 강의 + - 강의 최대 `수강 인원` 제한 + - `결재 금액` = 수강료] + - 무료 강의 + - 최대 수강인원 제한 없음 + - 상태 : 준비중, 모집중, 종료 + - 시작일, 종료일 + - 강의 커버 이미지 + +- `강의 커버 이미지 (SessionCoverImage)` + - 크기 : 1MB + - 타입 : gif, jpg(jpeg), png, svg + - dimension + - width >= 300, height >= 200 + - width : height = 3 : 2 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 0000000000..2f8bbf5b31 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/BaseEntity.java @@ -0,0 +1,36 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public abstract class BaseEntity { + private Long id; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + protected BaseEntity() { + this.createdAt = LocalDateTime.now(); + } + + protected BaseEntity(Long id) { + this.id = id; + this.createdAt = LocalDateTime.now(); + } + + protected BaseEntity(Long id, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.id = id; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long getId() { + return id; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/Course.java deleted file mode 100644 index 0f69716043..0000000000 --- a/src/main/java/nextstep/courses/domain/Course.java +++ /dev/null @@ -1,53 +0,0 @@ -package nextstep.courses.domain; - -import java.time.LocalDateTime; - -public class Course { - private Long id; - - private String title; - - private Long creatorId; - - private LocalDateTime createdAt; - - private LocalDateTime updatedAt; - - public Course() { - } - - public Course(String title, Long creatorId) { - this(0L, title, creatorId, LocalDateTime.now(), null); - } - - public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.title = title; - this.creatorId = creatorId; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public String getTitle() { - return title; - } - - public Long getCreatorId() { - return creatorId; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - @Override - public String toString() { - return "Course{" + - "id=" + id + - ", title='" + title + '\'' + - ", creatorId=" + creatorId + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + - '}'; - } -} diff --git a/src/main/java/nextstep/courses/domain/course/Course.java b/src/main/java/nextstep/courses/domain/course/Course.java new file mode 100644 index 0000000000..9e390236f0 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/Course.java @@ -0,0 +1,61 @@ +package nextstep.courses.domain.course; + +import java.time.LocalDateTime; +import java.util.Objects; +import nextstep.courses.domain.BaseEntity; + +public class Course extends BaseEntity { + private String title; + private Long creatorId; + + public Course() { + } + + public Course(String title, Long creatorId) { + this(null, title, creatorId); + } + + public Course(Long id, String title, Long creatorId) { + super(id); + this.title = title; + this.creatorId = creatorId; + } + + public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + super(id, createdAt, updatedAt); + this.title = title; + this.creatorId = creatorId; + } + + public String getTitle() { + return title; + } + + public Long getCreatorId() { + return creatorId; + } + + @Override + public String toString() { + return "Course{" + + "id=" + getId() + + ", title='" + title + '\'' + + ", creatorId=" + creatorId + + ", createdAt=" + getCreatedAt() + + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Course course = (Course) o; + return Objects.equals(getId(), course.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } +} 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 6aaeb638d1..28180d25e0 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/domain/image/SessionCoverImage.java b/src/main/java/nextstep/courses/domain/image/SessionCoverImage.java new file mode 100644 index 0000000000..814e160962 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/SessionCoverImage.java @@ -0,0 +1,17 @@ +package nextstep.courses.domain.image; + +public class SessionCoverImage { + private final SessionImageDimension dimension; + private final SessionImageExtension extension; + private final SessionImageCapacity capacity; + + public SessionCoverImage(int width, int height, String extension, long bytes) { + this(new SessionImageDimension(width, height), SessionImageExtension.from(extension), new SessionImageCapacity(bytes)); + } + + public SessionCoverImage(SessionImageDimension dimension, SessionImageExtension extension, SessionImageCapacity capacity) { + this.dimension = dimension; + this.extension = extension; + this.capacity = capacity; + } +} diff --git a/src/main/java/nextstep/courses/domain/image/SessionImageCapacity.java b/src/main/java/nextstep/courses/domain/image/SessionImageCapacity.java new file mode 100644 index 0000000000..d9477c35fc --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/SessionImageCapacity.java @@ -0,0 +1,29 @@ +package nextstep.courses.domain.image; + +public class SessionImageCapacity { + private static final long MAX_CAPACITY_BYTES = 1024 * 1024; // 1MB + private final long bytes; + + public SessionImageCapacity(long bytes) { + validate(bytes); + this.bytes = bytes; + } + + public static SessionImageCapacity ofKB(long kb) { + return new SessionImageCapacity(kb * 1024); + } + + public static SessionImageCapacity ofMB(long mb) { + return new SessionImageCapacity(mb * 1024 * 1024); + } + + private void validate(long bytes) { + if (bytes <= 0) { + throw new IllegalArgumentException("이미지 크기는 0보다 커야 합니다."); + } + if (bytes > MAX_CAPACITY_BYTES) { + throw new IllegalArgumentException("이미지 크기는 1MB 이하여야 합니다."); + } + } + +} diff --git a/src/main/java/nextstep/courses/domain/image/SessionImageDimension.java b/src/main/java/nextstep/courses/domain/image/SessionImageDimension.java new file mode 100644 index 0000000000..f033bb6351 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/SessionImageDimension.java @@ -0,0 +1,38 @@ +package nextstep.courses.domain.image; + +public class SessionImageDimension { + private static final int MIN_WIDTH = 300; + private static final int MIN_HEIGHT = 200; + private static final int RATIO_W = 3; + private static final int RATIO_H = 2; + + private int width; + private int height; + + public SessionImageDimension(int width, int height) { + validateMinLength(width, height); + validateRatio(width, height); + this.width = width; + this.height = height; + } + private void validateMinLength(int width, int height){ + if(!(width >= MIN_WIDTH && height >= MIN_HEIGHT)) { + throw new IllegalArgumentException("이미지는 가로 300이상, 세로 200 이상이어야 합니다."); + } + } + + private void validateRatio(int width, int height){ + int gcd = gcd(width, height); + int ratioW = width / gcd; + int ratioH = height / gcd; + if(!(ratioW == RATIO_W && ratioH == RATIO_H)){ + throw new IllegalArgumentException("이미지는 가로 x 세로 3대 2이어야 합니다."); + } + } + + private int gcd(int a, int b) { + return b == 0 ? a : gcd(b, a % b); + } + + +} diff --git a/src/main/java/nextstep/courses/domain/image/SessionImageExtension.java b/src/main/java/nextstep/courses/domain/image/SessionImageExtension.java new file mode 100644 index 0000000000..d1e7515d90 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/image/SessionImageExtension.java @@ -0,0 +1,27 @@ +package nextstep.courses.domain.image; + +import java.util.ArrayList; +import java.util.List; + +public enum SessionImageExtension { + GIF("gif"), + JPG("jpg", "jpeg"), + PNG("png"), + SVG("svg"); + + private List names; + + SessionImageExtension(String... names){ + this.names = new ArrayList<>(List.of(names)); + } + + public static SessionImageExtension from(String extension) { + String lowerExt = extension.toLowerCase(); + for (SessionImageExtension type : values()) { + if (type.names.contains(lowerExt)) { + return type; + } + } + throw new IllegalArgumentException("지원하지 않는 이미지 확장자입니다: " + extension); + } +} diff --git a/src/main/java/nextstep/courses/domain/registration/Registration.java b/src/main/java/nextstep/courses/domain/registration/Registration.java new file mode 100644 index 0000000000..b9e25cda08 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/registration/Registration.java @@ -0,0 +1,23 @@ +package nextstep.courses.domain.registration; + +import java.time.LocalDateTime; + +public class Registration { + private final Long sessionId; + private final Long studentId; + private final LocalDateTime enrolledAt; + + public Registration(Long sessionId, Long studentId) { + this(sessionId, studentId, LocalDateTime.now()); + } + + public Registration(Long sessionId, Long studentId, LocalDateTime enrolledAt) { + this.sessionId = sessionId; + this.studentId = studentId; + this.enrolledAt = enrolledAt; + } + public boolean contains(Long studentId) { + return this.studentId == studentId; + } + +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/registration/Registrations.java b/src/main/java/nextstep/courses/domain/registration/Registrations.java new file mode 100644 index 0000000000..d85102f1ab --- /dev/null +++ b/src/main/java/nextstep/courses/domain/registration/Registrations.java @@ -0,0 +1,33 @@ +package nextstep.courses.domain.registration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Registrations { + private final List registrations; + + public Registrations() { + this(new ArrayList<>()); + } + + public Registrations(List registrations) { + this.registrations = registrations; + } + + public Registrations add(Registration registration) { + List newList = new ArrayList<>(registrations); + newList.add(registration); + return new Registrations(newList); + } + + public int count() { + return registrations.size(); + } + + public boolean contains(Long studentId) { + return registrations.stream() + .anyMatch(r -> r.contains(studentId)); + } + +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/session/Enrollment.java b/src/main/java/nextstep/courses/domain/session/Enrollment.java new file mode 100644 index 0000000000..8349d21eea --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/Enrollment.java @@ -0,0 +1,41 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.domain.session.type.FreeType; +import nextstep.courses.domain.session.type.SessionType; + +public class Enrollment { + private SessionState state; + private SessionType type; + + public Enrollment() { + this(SessionState.PREPARING, new FreeType()); + } + + public Enrollment(SessionType type) { + this(SessionState.PREPARING, type); + } + + public Enrollment(SessionState state, SessionType type) { + this.state = state; + this.type = type; + } + + public void enroll(long payAmount) { + validateState(); + this.type = type.enroll(payAmount); + } + + public void open() { + this.state = state.open(); + } + + public void close() { + this.state = state.close(); + } + + private void validateState() { + if (!state.canEnroll()) { + throw new IllegalStateException("모집중인 강의만 수강신청이 가능합니다."); + } + } +} \ No newline at end of file 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 0000000000..a506fbbe29 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/Session.java @@ -0,0 +1,38 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.domain.BaseEntity; +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.image.SessionCoverImage; + +public class Session extends BaseEntity { + private final Course course; + private final Term term; + private final SessionCoverImage cover; + private final SessionPeriod period; + private final Enrollment enrollment; + + public Session(Course course, int term, SessionCoverImage cover, String startDay, String endDay) { + this(null, course, new Term(term), cover, new SessionPeriod(startDay, endDay), new Enrollment()); + } + + public Session(Long id, Course course, Term term, SessionCoverImage cover, SessionPeriod period, Enrollment enrollment) { + super(id); + this.course = course; + this.term = term; + this.cover = cover; + this.period = period; + this.enrollment = enrollment; + } + + public void enroll(long payAmount) { + enrollment.enroll(payAmount); + } + + public void open() { + enrollment.open(); + } + + public void close() { + enrollment.close(); + } +} diff --git a/src/main/java/nextstep/courses/domain/session/SessionPeriod.java b/src/main/java/nextstep/courses/domain/session/SessionPeriod.java new file mode 100644 index 0000000000..af5089b4c3 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/SessionPeriod.java @@ -0,0 +1,24 @@ +package nextstep.courses.domain.session; + +import java.time.LocalDate; + +public class SessionPeriod { + private final LocalDate startDay; + private final LocalDate endDay; + + public SessionPeriod(String startDay, String endDay) { + this(LocalDate.parse(startDay), LocalDate.parse(endDay)); + } + + public SessionPeriod(LocalDate startDay, LocalDate endDay) { + validate(startDay, endDay); + this.startDay = startDay; + this.endDay = endDay; + } + + private void validate(LocalDate startDay, LocalDate endDay) { + if (startDay.isAfter(endDay)) { + throw new IllegalArgumentException("시작일은 종료일보다 이전이어야 합니다."); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/session/SessionState.java b/src/main/java/nextstep/courses/domain/session/SessionState.java new file mode 100644 index 0000000000..a0cab17411 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/SessionState.java @@ -0,0 +1,25 @@ +package nextstep.courses.domain.session; + +public enum SessionState { + PREPARING, + RECRUITING, + CLOSED; + + public SessionState open() { + if (this != PREPARING) { + throw new IllegalStateException("준비중인 강의만 모집을 시작할 수 있습니다."); + } + return RECRUITING; + } + + public SessionState close() { + if (this != RECRUITING) { + throw new IllegalStateException("모집중인 강의만 종료할 수 있습니다."); + } + return CLOSED; + } + + public boolean canEnroll() { + return this == RECRUITING; + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/session/Term.java b/src/main/java/nextstep/courses/domain/session/Term.java new file mode 100644 index 0000000000..66523f35d7 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/Term.java @@ -0,0 +1,37 @@ +package nextstep.courses.domain.session; + +import java.util.Objects; + +public class Term { + private static final int MIN_TERM = 1; + + private final int value; + + public Term(int value) { + validate(value); + this.value = value; + } + + private void validate(int value) { + if (value < MIN_TERM) { + throw new IllegalArgumentException("기수는 " + MIN_TERM + " 이상이어야 합니다."); + } + } + + public int getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Term term = (Term) o; + return value == term.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/session/type/FreeType.java b/src/main/java/nextstep/courses/domain/session/type/FreeType.java new file mode 100644 index 0000000000..b6b425d1af --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/type/FreeType.java @@ -0,0 +1,9 @@ +package nextstep.courses.domain.session.type; + +public class FreeType implements SessionType { + + @Override + public SessionType enroll(long payAmount) { + return this; + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/session/type/PaidType.java b/src/main/java/nextstep/courses/domain/session/type/PaidType.java new file mode 100644 index 0000000000..dc680bca29 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/type/PaidType.java @@ -0,0 +1,55 @@ +package nextstep.courses.domain.session.type; + +import java.util.Objects; + +public class PaidType implements SessionType { + private final int maxCapacity; + private final long tuitionFee; + private final int studentCount; + + public PaidType(int maxCapacity, long tuitionFee) { + this(maxCapacity, tuitionFee, 0); + } + + public PaidType(int maxCapacity, long tuitionFee, int studentCount) { + validateCapacity(maxCapacity, studentCount); + this.maxCapacity = maxCapacity; + this.tuitionFee = tuitionFee; + this.studentCount = studentCount; + } + + @Override + public SessionType enroll(long payAmount) { + int newCount = studentCount + 1; + validateCapacity(maxCapacity, newCount); + validateTuitionFee(payAmount); + return new PaidType(maxCapacity, tuitionFee, newCount); + } + + private void validateCapacity(int maxCapacity, int studentCount) { + if (maxCapacity < studentCount) { + throw new IllegalArgumentException("최대 수강 인원을 초과할 수 없습니다."); + } + } + + private void validateTuitionFee(long payAmount) { + if (payAmount != tuitionFee) { + throw new IllegalArgumentException("수강료와 지불한 금액이 정확히 일치해야 합니다."); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PaidType that = (PaidType) o; + return maxCapacity == that.maxCapacity + && tuitionFee == that.tuitionFee + && studentCount == that.studentCount; + } + + @Override + public int hashCode() { + return Objects.hash(maxCapacity, tuitionFee, studentCount); + } +} \ No newline at end of file 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 0000000000..3998c88427 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/session/type/SessionType.java @@ -0,0 +1,5 @@ +package nextstep.courses.domain.session.type; + +public interface SessionType { + SessionType enroll(long payAmount); +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java index f9122cbe33..7489afc31d 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/main/java/nextstep/courses/service/SessionEnrollService.java b/src/main/java/nextstep/courses/service/SessionEnrollService.java new file mode 100644 index 0000000000..5b12a385a7 --- /dev/null +++ b/src/main/java/nextstep/courses/service/SessionEnrollService.java @@ -0,0 +1,16 @@ +package nextstep.courses.service; + +import nextstep.courses.domain.course.CourseRepository; +import nextstep.courses.domain.session.Session; +import nextstep.payments.domain.Payment; +import org.springframework.stereotype.Service; + +@Service +public class SessionEnrollService { + private CourseRepository courseRepository; + + public void enroll(Session session, Payment payment){ + session.enroll(payment.getAmount()); + } + +} diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 57d833f851..8dcd2fa39a 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -26,4 +26,16 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.amount = amount; this.createdAt = LocalDateTime.now(); } + + public Long getSessionId() { + return sessionId; + } + + public Long getNsUserId() { + return nsUserId; + } + + public Long getAmount() { + return amount; + } } diff --git a/src/test/java/nextstep/courses/domain/course/CourseTest.java b/src/test/java/nextstep/courses/domain/course/CourseTest.java new file mode 100644 index 0000000000..19f32d5394 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/CourseTest.java @@ -0,0 +1,5 @@ +package nextstep.courses.domain.course; + +class CourseTest { + +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/image/SessionCoverImageTest.java b/src/test/java/nextstep/courses/domain/image/SessionCoverImageTest.java new file mode 100644 index 0000000000..795afcfeee --- /dev/null +++ b/src/test/java/nextstep/courses/domain/image/SessionCoverImageTest.java @@ -0,0 +1,28 @@ +package nextstep.courses.domain.image; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SessionCoverImageTest { + private static final int WIDTH = 300, HEIGHT = 200; + private static final long VALID_BYTES = 1024 * 500; // 500KB + + @ParameterizedTest + @CsvSource({"gif","jpg","jpeg", "png", "svg"}) + void 허용된_확장자(String extension){ + assertDoesNotThrow(() -> new SessionCoverImage(WIDTH, HEIGHT, extension, VALID_BYTES)); + } + + @ParameterizedTest + @CsvSource({"webp"}) + void 허용하지않은_확장자는_예외(String extension){ + assertThatThrownBy(() -> new SessionCoverImage(WIDTH, HEIGHT, extension, VALID_BYTES)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("지원하지 않는 이미지 확장자입니다: " + extension); + } + + +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/image/SessionImageCapacityTest.java b/src/test/java/nextstep/courses/domain/image/SessionImageCapacityTest.java new file mode 100644 index 0000000000..b5be7d09c8 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/image/SessionImageCapacityTest.java @@ -0,0 +1,48 @@ +package nextstep.courses.domain.image; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class SessionImageCapacityTest { + + @Test + void 크기_1MB이하_정상생성() { + assertThatCode(() -> new SessionImageCapacity(1024 * 1024)) + .doesNotThrowAnyException(); + } + + @Test + void 크기_1MB초과_예외() { + assertThatThrownBy(() -> new SessionImageCapacity(1024 * 1024 + 1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지 크기는 1MB 이하여야 합니다."); + } + + @Test + void 크기_0이하_예외() { + assertThatThrownBy(() -> new SessionImageCapacity(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지 크기는 0보다 커야 합니다."); + } + + @Test + void KB단위_생성() { + assertThatCode(() -> SessionImageCapacity.ofKB(500)) + .doesNotThrowAnyException(); + } + + @Test + void MB단위_1MB_정상생성() { + assertThatCode(() -> SessionImageCapacity.ofMB(1)) + .doesNotThrowAnyException(); + } + + @Test + void MB단위_2MB_예외() { + assertThatThrownBy(() -> SessionImageCapacity.ofMB(2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지 크기는 1MB 이하여야 합니다."); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/image/SessionImageDimensionTest.java b/src/test/java/nextstep/courses/domain/image/SessionImageDimensionTest.java new file mode 100644 index 0000000000..82328a6598 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/image/SessionImageDimensionTest.java @@ -0,0 +1,40 @@ +package nextstep.courses.domain.image; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SessionImageDimensionTest { + + @ParameterizedTest + @CsvSource({"300,200"}) + void 너비는_300이상_높이는_200이상(int width, int height){ + assertDoesNotThrow(() -> new SessionImageDimension(width, height)); + } + + @ParameterizedTest + @CsvSource({"299,299", "299,300", "300,199"}) + void 그_외_너비_예외(int width, int height){ + assertThatThrownBy(() -> new SessionImageDimension(width, height)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지는 가로 300이상, 세로 200 이상이어야 합니다."); + } + + @ParameterizedTest + @CsvSource({"300,200","1200,800"}) + void 너비와_높이_비율은_3_대_2(int width, int height){ + assertDoesNotThrow(() -> new SessionImageDimension(width, height)); + + } + + @ParameterizedTest + @CsvSource({"400,200", "600,300", "900,400"}) + void 그_외_비율_예외(int width, int height){ + assertThatThrownBy(() -> new SessionImageDimension(width, height)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미지는 가로 x 세로 3대 2이어야 합니다."); + } + +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/SessionBuilder.java b/src/test/java/nextstep/courses/domain/session/SessionBuilder.java new file mode 100644 index 0000000000..76061345e8 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/SessionBuilder.java @@ -0,0 +1,65 @@ +package nextstep.courses.domain.session; + +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.image.SessionCoverImage; +import nextstep.courses.domain.session.type.FreeType; +import nextstep.courses.domain.session.type.SessionType; + +public class SessionBuilder { + private Long id = null; + private Course course = new Course("TDD", 1L); + private Term term = new Term(1); + private SessionCoverImage cover = new SessionCoverImage(300, 200, "png", 1024 * 500); + private SessionPeriod period = new SessionPeriod("2025-01-01", "2025-01-31"); + private SessionState state = SessionState.PREPARING; + private SessionType type = new FreeType(); + + public static SessionBuilder aSession() { + return new SessionBuilder(); + } + + public SessionBuilder withId(Long id) { + this.id = id; + return this; + } + + public SessionBuilder withCourse(Course course) { + this.course = course; + return this; + } + + public SessionBuilder withTerm(int term) { + this.term = new Term(term); + return this; + } + + public SessionBuilder withCover(SessionCoverImage cover) { + this.cover = cover; + return this; + } + + public SessionBuilder withPeriod(String startDay, String endDay) { + this.period = new SessionPeriod(startDay, endDay); + return this; + } + + public SessionBuilder withState(SessionState state) { + this.state = state; + return this; + } + + public SessionBuilder withType(SessionType type) { + this.type = type; + return this; + } + + public SessionBuilder recruiting() { + this.state = SessionState.RECRUITING; + return this; + } + + public Session build() { + Enrollment enrollment = new Enrollment(state, type); + return new Session(id, course, term, cover, period, enrollment); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/SessionPeriodTest.java b/src/test/java/nextstep/courses/domain/session/SessionPeriodTest.java new file mode 100644 index 0000000000..9e27524fbc --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/SessionPeriodTest.java @@ -0,0 +1,38 @@ +package nextstep.courses.domain.session; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.time.LocalDate; +import org.junit.jupiter.api.Test; + +class SessionPeriodTest { + + @Test + void 시작일이_종료일보다_이전이면_정상_생성() { + assertThatCode(() -> new SessionPeriod("2024-01-01", "2024-01-31")) + .doesNotThrowAnyException(); + } + + @Test + void 시작일과_종료일이_같으면_정상_생성() { + assertThatCode(() -> new SessionPeriod("2024-01-01", "2024-01-01")) + .doesNotThrowAnyException(); + } + + @Test + void 시작일이_종료일보다_이후면_예외() { + assertThatThrownBy(() -> new SessionPeriod("2024-01-31", "2024-01-01")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("시작일은 종료일보다 이전이어야 합니다."); + } + + @Test + void LocalDate로_생성() { + LocalDate start = LocalDate.of(2024, 1, 1); + LocalDate end = LocalDate.of(2024, 1, 31); + + assertThatCode(() -> new SessionPeriod(start, end)) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/SessionStateTest.java b/src/test/java/nextstep/courses/domain/session/SessionStateTest.java new file mode 100644 index 0000000000..bbd4439a3a --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/SessionStateTest.java @@ -0,0 +1,48 @@ +package nextstep.courses.domain.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class SessionStateTest { + + @Test + void 준비중에서_모집시작_가능() { + assertThat(SessionState.PREPARING.open()).isEqualTo(SessionState.RECRUITING); + } + + @Test + void 모집중에서_종료_가능() { + assertThat(SessionState.RECRUITING.close()).isEqualTo(SessionState.CLOSED); + } + + @Test + void 준비중이_아닐때_모집시작하면_예외() { + assertThatThrownBy(() -> SessionState.RECRUITING.open()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("준비중인 강의만 모집을 시작할 수 있습니다."); + + assertThatThrownBy(() -> SessionState.CLOSED.open()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("준비중인 강의만 모집을 시작할 수 있습니다."); + } + + @Test + void 모집중이_아닐때_종료하면_예외() { + assertThatThrownBy(() -> SessionState.PREPARING.close()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("모집중인 강의만 종료할 수 있습니다."); + + assertThatThrownBy(() -> SessionState.CLOSED.close()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("모집중인 강의만 종료할 수 있습니다."); + } + + @Test + void 모집중일때만_수강신청_가능() { + assertThat(SessionState.PREPARING.canEnroll()).isFalse(); + assertThat(SessionState.RECRUITING.canEnroll()).isTrue(); + assertThat(SessionState.CLOSED.canEnroll()).isFalse(); + } +} \ 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 0000000000..21513ea3c1 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/SessionTest.java @@ -0,0 +1,44 @@ +package nextstep.courses.domain.session; + +import static nextstep.courses.domain.session.SessionBuilder.aSession; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class SessionTest { + + @Test + void 모집중일때_수강신청_가능() { + Session session = aSession().recruiting().build(); + + assertDoesNotThrow(() -> session.enroll(0)); + } + + @Test + void 모집중이_아닐때_수강신청하면_예외() { + Session session = aSession().build(); + + assertThatThrownBy(() -> session.enroll(0)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("모집중인 강의만 수강신청이 가능합니다."); + } + + @Test + void 준비중이_아닐때_모집시작하면_예외() { + Session session = aSession().recruiting().build(); + + assertThatThrownBy(() -> session.open()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("준비중인 강의만 모집을 시작할 수 있습니다."); + } + + @Test + void 모집중이_아닐때_종료하면_예외() { + Session session = aSession().build(); + + assertThatThrownBy(() -> session.close()) + .isInstanceOf(IllegalStateException.class) + .hasMessage("모집중인 강의만 종료할 수 있습니다."); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/type/FreeTypeTest.java b/src/test/java/nextstep/courses/domain/session/type/FreeTypeTest.java new file mode 100644 index 0000000000..bc01653de8 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/type/FreeTypeTest.java @@ -0,0 +1,34 @@ +package nextstep.courses.domain.session.type; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import nextstep.courses.domain.session.type.FreeType; +import nextstep.courses.domain.session.type.SessionType; +import org.junit.jupiter.api.Test; + +class FreeTypeTest { + + @Test + void 수강인원_제한없음() { + SessionType type = new FreeType(); + + for (int i = 0; i < 1000; i++) { + type = type.enroll(0); + } + + assertThat(type).isInstanceOf(FreeType.class); + } + + @Test + void 금액_상관없이_수강가능() { + SessionType type = new FreeType(); + + assertThatCode(() -> type.enroll(0)) + .doesNotThrowAnyException(); + assertThatCode(() -> type.enroll(100)) + .doesNotThrowAnyException(); + assertThatCode(() -> type.enroll(999999)) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/session/type/PaidTypeTest.java b/src/test/java/nextstep/courses/domain/session/type/PaidTypeTest.java new file mode 100644 index 0000000000..da71011700 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/session/type/PaidTypeTest.java @@ -0,0 +1,36 @@ +package nextstep.courses.domain.session.type; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.courses.domain.session.type.PaidType; +import org.junit.jupiter.api.Test; + +class PaidTypeTest { + + @Test + void 최대수강인원_초과하면_예외() { + PaidType type = new PaidType(300, 1000, 300); + + assertThatThrownBy(() -> type.enroll(1000)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("최대 수강 인원을 초과할 수 없습니다."); + } + + @Test + void 결제금액과_수강료가_동일하지_않으면_예외() { + PaidType type = new PaidType(300, 1000); + + assertThatThrownBy(() -> type.enroll(999)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("수강료와 지불한 금액이 정확히 일치해야 합니다."); + } + + @Test + void 최대수강인원이하_결제금액과수강료동일하면_성공() { + PaidType type = new PaidType(300, 1000); + + assertThatCode(() -> type.enroll(1000)) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java index f087fc0ad2..530375d3e1 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;