diff --git a/README.md b/README.md index dfc62046f..f8c2a9da3 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,94 @@ # 학습 관리 시스템(Learning Management System) + ## 진행 방법 + * 학습 관리 시스템의 수강신청 요구사항을 파악한다. * 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. * 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. * 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 + * [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) +## LMS STEP 1 기능 목록 -## LMS 기능 목록 * `Question` * [x] : 로그인 사용자, 질문자와 모든 답변자들이 같으면 답변을 삭제 상태로 바꿀 수 있다. - * [x] : 질문이 삭제되면 삭제 히스토리에게 질문 삭제 요청을 보낸다. - * [x] : 질문이 삭제되면 삭제 히스토리에게 질문에 대한 댓글들의 삭제 요청을 보낸다. + * [x] : 질문이 삭제되면 삭제 히스토리에게 질문 삭제 요청을 보낸다. + * [x] : 질문이 삭제되면 삭제 히스토리에게 질문에 대한 댓글들의 삭제 요청을 보낸다. * [x] : 삭제할 수 없다면 예외를 던진다. - * [x] : 사용자와 질문자가 달라 삭제할 수 없다면 예외를 던진다. - * [x] : 질문자와 답변자들이 달라 삭제할 수 없다면 예외를 던진다. + * [x] : 사용자와 질문자가 달라 삭제할 수 없다면 예외를 던진다. + * [x] : 질문자와 답변자들이 달라 삭제할 수 없다면 예외를 던진다. * `Answer` * [x] : 질문자와 답변자가 같은지 다른지 확인한다. * [x] : 질문이 삭제되면, 댓글도 삭제한다. + +## 수강 신청 기능 요구사항 + +* 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다. +* 강의는 시작일과 종료일을 가진다. +* 강의는 강의 커버 이미지 정보를 가진다. +* 이미지 크기는 1MB 이하여야 한다. +* 이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다. +* 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다. +* 강의는 무료 강의와 유료 강의로 나뉜다. +* 무료 강의는 최대 수강 인원 제한이 없다. +* 유료 강의는 강의 최대 수강 인원을 초과할 수 없다. +* 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다. +* 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다. +* 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. +* 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. +* 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다. + +## LMS STEP 2 기능 목록 + +* `Course` : 과정 + * [] : + +* `Session` : 강의 + * [x] : 강의 타입에 따라 수강생을 등록한다. + +* `SessionParticipants` : 강의 수강 인원 정보 + * [x] : 강의에 사람이 다 찼는지 확인한다. + +* `SessionStatus` : 강의 상태 + * [x] : 강의를 수강할 수 있는지 알려준다. + +* `SessionType` : 강의 타입 + * [x] : 강의 타입이 유료인지 무료인지 확인한다. + +* `SessionPeriod` : 강의 기간 + * [x] : 강의가 시작했는지 확인한다. + +* `Image` : 이미지 + * [] : + +* `ImageSize` : 이미지 사이즈 + * [x] : 이미지의 width는 300픽셀 이상인지 검증한다. + * [x] : 이미지의 height는 200픽셀 이상인지 검증한다. + * [x] : width와 height의 비율은 3:2 인지 검증한다. + +* `ImageType` : 이미지 타입 + * [x] : 사용할 수 있는 이미지 타입인지 검증한다. + +* `ImageCapacityType` : 이미지 용량 타입 + * [x] : 이미지 용량 이름으로 이미지 용량 타입을 찾는다. + +* `ImageCapacity` : 이미지 용량 + * [x] : 이미지 용량이 1MB 이하인지 검증한다. + +* `Enrollment` : 등록 + * [x] : 유료 강의 수강 신청을 한다. + * [x] : 무료 강의 수강 신청을 한다. + * [x] : 유료 강의의 경우, 수강 인원이 모두 채워지면 예외를 던진다. + +* `Payments` : 결제 정보들 + * [] : + +* `SessionPayment` : 결제 정보 + * [] : + + + diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/Course.java index 0f6971604..535730c1b 100644 --- a/src/main/java/nextstep/courses/domain/Course.java +++ b/src/main/java/nextstep/courses/domain/Course.java @@ -1,6 +1,10 @@ package nextstep.courses.domain; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import nextstep.session.domain.Session; public class Course { private Long id; @@ -13,6 +17,8 @@ public class Course { private LocalDateTime updatedAt; + private List sessions = new ArrayList<>(); + public Course() { } @@ -20,6 +26,10 @@ public Course(String title, Long creatorId) { this(0L, title, creatorId, LocalDateTime.now(), null); } + public Course(Long id, String title, Long creatorId) { + this(id, title, creatorId, LocalDateTime.now(), null); + } + public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.title = title; @@ -28,6 +38,14 @@ public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, Lo this.updatedAt = updatedAt; } + public void addSession(Session session) { + sessions.add(session); + } + + public Long getId() { + return id; + } + public String getTitle() { return title; } @@ -40,6 +58,28 @@ public LocalDateTime getCreatedAt() { return createdAt; } + public List getSessions() { + return sessions; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Course course = (Course) o; + return Objects.equals(title, course.title) && Objects.equals(creatorId, course.creatorId) + && Objects.equals(sessions, course.sessions); + } + + @Override + public int hashCode() { + return Objects.hash(title, creatorId, sessions); + } + @Override public String toString() { return "Course{" + diff --git a/src/main/java/nextstep/courses/dto/CreateCourseRequest.java b/src/main/java/nextstep/courses/dto/CreateCourseRequest.java new file mode 100644 index 000000000..df33c6422 --- /dev/null +++ b/src/main/java/nextstep/courses/dto/CreateCourseRequest.java @@ -0,0 +1,13 @@ +package nextstep.courses.dto; + +import nextstep.courses.domain.Course; + +public class CreateCourseRequest { + + private String title; + private Long creatorId; + + public Course toEntity() { + return new Course(title, creatorId); + } +} diff --git a/src/main/java/nextstep/courses/infrastructure/MockCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/MockCourseRepository.java new file mode 100644 index 000000000..ec95b153c --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/MockCourseRepository.java @@ -0,0 +1,31 @@ +package nextstep.courses.infrastructure; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import nextstep.courses.domain.Course; +import nextstep.courses.domain.CourseRepository; + +public class MockCourseRepository implements CourseRepository { + + private Map courseStorage = new LinkedHashMap<>(); + + public MockCourseRepository() { + this.courseStorage.put(1, new Course(1L, "코스", 1L)); + } + + @Override + public int save(Course course) { + long courseId = Optional.ofNullable(course.getId()) + .orElseGet(() -> (long) (courseStorage.size() + 1)); + int intCourseId = Math.toIntExact(courseId); + courseStorage.put(intCourseId, course); + return intCourseId; + } + + @Override + public Course findById(Long id) { + return Optional.ofNullable(courseStorage.get(Integer.parseInt(String.valueOf(id)))) + .orElseThrow(() -> new IllegalStateException("과정을 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/nextstep/courses/sevice/CourseService.java b/src/main/java/nextstep/courses/sevice/CourseService.java new file mode 100644 index 000000000..e736512ed --- /dev/null +++ b/src/main/java/nextstep/courses/sevice/CourseService.java @@ -0,0 +1,26 @@ +package nextstep.courses.sevice; + +import nextstep.courses.domain.Course; +import nextstep.courses.domain.CourseRepository; +import nextstep.courses.dto.CreateCourseRequest; + +public class CourseService { + + private final CourseRepository courseRepository; + + public CourseService(CourseRepository courseRepository) { + this.courseRepository = courseRepository; + } + + public int saveCourse(CreateCourseRequest request) { + return courseRepository.save(request.toEntity()); + } + + public void saveCourse(Course course) { + courseRepository.save(course); + } + + public Course findCourse(Long courseId) { + return courseRepository.findById(courseId); + } +} diff --git a/src/main/java/nextstep/image/domain/Image.java b/src/main/java/nextstep/image/domain/Image.java new file mode 100644 index 000000000..b52c30e87 --- /dev/null +++ b/src/main/java/nextstep/image/domain/Image.java @@ -0,0 +1,36 @@ +package nextstep.image.domain; + +import java.util.Objects; + +public class Image { + + private ImageCapacity imageCapacity; + + private ImageType imageType; + + private ImageSize imageSize; + + public Image(ImageCapacity imageCapacity, ImageType imageType, ImageSize imageSize) { + this.imageCapacity = imageCapacity; + this.imageType = imageType; + this.imageSize = imageSize; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Image image = (Image) o; + return Objects.equals(imageCapacity, image.imageCapacity) && imageType == image.imageType + && Objects.equals(imageSize, image.imageSize); + } + + @Override + public int hashCode() { + return Objects.hash(imageCapacity, imageType, imageSize); + } +} diff --git a/src/main/java/nextstep/image/domain/ImageCapacity.java b/src/main/java/nextstep/image/domain/ImageCapacity.java new file mode 100644 index 000000000..19f2a2dac --- /dev/null +++ b/src/main/java/nextstep/image/domain/ImageCapacity.java @@ -0,0 +1,62 @@ +package nextstep.image.domain; + +import java.util.Objects; +import nextstep.image.exception.OutOfRangeCapacityException; +import nextstep.image.exception.OutOfRangeCapacityTypeException; + +public class ImageCapacity { + + public static final String OUT_OF_RANGE_CAPACITY_EXCEPTION = "이미지 용량이 1MB를 넘습니다."; + public static final String OUT_OF_RANGE_CAPACITY_TYPE_EXCEPTION = "이미지 용량 타입이 MB를 넘습니다."; + + private long value; + private ImageCapacityType imageCapacityType; + + public ImageCapacity(long value, ImageCapacityType imageCapacityType) { + validateImageCapacity(value, imageCapacityType); + this.value = value; + this.imageCapacityType = imageCapacityType; + } + + public ImageCapacity(long value, String capacityTypeName) { + this(value, ImageCapacityType.findByName(capacityTypeName)); + } + + private static void validateImageCapacity(long value, ImageCapacityType imageCapacityType) { + validateCapacityTypeRange(imageCapacityType); + validateCapacityRange(value, imageCapacityType); + } + + private static void validateCapacityTypeRange(ImageCapacityType imageCapacityType) { + if (imageCapacityType.isGreaterThanMB()) { + throw new OutOfRangeCapacityTypeException(OUT_OF_RANGE_CAPACITY_TYPE_EXCEPTION); + } + } + + private static void validateCapacityRange(long value, ImageCapacityType imageCapacityType) { + if (outOfRange(value, imageCapacityType)) { + throw new OutOfRangeCapacityException(OUT_OF_RANGE_CAPACITY_EXCEPTION); + } + } + + private static boolean outOfRange(long value, ImageCapacityType imageCapacityType) { + return imageCapacityType.isLessThanMB() && value > 1; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ImageCapacity that = (ImageCapacity) o; + return value == that.value && imageCapacityType == that.imageCapacityType; + } + + @Override + public int hashCode() { + return Objects.hash(value, imageCapacityType); + } +} diff --git a/src/main/java/nextstep/image/domain/ImageCapacityType.java b/src/main/java/nextstep/image/domain/ImageCapacityType.java new file mode 100644 index 000000000..deb21b13f --- /dev/null +++ b/src/main/java/nextstep/image/domain/ImageCapacityType.java @@ -0,0 +1,34 @@ +package nextstep.image.domain; + +import java.util.Arrays; +import nextstep.image.exception.CannotFindImageCapacityTypeException; + +public enum ImageCapacityType { + + KB("kb"), + MB("mb"), + GB("gb"); + + public static final String CANNOT_FIND_IMAGE_CAPACITY_TYPE_EXCEPTION = "이미지 용량 타입을 찾을 수 없습니다."; + + private final String name; + + ImageCapacityType(String name) { + this.name = name; + } + + public static ImageCapacityType findByName(String name) { + return Arrays.stream(values()) + .filter(imageCapacityType -> imageCapacityType.name.equals(name)) + .findFirst() + .orElseThrow(() -> new CannotFindImageCapacityTypeException(CANNOT_FIND_IMAGE_CAPACITY_TYPE_EXCEPTION)); + } + + public boolean isLessThanMB() { + return !this.equals(KB); + } + + public boolean isGreaterThanMB() { + return this.equals(GB); + } +} diff --git a/src/main/java/nextstep/image/domain/ImageSize.java b/src/main/java/nextstep/image/domain/ImageSize.java new file mode 100644 index 000000000..f34db543c --- /dev/null +++ b/src/main/java/nextstep/image/domain/ImageSize.java @@ -0,0 +1,63 @@ +package nextstep.image.domain; + +import java.util.Objects; +import nextstep.image.exception.HeightValidationException; +import nextstep.image.exception.RatioValidationException; +import nextstep.image.exception.WidthValidationException; + +public class ImageSize { + + public static final String WIDTH_VALIDATION_EXCEPTION = "이미지의 너비는 300픽셀 이상이어야 합니다."; + public static final String HEIGHT_VALIATION_EXCEPTION = "이미지의 높이는 300픽셀 이상이어야 합니다."; + public static final String RATIO_VALIDATION_EXCEPTION = "너비 대 높이 비는 3 : 2 이어야 합니다."; + + private double width; + private double height; + + public ImageSize() { + + } + + public ImageSize(double width, double height) { + validateWidth(width); + validateHeight(height); + validateRatio(width, height); + this.width = width; + this.height = height; + } + + private void validateWidth(double width) { + if (width < 300) { + throw new WidthValidationException(WIDTH_VALIDATION_EXCEPTION); + } + } + + private void validateHeight(double height) { + if (height < 200) { + throw new HeightValidationException(HEIGHT_VALIATION_EXCEPTION); + } + } + + private void validateRatio(double width, double height) { + if (width / height != (double) 3 / 2) { + throw new RatioValidationException(RATIO_VALIDATION_EXCEPTION); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ImageSize imageSize = (ImageSize) o; + return width == imageSize.width && height == imageSize.height; + } + + @Override + public int hashCode() { + return Objects.hash(width, height); + } +} diff --git a/src/main/java/nextstep/image/domain/ImageType.java b/src/main/java/nextstep/image/domain/ImageType.java new file mode 100644 index 000000000..8dac53672 --- /dev/null +++ b/src/main/java/nextstep/image/domain/ImageType.java @@ -0,0 +1,28 @@ +package nextstep.image.domain; + +import java.util.Arrays; +import nextstep.image.exception.CannotFindImageTypeException; + +public enum ImageType { + + GIF("gif"), + JPG("jpg"), + JPEG("jpeg"), + PNG("png"), + SVG("svg"); + + public static final String CANNOT_FIND_IMAGE_TYPE_EXCEPTION = "해당 이미지 타입을 찾을 수 없습니다."; + + private final String name; + + ImageType(String name) { + this.name = name; + } + + public static ImageType findByName(String name) { + return Arrays.stream(values()) + .filter(imageType -> imageType.name.equals(name)) + .findFirst() + .orElseThrow(() -> new CannotFindImageTypeException(CANNOT_FIND_IMAGE_TYPE_EXCEPTION)); + } +} diff --git a/src/main/java/nextstep/image/dto/CreateImageRequest.java b/src/main/java/nextstep/image/dto/CreateImageRequest.java new file mode 100644 index 000000000..3a30a1b4e --- /dev/null +++ b/src/main/java/nextstep/image/dto/CreateImageRequest.java @@ -0,0 +1,23 @@ +package nextstep.image.dto; + +import nextstep.image.domain.Image; +import nextstep.image.domain.ImageCapacity; +import nextstep.image.domain.ImageSize; +import nextstep.image.domain.ImageType; + +public class CreateImageRequest { + + private long value; + private String capacityTypeName; + private ImageType imageType; + private long width; + private long height; + + public Image toEntity() { + return new Image( + new ImageCapacity(value, capacityTypeName), + imageType, + new ImageSize(width, height) + ); + } +} diff --git a/src/main/java/nextstep/image/exception/CannotFindImageCapacityTypeException.java b/src/main/java/nextstep/image/exception/CannotFindImageCapacityTypeException.java new file mode 100644 index 000000000..13f5e13f8 --- /dev/null +++ b/src/main/java/nextstep/image/exception/CannotFindImageCapacityTypeException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class CannotFindImageCapacityTypeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public CannotFindImageCapacityTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/image/exception/CannotFindImageTypeException.java b/src/main/java/nextstep/image/exception/CannotFindImageTypeException.java new file mode 100644 index 000000000..ef3e884b1 --- /dev/null +++ b/src/main/java/nextstep/image/exception/CannotFindImageTypeException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class CannotFindImageTypeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public CannotFindImageTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/image/exception/HeightValidationException.java b/src/main/java/nextstep/image/exception/HeightValidationException.java new file mode 100644 index 000000000..c0fe6e5eb --- /dev/null +++ b/src/main/java/nextstep/image/exception/HeightValidationException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class HeightValidationException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public HeightValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/image/exception/OutOfRangeCapacityException.java b/src/main/java/nextstep/image/exception/OutOfRangeCapacityException.java new file mode 100644 index 000000000..40739eece --- /dev/null +++ b/src/main/java/nextstep/image/exception/OutOfRangeCapacityException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class OutOfRangeCapacityException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public OutOfRangeCapacityException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/image/exception/OutOfRangeCapacityTypeException.java b/src/main/java/nextstep/image/exception/OutOfRangeCapacityTypeException.java new file mode 100644 index 000000000..3c3e54230 --- /dev/null +++ b/src/main/java/nextstep/image/exception/OutOfRangeCapacityTypeException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class OutOfRangeCapacityTypeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public OutOfRangeCapacityTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/image/exception/RatioValidationException.java b/src/main/java/nextstep/image/exception/RatioValidationException.java new file mode 100644 index 000000000..d4adf6106 --- /dev/null +++ b/src/main/java/nextstep/image/exception/RatioValidationException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class RatioValidationException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public RatioValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/image/exception/WidthValidationException.java b/src/main/java/nextstep/image/exception/WidthValidationException.java new file mode 100644 index 000000000..672ec308d --- /dev/null +++ b/src/main/java/nextstep/image/exception/WidthValidationException.java @@ -0,0 +1,9 @@ +package nextstep.image.exception; + +public class WidthValidationException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public WidthValidationException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index a4c8e6a06..48551ed0f 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -50,11 +50,6 @@ public boolean isDeleted() { return deleted; } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } - public boolean isOwner(NsUser writer) { return this.writer.equals(writer); } @@ -63,10 +58,6 @@ public NsUser getWriter() { return writer; } - public String getContents() { - return contents; - } - public void toQuestion(Question question) { this.question = question; } @@ -81,7 +72,7 @@ private void deleteAnswer() { } private DeleteHistory createDeleteAnswerHistory() { - return new DeleteHistory(ContentType.ANSWER, this.id, this.writer, LocalDateTime.now()); + return DeleteHistory.createDeleteHistoryByAnswer(this.id, this.writer, LocalDateTime.now()); } @Override diff --git a/src/main/java/nextstep/qna/domain/DeleteHistory.java b/src/main/java/nextstep/qna/domain/DeleteHistory.java index d13d4a6db..a871c9b52 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistory.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistory.java @@ -25,6 +25,26 @@ public DeleteHistory(ContentType contentType, Long contentId, NsUser deletedBy, this.createdDate = createdDate; } + public static DeleteHistory createDeleteHistoryByQuestion(Long contentId, NsUser deletedBy, + LocalDateTime createdDate) { + DeleteHistory deleteHistory = new DeleteHistory(); + deleteHistory.contentType = ContentType.QUESTION; + deleteHistory.contentId = contentId; + deleteHistory.deletedBy = deletedBy; + deleteHistory.createdDate = createdDate; + return deleteHistory; + } + + public static DeleteHistory createDeleteHistoryByAnswer(Long contentId, NsUser deletedBy, + LocalDateTime createdDate) { + DeleteHistory deleteHistory = new DeleteHistory(); + deleteHistory.contentType = ContentType.ANSWER; + deleteHistory.contentId = contentId; + deleteHistory.deletedBy = deletedBy; + deleteHistory.createdDate = createdDate; + return deleteHistory; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index 08480fd28..99c4155b9 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -41,24 +41,6 @@ public Long getId() { return id; } - public String getTitle() { - return title; - } - - public Question setTitle(String title) { - this.title = title; - return this; - } - - public String getContents() { - return contents; - } - - public Question setContents(String contents) { - this.contents = contents; - return this; - } - public NsUser getWriter() { return writer; } @@ -100,7 +82,7 @@ private void deleteQuestion() { } private DeleteHistory createDeleteQuestionHistory() { - return new DeleteHistory(ContentType.QUESTION, this.id, this.writer, LocalDateTime.now()); + return DeleteHistory.createDeleteHistoryByQuestion(this.id, this.writer, LocalDateTime.now()); } private List writeDeleteAnswersHistory() throws CannotDeleteException { diff --git a/src/main/java/nextstep/session/domain/CurrentPeriodStrategy.java b/src/main/java/nextstep/session/domain/CurrentPeriodStrategy.java new file mode 100644 index 000000000..ed4b4cbe5 --- /dev/null +++ b/src/main/java/nextstep/session/domain/CurrentPeriodStrategy.java @@ -0,0 +1,10 @@ +package nextstep.session.domain; + +import java.time.LocalDate; + +public class CurrentPeriodStrategy implements PeriodStrategy { + @Override + public LocalDate getLocalDate() { + return LocalDate.now(); + } +} diff --git a/src/main/java/nextstep/session/domain/Enrollment.java b/src/main/java/nextstep/session/domain/Enrollment.java new file mode 100644 index 000000000..1325245ff --- /dev/null +++ b/src/main/java/nextstep/session/domain/Enrollment.java @@ -0,0 +1,60 @@ +package nextstep.session.domain; + +import static nextstep.session.domain.PaymentType.COMPLETED; + +import java.util.ArrayList; +import java.util.List; +import nextstep.session.exception.SessionFullOfParticipantsException; +import nextstep.users.domain.NsUser; + +public class Enrollment { + + public static final String SESSION_FULL_OF_PARTICIPANTS_EXCEPTION = "강의 수강 인원이 모두 채워져 수강 신청을 할 수 없습니다."; + + private SessionParticipants participants = new SessionParticipants(); + + private List payments = new ArrayList<>(); + + private long price; + + public Enrollment(SessionParticipants participants, List payments, long price) { + this.participants = participants; + this.payments = payments; + this.price = price; + } + + public void enrollPaidSession(long fee, NsUser user) { + validateSessionIsFull(); + paymentSession(fee, user); + } + + private void validateSessionIsFull() { + if (participants.isFull()) { + throw new SessionFullOfParticipantsException(SESSION_FULL_OF_PARTICIPANTS_EXCEPTION); + } + } + + private void paymentSession(long fee, NsUser user) { + if (this.price == fee) { + enrollParticipant(user); + } + } + + private void enrollParticipant(NsUser user) { + this.participants.enroll(user); + this.payments = new ArrayList<>(payments); + this.payments.add(new SessionPayment(user, COMPLETED)); + } + + public void enrollFreeSession(NsUser user) { + enrollParticipant(user); + } + + public SessionParticipants getParticipants() { + return participants; + } + + public List getPayments() { + return payments; + } +} diff --git a/src/main/java/nextstep/session/domain/PaymentType.java b/src/main/java/nextstep/session/domain/PaymentType.java new file mode 100644 index 000000000..5e391524d --- /dev/null +++ b/src/main/java/nextstep/session/domain/PaymentType.java @@ -0,0 +1,6 @@ +package nextstep.session.domain; + +public enum PaymentType { + + COMPLETED, REFUND +} diff --git a/src/main/java/nextstep/session/domain/PeriodStrategy.java b/src/main/java/nextstep/session/domain/PeriodStrategy.java new file mode 100644 index 000000000..008de8094 --- /dev/null +++ b/src/main/java/nextstep/session/domain/PeriodStrategy.java @@ -0,0 +1,8 @@ +package nextstep.session.domain; + +import java.time.LocalDate; + +public interface PeriodStrategy { + + LocalDate getLocalDate(); +} diff --git a/src/main/java/nextstep/session/domain/Session.java b/src/main/java/nextstep/session/domain/Session.java new file mode 100644 index 000000000..193e4ef19 --- /dev/null +++ b/src/main/java/nextstep/session/domain/Session.java @@ -0,0 +1,101 @@ +package nextstep.session.domain; + +import java.util.Objects; +import nextstep.image.domain.Image; +import nextstep.session.exception.SessionOutOfDateException; +import nextstep.users.domain.NsUser; + +public class Session { + + public static final String SESSION_OUT_OF_DATE_EXCEPTION = "강의가 시작되어 수강 신청할 수 없습니다."; + + private Long sessionId; + + private SessionPeriod sessionPeriod; + + private Image image; + + private SessionType type; + + private SessionStatus status; + + private Enrollment enrollment; + + private long price; + + public Session() { + } + + public Session(SessionPeriod sessionPeriod, Image image, SessionType type, SessionStatus status, long price) { + this.sessionPeriod = sessionPeriod; + this.image = image; + this.type = type; + this.status = status; + this.price = price; + } + + public Session(SessionPeriod sessionPeriod, Image image, SessionType type, SessionStatus status, + Enrollment enrollment, + long price) { + this.sessionPeriod = sessionPeriod; + this.image = image; + this.type = type; + this.status = status; + this.enrollment = enrollment; + this.price = price; + } + + public void enrollSession(long fee, NsUser user) { + checkSessionIsStart(); + if (type.isPaid()) { + enrollment.enrollPaidSession(fee, user); + return; + } + enrollment.enrollFreeSession(user); + } + + private void checkSessionIsStart() { + if (sessionPeriod.isStart(new CurrentPeriodStrategy())) { + throw new SessionOutOfDateException(SESSION_OUT_OF_DATE_EXCEPTION); + } + } + + public Image getImage() { + return image; + } + + public Enrollment getEnrollment() { + return enrollment; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Session session = (Session) o; + return price == session.price && Objects.equals(image, session.image) && type == session.type + && status == session.status && Objects.equals(enrollment, session.enrollment); + } + + @Override + public int hashCode() { + return Objects.hash(image, type, status, enrollment, price); + } + + @Override + public String toString() { + return "Session{" + + "sessionId=" + sessionId + + ", sessionPeriod=" + sessionPeriod + + ", image=" + image + + ", type=" + type + + ", status=" + status + + ", enrollment=" + enrollment + + ", price=" + price + + '}'; + } +} diff --git a/src/main/java/nextstep/session/domain/SessionParticipants.java b/src/main/java/nextstep/session/domain/SessionParticipants.java new file mode 100644 index 000000000..afa459d3a --- /dev/null +++ b/src/main/java/nextstep/session/domain/SessionParticipants.java @@ -0,0 +1,62 @@ +package nextstep.session.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import nextstep.users.domain.NsUser; + +public class SessionParticipants { + + private List participants = new ArrayList<>(); + + private long maxNumberOfParticipants; + + public SessionParticipants() { + } + + public SessionParticipants(List participants, long maxNumberOfParticipants) { + this.participants = participants; + this.maxNumberOfParticipants = maxNumberOfParticipants; + } + + public void enroll(NsUser user) { + if (!isFull()) { + this.participants = new ArrayList<>(participants); + this.participants.add(user); + } + } + + public boolean isFull() { + return maxNumberOfParticipants - participants.size() <= 0; + } + + public List getParticipants() { + return participants; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SessionParticipants that = (SessionParticipants) o; + return maxNumberOfParticipants == that.maxNumberOfParticipants && Objects.equals(participants, + that.participants); + } + + @Override + public int hashCode() { + return Objects.hash(participants, maxNumberOfParticipants); + } + + @Override + public String toString() { + return "SessionParticipants{" + + "participants=" + participants + + ", maxNumberOfParticipants=" + maxNumberOfParticipants + + '}'; + } +} diff --git a/src/main/java/nextstep/session/domain/SessionPayment.java b/src/main/java/nextstep/session/domain/SessionPayment.java new file mode 100644 index 000000000..28a510c23 --- /dev/null +++ b/src/main/java/nextstep/session/domain/SessionPayment.java @@ -0,0 +1,44 @@ +package nextstep.session.domain; + +import java.util.Objects; +import nextstep.users.domain.NsUser; + +public class SessionPayment { + + private NsUser participant; + + private PaymentType paymentType; + + public SessionPayment() { + } + + public SessionPayment(NsUser user, PaymentType paymentType) { + this.participant = user; + this.paymentType = paymentType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SessionPayment that = (SessionPayment) o; + return Objects.equals(participant, that.participant) && paymentType == that.paymentType; + } + + @Override + public int hashCode() { + return Objects.hash(participant, paymentType); + } + + @Override + public String toString() { + return "SessionPayment{" + + "participant=" + participant + + ", paymentType=" + paymentType + + '}'; + } +} diff --git a/src/main/java/nextstep/session/domain/SessionPeriod.java b/src/main/java/nextstep/session/domain/SessionPeriod.java new file mode 100644 index 000000000..ea923ccef --- /dev/null +++ b/src/main/java/nextstep/session/domain/SessionPeriod.java @@ -0,0 +1,36 @@ +package nextstep.session.domain; + +import java.time.LocalDate; +import java.util.Objects; + +public class SessionPeriod { + private final LocalDate startDate; + private final LocalDate endDate; + + public SessionPeriod(LocalDate startDate, LocalDate endDate) { + this.startDate = startDate; + this.endDate = endDate; + } + + public boolean isStart(PeriodStrategy periodStrategy) { + LocalDate currentDate = periodStrategy.getLocalDate(); + return currentDate.isEqual(startDate) || currentDate.isAfter(startDate); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SessionPeriod that = (SessionPeriod) o; + return Objects.equals(startDate, that.startDate) && Objects.equals(endDate, that.endDate); + } + + @Override + public int hashCode() { + return Objects.hash(startDate, endDate); + } +} diff --git a/src/main/java/nextstep/session/domain/SessionStatus.java b/src/main/java/nextstep/session/domain/SessionStatus.java new file mode 100644 index 000000000..8b3ff99e0 --- /dev/null +++ b/src/main/java/nextstep/session/domain/SessionStatus.java @@ -0,0 +1,10 @@ +package nextstep.session.domain; + +public enum SessionStatus { + + PREPARING, RECRUITING, END; + + public boolean checkTakingLecture() { + return this.equals(RECRUITING); + } +} diff --git a/src/main/java/nextstep/session/domain/SessionType.java b/src/main/java/nextstep/session/domain/SessionType.java new file mode 100644 index 000000000..f838d8676 --- /dev/null +++ b/src/main/java/nextstep/session/domain/SessionType.java @@ -0,0 +1,10 @@ +package nextstep.session.domain; + +public enum SessionType { + + PAID, FREE; + + public boolean isPaid() { + return this.equals(PAID); + } +} diff --git a/src/main/java/nextstep/session/dto/CreateSessionRequest.java b/src/main/java/nextstep/session/dto/CreateSessionRequest.java new file mode 100644 index 000000000..d54904791 --- /dev/null +++ b/src/main/java/nextstep/session/dto/CreateSessionRequest.java @@ -0,0 +1,69 @@ +package nextstep.session.dto; + +import java.time.LocalDate; +import nextstep.image.domain.Image; +import nextstep.image.domain.ImageCapacity; +import nextstep.image.domain.ImageCapacityType; +import nextstep.image.domain.ImageSize; +import nextstep.image.domain.ImageType; +import nextstep.session.domain.Session; +import nextstep.session.domain.SessionPeriod; +import nextstep.session.domain.SessionStatus; +import nextstep.session.domain.SessionType; + +public class CreateSessionRequest { + + private Long courseId; + + private LocalDate startDate; + + private LocalDate endDate; + + private long value; + + private ImageCapacityType imageCapacityType; + + private ImageType imageType; + + private long width; + private long height; + + private SessionType type; + + private SessionStatus status; + + private long price; + + public CreateSessionRequest(Long courseId, LocalDate startDate, LocalDate endDate, long value, + ImageCapacityType imageCapacityType, ImageType imageType, long width, long height, + SessionType type, SessionStatus status, long price) { + this.courseId = courseId; + this.startDate = startDate; + this.endDate = endDate; + this.value = value; + this.imageCapacityType = imageCapacityType; + this.imageType = imageType; + this.width = width; + this.height = height; + this.type = type; + this.status = status; + this.price = price; + } + + public Session toEntity() { + SessionPeriod sessionPeriod = new SessionPeriod(startDate, endDate); + ImageCapacity imageCapacity = new ImageCapacity(value, imageCapacityType); + ImageSize imageSize = new ImageSize(width, height); + return new Session( + sessionPeriod, + new Image(imageCapacity, imageType, imageSize), + type, + status, + price + ); + } + + public Long getCourseId() { + return courseId; + } +} diff --git a/src/main/java/nextstep/session/dto/EnrollSessionRequest.java b/src/main/java/nextstep/session/dto/EnrollSessionRequest.java new file mode 100644 index 000000000..b6ea7adca --- /dev/null +++ b/src/main/java/nextstep/session/dto/EnrollSessionRequest.java @@ -0,0 +1,28 @@ +package nextstep.session.dto; + +public class EnrollSessionRequest { + + private Long sessionId; + + private long fee; + + private String userId; + + public EnrollSessionRequest(Long sessionId, long fee, String userId) { + this.sessionId = sessionId; + this.fee = fee; + this.userId = userId; + } + + public Long getSessionId() { + return sessionId; + } + + public long getFee() { + return fee; + } + + public String getUserId() { + return userId; + } +} diff --git a/src/main/java/nextstep/session/exception/SessionFullOfParticipantsException.java b/src/main/java/nextstep/session/exception/SessionFullOfParticipantsException.java new file mode 100644 index 000000000..c1fabaf22 --- /dev/null +++ b/src/main/java/nextstep/session/exception/SessionFullOfParticipantsException.java @@ -0,0 +1,9 @@ +package nextstep.session.exception; + +public class SessionFullOfParticipantsException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public SessionFullOfParticipantsException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/session/exception/SessionOutOfDateException.java b/src/main/java/nextstep/session/exception/SessionOutOfDateException.java new file mode 100644 index 000000000..b8f3a3e4a --- /dev/null +++ b/src/main/java/nextstep/session/exception/SessionOutOfDateException.java @@ -0,0 +1,9 @@ +package nextstep.session.exception; + +public class SessionOutOfDateException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public SessionOutOfDateException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/session/repository/MockSessionRepository.java b/src/main/java/nextstep/session/repository/MockSessionRepository.java new file mode 100644 index 000000000..3c8421732 --- /dev/null +++ b/src/main/java/nextstep/session/repository/MockSessionRepository.java @@ -0,0 +1,54 @@ +package nextstep.session.repository; + +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import nextstep.image.domain.Image; +import nextstep.image.domain.ImageCapacity; +import nextstep.image.domain.ImageSize; +import nextstep.image.domain.ImageType; +import nextstep.session.domain.Enrollment; +import nextstep.session.domain.PaymentType; +import nextstep.session.domain.Session; +import nextstep.session.domain.SessionParticipants; +import nextstep.session.domain.SessionPayment; +import nextstep.session.domain.SessionPeriod; +import nextstep.session.domain.SessionStatus; +import nextstep.session.domain.SessionType; +import nextstep.users.domain.NsUser; + +public class MockSessionRepository implements SessionRepository { + + private Map sessionStorage = new LinkedHashMap<>(); + + public MockSessionRepository() { + this.sessionStorage.put(1L, new Session( + new SessionPeriod(LocalDate.of(2023, 12, 20), LocalDate.of(2023, 12, 30)), + new Image(new ImageCapacity(10, "kb"), ImageType.JPEG, new ImageSize(600, 400)), + SessionType.PAID, + SessionStatus.RECRUITING, + new Enrollment(new SessionParticipants(List.of(new NsUser(0L, "tempId", "5678", "이수찬", "email")), 3), + List.of(new SessionPayment(new NsUser(0L, "tempId", "5678", "이수찬", "email"), + PaymentType.COMPLETED)), + 10000 + ), + 10000 + )); + } + + @Override + public Long save(Session session) { + long size = sessionStorage.size(); + long sessionId = size + 1; + sessionStorage.put(sessionId, session); + return sessionId; + } + + @Override + public Session findById(Long id) { + return Optional.ofNullable(sessionStorage.get(id)) + .orElseThrow(() -> new IllegalStateException("강의를 찾을 수 없습니다.")); + } +} diff --git a/src/main/java/nextstep/session/repository/SessionRepository.java b/src/main/java/nextstep/session/repository/SessionRepository.java new file mode 100644 index 000000000..63410e5b3 --- /dev/null +++ b/src/main/java/nextstep/session/repository/SessionRepository.java @@ -0,0 +1,10 @@ +package nextstep.session.repository; + +import nextstep.session.domain.Session; + +public interface SessionRepository { + + Long save(Session session); + + Session findById(Long id); +} diff --git a/src/main/java/nextstep/session/service/SessionService.java b/src/main/java/nextstep/session/service/SessionService.java new file mode 100644 index 000000000..74e2fc814 --- /dev/null +++ b/src/main/java/nextstep/session/service/SessionService.java @@ -0,0 +1,42 @@ +package nextstep.session.service; + +import nextstep.courses.domain.Course; +import nextstep.courses.sevice.CourseService; +import nextstep.session.domain.Session; +import nextstep.session.dto.CreateSessionRequest; +import nextstep.session.dto.EnrollSessionRequest; +import nextstep.session.repository.SessionRepository; +import nextstep.users.domain.NsUser; +import nextstep.users.service.UserService; + +public class SessionService { + + private final SessionRepository sessionRepository; + private final UserService userService; + private final CourseService courseService; + + public SessionService(SessionRepository sessionRepository, UserService userService, CourseService courseService) { + this.sessionRepository = sessionRepository; + this.userService = userService; + this.courseService = courseService; + } + + public Long save(CreateSessionRequest request) { + Course course = courseService.findCourse(request.getCourseId()); + Session session = request.toEntity(); + course.addSession(session); + courseService.saveCourse(course); + return sessionRepository.save(session); + } + + public Session findSession(Long sessionId) { + return sessionRepository.findById(sessionId); + } + + public void enrollSession(EnrollSessionRequest request) { + Session session = findSession(request.getSessionId()); + NsUser user = userService.findUser(request.getUserId()); + session.enrollSession(request.getFee(), user); + sessionRepository.save(session); + } +} diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java index 40c699d43..769632845 100755 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -117,6 +117,24 @@ public boolean isGuestUser() { return false; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NsUser nsUser = (NsUser) o; + return Objects.equals(userId, nsUser.userId) && Objects.equals(password, nsUser.password) + && Objects.equals(name, nsUser.name) && Objects.equals(email, nsUser.email); + } + + @Override + public int hashCode() { + return Objects.hash(userId, password, name, email); + } + @Override public String toString() { return "NsUser{" + diff --git a/src/main/java/nextstep/users/exception/CannotFindUserException.java b/src/main/java/nextstep/users/exception/CannotFindUserException.java new file mode 100644 index 000000000..ee67d130e --- /dev/null +++ b/src/main/java/nextstep/users/exception/CannotFindUserException.java @@ -0,0 +1,9 @@ +package nextstep.users.exception; + +public class CannotFindUserException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public CannotFindUserException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/users/infrastructure/MockUserRepository.java b/src/main/java/nextstep/users/infrastructure/MockUserRepository.java new file mode 100644 index 000000000..04e343a43 --- /dev/null +++ b/src/main/java/nextstep/users/infrastructure/MockUserRepository.java @@ -0,0 +1,22 @@ +package nextstep.users.infrastructure; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import nextstep.users.domain.NsUser; +import nextstep.users.domain.UserRepository; + +public class MockUserRepository implements UserRepository { + + private Map userStorage = new LinkedHashMap<>(); + + public MockUserRepository() { + this.userStorage = new LinkedHashMap<>(); + userStorage.put(1L, new NsUser(1L, "wlscww", "1234", "이수찬", "email")); + } + + @Override + public Optional findByUserId(String userId) { + return Optional.ofNullable(userStorage.get(1L)); + } +} diff --git a/src/main/java/nextstep/users/service/UserService.java b/src/main/java/nextstep/users/service/UserService.java new file mode 100644 index 000000000..422db5c73 --- /dev/null +++ b/src/main/java/nextstep/users/service/UserService.java @@ -0,0 +1,20 @@ +package nextstep.users.service; + +import nextstep.users.domain.NsUser; +import nextstep.users.domain.UserRepository; +import nextstep.users.exception.CannotFindUserException; + +public class UserService { + + public static final String CANNOT_FIND_USER_EXCEPTION = "유저 아이디에 해당하는 유저를 찾을 수 없습니다."; + private final UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public NsUser findUser(String userId) { + return userRepository.findByUserId(userId) + .orElseThrow(() -> new CannotFindUserException(CANNOT_FIND_USER_EXCEPTION)); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 219fe02ad..ac167c12e 100755 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,4 @@ spring.h2.console.enabled=true - spring.datasource.url=jdbc:h2:mem://localhost/~/java-lms;DB_CLOSE_ON_EXIT=FALSE - logging.level.org.springframework.jdbc.core=TRACE logging.level.org.springframework.jdbc.datasource.init=TRACE diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 4a6bc5a66..1f227cc46 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,10 +1,24 @@ -INSERT INTO ns_user (id, user_id, password, name, email, created_at) values (1, 'javajigi', 'test', '자바지기', 'javajigi@slipp.net', CURRENT_TIMESTAMP()); -INSERT INTO ns_user (id, user_id, password, name, email, created_at) values (2, 'sanjigi', 'test', '산지기', 'sanjigi@slipp.net', CURRENT_TIMESTAMP()); +INSERT INTO ns_user (id, user_id, password, name, email, created_at) +values (1, 'javajigi', 'test', '자바지기', 'javajigi@slipp.net', CURRENT_TIMESTAMP()); +INSERT INTO ns_user (id, user_id, password, name, email, created_at) +values (2, 'sanjigi', 'test', '산지기', 'sanjigi@slipp.net', CURRENT_TIMESTAMP()); -INSERT INTO question (id, writer_id, title, contents, created_at, deleted) VALUES (1, 1, '국내에서 Ruby on Rails와 Play가 활성화되기 힘든 이유는 뭘까?', 'Ruby on Rails(이하 RoR)는 2006년 즈음에 정말 뜨겁게 달아올랐다가 금방 가라 앉았다. Play 프레임워크는 정말 한 순간 잠시 눈에 뜨이다가 사라져 버렸다. RoR과 Play 기반으로 개발을 해보면 정말 생산성이 높으며, 웹 프로그래밍이 재미있기까지 하다. Spring MVC + JPA(Hibernate) 기반으로 진행하면 설정할 부분도 많고, 기본으로 지원하지 않는 기능도 많아 RoR과 Play에서 기본적으로 지원하는 기능을 서비스하려면 추가적인 개발이 필요하다.', CURRENT_TIMESTAMP(), false); +INSERT INTO question (id, writer_id, title, contents, created_at, deleted) +VALUES (1, 1, '국내에서 Ruby on Rails와 Play가 활성화되기 힘든 이유는 뭘까?', + 'Ruby on Rails(이하 RoR)는 2006년 즈음에 정말 뜨겁게 달아올랐다가 금방 가라 앉았다. Play 프레임워크는 정말 한 순간 잠시 눈에 뜨이다가 사라져 버렸다. RoR과 Play 기반으로 개발을 해보면 정말 생산성이 높으며, 웹 프로그래밍이 재미있기까지 하다. Spring MVC + JPA(Hibernate) 기반으로 진행하면 설정할 부분도 많고, 기본으로 지원하지 않는 기능도 많아 RoR과 Play에서 기본적으로 지원하는 기능을 서비스하려면 추가적인 개발이 필요하다.', + CURRENT_TIMESTAMP(), false); -INSERT INTO answer (writer_id, contents, created_at, question_id, deleted) VALUES (1, 'http://underscorejs.org/docs/underscore.html Underscore.js 강추합니다! 쓸일도 많고, 코드도 길지 않고, 자바스크립트의 언어나 기본 API를 보완하는 기능들이라 자바스크립트 이해에 도움이 됩니다. 무엇보다 라이브러리 자체가 아주 유용합니다.', CURRENT_TIMESTAMP(), 1, false); +INSERT INTO answer (writer_id, contents, created_at, question_id, deleted) +VALUES (1, + 'http://underscorejs.org/docs/underscore.html Underscore.js 강추합니다! 쓸일도 많고, 코드도 길지 않고, 자바스크립트의 언어나 기본 API를 보완하는 기능들이라 자바스크립트 이해에 도움이 됩니다. 무엇보다 라이브러리 자체가 아주 유용합니다.', + CURRENT_TIMESTAMP(), 1, false); -INSERT INTO answer (writer_id, contents, created_at, question_id, deleted) VALUES (2, '언더스코어 강력 추천드려요. 다만 최신 버전을 공부하는 것보다는 0.10.0 버전부터 보는게 더 좋더군요. 코드의 변천사도 알 수 있고, 최적화되지 않은 코드들이 기능은 그대로 두고 최적화되어 가는 걸 보면 재미가 있습니다 :)', CURRENT_TIMESTAMP(), 1, false); +INSERT INTO answer (writer_id, contents, created_at, question_id, deleted) +VALUES (2, + '언더스코어 강력 추천드려요. 다만 최신 버전을 공부하는 것보다는 0.10.0 버전부터 보는게 더 좋더군요. 코드의 변천사도 알 수 있고, 최적화되지 않은 코드들이 기능은 그대로 두고 최적화되어 가는 걸 보면 재미가 있습니다 :)', + CURRENT_TIMESTAMP(), 1, false); -INSERT INTO question (id, writer_id, title, contents, created_at, deleted) VALUES (2, 2, 'runtime 에 reflect 발동 주체 객체가 뭔지 알 방법이 있을까요?', '설계를 희한하게 하는 바람에 꼬인 문제같긴 합니다만. 여쭙습니다. 상황은 mybatis select 실행될 시에 return object 의 getter 가 호출되면서인데요. getter 안에 다른 property 에 의존중인 코드가 삽입되어 있어서, 만약 다른 mybatis select 구문에 해당 property 가 없다면 exception 이 발생하게 됩니다.', CURRENT_TIMESTAMP(), false); +INSERT INTO question (id, writer_id, title, contents, created_at, deleted) +VALUES (2, 2, 'runtime 에 reflect 발동 주체 객체가 뭔지 알 방법이 있을까요?', + '설계를 희한하게 하는 바람에 꼬인 문제같긴 합니다만. 여쭙습니다. 상황은 mybatis select 실행될 시에 return object 의 getter 가 호출되면서인데요. getter 안에 다른 property 에 의존중인 코드가 삽입되어 있어서, 만약 다른 mybatis select 구문에 해당 property 가 없다면 exception 이 발생하게 됩니다.', + CURRENT_TIMESTAMP(), false); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8d5a988c8..1c65adb69 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,50 +1,55 @@ -create table course ( - id bigint generated by default as identity, - title varchar(255) not null, - creator_id bigint not null, - created_at timestamp not null, +create table course +( + id bigint generated by default as identity, + title varchar(255) not null, + creator_id bigint not null, + created_at timestamp not null, updated_at timestamp, primary key (id) ); -create table ns_user ( - id bigint generated by default as identity, - user_id varchar(20) not null, - password varchar(20) not null, - name varchar(20) not null, - email varchar(50), - created_at timestamp not null, +create table ns_user +( + id bigint generated by default as identity, + user_id varchar(20) not null, + password varchar(20) not null, + name varchar(20) not null, + email varchar(50), + created_at timestamp not null, updated_at timestamp, primary key (id) ); -create table question ( - id bigint generated by default as identity, - created_at timestamp not null, +create table question +( + id bigint generated by default as identity, + created_at timestamp not null, updated_at timestamp, - contents clob, - deleted boolean not null, - title varchar(100) not null, - writer_id bigint, + contents clob, + deleted boolean not null, + title varchar(100) not null, + writer_id bigint, primary key (id) ); -create table answer ( - id bigint generated by default as identity, - created_at timestamp not null, - updated_at timestamp, - contents clob, - deleted boolean not null, +create table answer +( + id bigint generated by default as identity, + created_at timestamp not null, + updated_at timestamp, + contents clob, + deleted boolean not null, question_id bigint, - writer_id bigint, + writer_id bigint, primary key (id) ); -create table delete_history ( - id bigint not null, - content_id bigint, - content_type varchar(255), - created_date timestamp, +create table delete_history +( + id bigint not null, + content_id bigint, + content_type varchar(255), + created_date timestamp, deleted_by_id bigint, primary key (id) ); diff --git a/src/test/java/nextstep/image/domain/ImageCapacityTest.java b/src/test/java/nextstep/image/domain/ImageCapacityTest.java new file mode 100644 index 000000000..0a1a9d2d3 --- /dev/null +++ b/src/test/java/nextstep/image/domain/ImageCapacityTest.java @@ -0,0 +1,41 @@ +package nextstep.image.domain; + +import static nextstep.image.domain.ImageCapacityType.MB; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.image.exception.OutOfRangeCapacityException; +import nextstep.image.exception.OutOfRangeCapacityTypeException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ImageCapacityTest { + + @Test + @DisplayName("이미지 용량은 1MB 이하여야 한다.") + void validate_image_capacity() { + // when + ImageCapacity result = new ImageCapacity(1, "mb"); + + // then + assertThat(result).isEqualTo(new ImageCapacity(1, MB)); + } + + @Test + @DisplayName("이미지 용량 타입이 mb보다 큰 타입이면 예외를 던진다.") + void validate_image_capacity_type() { + // when // then + assertThatThrownBy(() -> new ImageCapacity(1, "gb")) + .isExactlyInstanceOf(OutOfRangeCapacityTypeException.class) + .hasMessage(ImageCapacity.OUT_OF_RANGE_CAPACITY_TYPE_EXCEPTION); + } + + @Test + @DisplayName("이미지 용량이 1MB 보다 크면 예외를 던진다.") + void validate_image_capacity_range() { + // when // then + assertThatThrownBy(() -> new ImageCapacity(2, "mb")) + .isExactlyInstanceOf(OutOfRangeCapacityException.class) + .hasMessage(ImageCapacity.OUT_OF_RANGE_CAPACITY_EXCEPTION); + } +} diff --git a/src/test/java/nextstep/image/domain/ImageCapacityTypeTest.java b/src/test/java/nextstep/image/domain/ImageCapacityTypeTest.java new file mode 100644 index 000000000..bccd6f724 --- /dev/null +++ b/src/test/java/nextstep/image/domain/ImageCapacityTypeTest.java @@ -0,0 +1,38 @@ +package nextstep.image.domain; + +import static nextstep.image.domain.ImageCapacityType.CANNOT_FIND_IMAGE_CAPACITY_TYPE_EXCEPTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.image.exception.CannotFindImageCapacityTypeException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ImageCapacityTypeTest { + + @ParameterizedTest + @DisplayName("이름으로 이미지 용량을 찾을 수 있다.") + @CsvSource(value = {"mb, MB", "gb, GB"}) + void find_by_name(String given, ImageCapacityType expected) { + // when + ImageCapacityType result = ImageCapacityType.findByName(given); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("이름으로 이미지 용량을 찾을 수 없으면 예외를 던진다.") + void cannot_find_image_capacity_type(){ + // given + String tb = "tb"; + + // when // then + assertThatThrownBy(() -> ImageCapacityType.findByName(tb)) + .isExactlyInstanceOf(CannotFindImageCapacityTypeException.class) + .hasMessage(CANNOT_FIND_IMAGE_CAPACITY_TYPE_EXCEPTION); + } +} diff --git a/src/test/java/nextstep/image/domain/ImageSizeTest.java b/src/test/java/nextstep/image/domain/ImageSizeTest.java new file mode 100644 index 000000000..c110bf4a3 --- /dev/null +++ b/src/test/java/nextstep/image/domain/ImageSizeTest.java @@ -0,0 +1,54 @@ +package nextstep.image.domain; + +import static nextstep.image.domain.ImageSize.HEIGHT_VALIATION_EXCEPTION; +import static nextstep.image.domain.ImageSize.RATIO_VALIDATION_EXCEPTION; +import static nextstep.image.domain.ImageSize.WIDTH_VALIDATION_EXCEPTION; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.image.exception.HeightValidationException; +import nextstep.image.exception.RatioValidationException; +import nextstep.image.exception.WidthValidationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ImageSizeTest { + + @Test + @DisplayName("이미지 width가 300 픽셀 이상인지 검증한다.") + void width_exception() { + // given + int width = 299; + int height = 200; + + // when // then + assertThatThrownBy(() -> new ImageSize(width, height)) + .isExactlyInstanceOf(WidthValidationException.class) + .hasMessage(WIDTH_VALIDATION_EXCEPTION); + } + + @Test + @DisplayName("이미지의 height는 200픽셀 이상인지 검증한다.") + void height_exception() { + // given + int width = 300; + int height = 199; + + // when // then + assertThatThrownBy(() -> new ImageSize(width, height)) + .isExactlyInstanceOf(HeightValidationException.class) + .hasMessage(HEIGHT_VALIATION_EXCEPTION); + } + + @Test + @DisplayName("width와 height의 비율은 3:2 인지 검증한다.") + void validate_ratio() { + // given + int width = 300; + int height = 201; + + // when // then + assertThatThrownBy(() -> new ImageSize(width, height)) + .isExactlyInstanceOf(RatioValidationException.class) + .hasMessage(RATIO_VALIDATION_EXCEPTION); + } +} diff --git a/src/test/java/nextstep/image/domain/ImageTypeTest.java b/src/test/java/nextstep/image/domain/ImageTypeTest.java new file mode 100644 index 000000000..7879cfd0c --- /dev/null +++ b/src/test/java/nextstep/image/domain/ImageTypeTest.java @@ -0,0 +1,34 @@ +package nextstep.image.domain; + +import static nextstep.image.domain.ImageType.CANNOT_FIND_IMAGE_TYPE_EXCEPTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.image.exception.CannotFindImageTypeException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ImageTypeTest { + + @ParameterizedTest + @DisplayName("사용할 수 있는 이미지 타입인지 검증한다.") + @CsvSource(value = {"gif, GIF", "jpg, JPG", "jpeg, JPEG", "png, PNG", "svg, SVG"}) + void find_image_type_by_name(String given, ImageType expected) { + // when + ImageType result = ImageType.findByName(given); + + // then + assertThat(result).isEqualTo(expected); + } + + @Test + @DisplayName("원하는 이미지 타입을 찾을 수 없으면 예외를 던진다.") + void cannot_find_image_type(){ + // when // then + assertThatThrownBy(() -> ImageType.findByName("xrp")) + .isExactlyInstanceOf(CannotFindImageTypeException.class) + .hasMessage(CANNOT_FIND_IMAGE_TYPE_EXCEPTION); + } +} diff --git a/src/test/java/nextstep/session/domain/EnrollmentTest.java b/src/test/java/nextstep/session/domain/EnrollmentTest.java new file mode 100644 index 000000000..e93e88724 --- /dev/null +++ b/src/test/java/nextstep/session/domain/EnrollmentTest.java @@ -0,0 +1,59 @@ +package nextstep.session.domain; + +import static nextstep.session.domain.Enrollment.SESSION_FULL_OF_PARTICIPANTS_EXCEPTION; +import static nextstep.session.domain.PaymentType.COMPLETED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import nextstep.session.exception.SessionFullOfParticipantsException; +import nextstep.users.domain.NsUser; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class EnrollmentTest { + + @Test + @DisplayName("유료 강의의 경우, 수강 인원이 모두 채워져 강의를 신청할 수 없으면 예외를 던진다.") + void full_session() { + // given + Enrollment enrollment = new Enrollment(new SessionParticipants(List.of(new NsUser()), 1), null, 10000); + + // when // then + assertThatThrownBy(() -> enrollment.enrollPaidSession(10000, new NsUser())) + .isExactlyInstanceOf(SessionFullOfParticipantsException.class) + .hasMessage(SESSION_FULL_OF_PARTICIPANTS_EXCEPTION); + } + + @Test + @DisplayName("유료 강의를 등록하다.") + void enroll_paid_session() { + // given + Enrollment enrollment = new Enrollment(new SessionParticipants(List.of(new NsUser()), 4), + List.of(new SessionPayment(new NsUser(), COMPLETED)), 10000); + + // when + NsUser participant = new NsUser(1L, "wlscww", "1234", "이수찬", "email"); + enrollment.enrollPaidSession(10000, participant); + + // then + assertThat(enrollment.getParticipants().getParticipants()).contains(participant); + assertThat(enrollment.getPayments()).contains(new SessionPayment(participant, COMPLETED)); + } + + @Test + @DisplayName("무료 걍의를 등록한다.") + void enroll_free_session() { + // given + Enrollment enrollment = new Enrollment(new SessionParticipants(List.of(new NsUser()), 4), + List.of(new SessionPayment(new NsUser(), COMPLETED)), 0); + + // when + NsUser participant = new NsUser(1L, "wlscww", "1234", "이수찬", "email"); + enrollment.enrollFreeSession(participant); + + // then + assertThat(enrollment.getParticipants().getParticipants()).contains(participant); + assertThat(enrollment.getPayments()).contains(new SessionPayment(participant, COMPLETED)); + } +} diff --git a/src/test/java/nextstep/session/domain/SessionParticipantsTest.java b/src/test/java/nextstep/session/domain/SessionParticipantsTest.java new file mode 100644 index 000000000..6a99e6afa --- /dev/null +++ b/src/test/java/nextstep/session/domain/SessionParticipantsTest.java @@ -0,0 +1,51 @@ +package nextstep.session.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import nextstep.users.domain.NsUser; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class SessionParticipantsTest { + + @ParameterizedTest + @DisplayName("수강생들이 다 찼는지 확인한다.") + @CsvSource(value = {"4, false", "3, true"}) + void check_session_is_full(long given, boolean expected) { + // given + SessionParticipants sessionParticipants = createSessionParticipants(given); + + // when + boolean result = sessionParticipants.isFull(); + + // then + assertThat(result).isEqualTo(expected); + } + + private SessionParticipants createSessionParticipants(long given) { + return new SessionParticipants( + List.of(new NsUser(1L, "1", "1234", "이수찬1", "email"), + new NsUser(2L, "2", "1234", "이수찬2", "email"), + new NsUser(3L, "3", "1234", "이수찬3", "email") + ), given + ); + } + + @ParameterizedTest + @DisplayName("수강생을 등록하면 수강 인원이 1 오른다.") + @ValueSource(longs = {5, 6}) + void enroll(long given) { + // given + SessionParticipants sessionParticipants = createSessionParticipants(given); + + // when + NsUser user = new NsUser(4L, "4", "1234", "이수찬4", "email"); + sessionParticipants.enroll(user); + + // then + assertThat(sessionParticipants.getParticipants()).contains(user); + } +} diff --git a/src/test/java/nextstep/session/domain/SessionPeriodTest.java b/src/test/java/nextstep/session/domain/SessionPeriodTest.java new file mode 100644 index 000000000..11e7f966b --- /dev/null +++ b/src/test/java/nextstep/session/domain/SessionPeriodTest.java @@ -0,0 +1,39 @@ +package nextstep.session.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.time.LocalDate; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +public class SessionPeriodTest { + + @ParameterizedTest + @DisplayName("강의가 시작했는지 확인한다.") + @MethodSource("parametersProvider") + void check_session_is_start(LocalDate given, boolean expected) { + // given + SessionPeriod sessionPeriod = new SessionPeriod( + LocalDate.of(2023, 12, 8), + LocalDate.of(2023, 12, 25) + ); + + // when + boolean result = sessionPeriod.isStart(() -> given); + + // then + assertThat(result).isEqualTo(expected); + } + + private static Stream parametersProvider() { + return Stream.of( + arguments(LocalDate.of(2023,12,7), false), + arguments(LocalDate.of(2023,12,8), true) + ); + } +} diff --git a/src/test/java/nextstep/session/domain/SessionStatusTest.java b/src/test/java/nextstep/session/domain/SessionStatusTest.java new file mode 100644 index 000000000..5a3b9ff26 --- /dev/null +++ b/src/test/java/nextstep/session/domain/SessionStatusTest.java @@ -0,0 +1,22 @@ +package nextstep.session.domain; + +import static nextstep.session.domain.SessionStatus.RECRUITING; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class SessionStatusTest { + + @ParameterizedTest + @DisplayName("강의를 들을 수 있는지 물어본다.") + @CsvSource(value = {"PREPARING, false", "RECRUITING, true", "END, false"}) + void check_taking_lecture(SessionStatus given, boolean expected) { + // when + boolean result = given.checkTakingLecture(); + + // then + assertThat(result).isEqualTo(expected); + } +} diff --git a/src/test/java/nextstep/session/service/SessionServiceTest.java b/src/test/java/nextstep/session/service/SessionServiceTest.java new file mode 100644 index 000000000..1a0422be2 --- /dev/null +++ b/src/test/java/nextstep/session/service/SessionServiceTest.java @@ -0,0 +1,106 @@ +package nextstep.session.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.util.List; +import nextstep.courses.domain.Course; +import nextstep.courses.domain.CourseRepository; +import nextstep.courses.infrastructure.MockCourseRepository; +import nextstep.courses.sevice.CourseService; +import nextstep.image.domain.Image; +import nextstep.image.domain.ImageCapacity; +import nextstep.image.domain.ImageCapacityType; +import nextstep.image.domain.ImageSize; +import nextstep.image.domain.ImageType; +import nextstep.session.domain.PaymentType; +import nextstep.session.domain.Session; +import nextstep.session.domain.SessionPayment; +import nextstep.session.domain.SessionPeriod; +import nextstep.session.domain.SessionStatus; +import nextstep.session.domain.SessionType; +import nextstep.session.dto.CreateSessionRequest; +import nextstep.session.dto.EnrollSessionRequest; +import nextstep.session.repository.MockSessionRepository; +import nextstep.users.domain.NsUser; +import nextstep.users.infrastructure.MockUserRepository; +import nextstep.users.service.UserService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SessionServiceTest { + + @Test + @DisplayName("강의를 수강신청한다.") + void enroll_session() { + // given + EnrollSessionRequest request = new EnrollSessionRequest(1L, 10000, "wlscww"); + + // when + MockSessionRepository sessionRepository = new MockSessionRepository(); + SessionService sessionService = new SessionService( + sessionRepository, + new UserService(new MockUserRepository()), + new CourseService(new MockCourseRepository())); + sessionService.enrollSession(request); + + // then + Session findSession = sessionRepository.findById(request.getSessionId()); + + assertThat(findSession.getEnrollment().getPayments()).contains( + new SessionPayment(new NsUser(0L, "tempId", "5678", "이수찬", "email"), PaymentType.COMPLETED), + new SessionPayment(new NsUser(1L, "wlscww", "1234", "이수찬", "email"), PaymentType.COMPLETED)); + assertThat(findSession.getEnrollment().getParticipants().getParticipants()).contains( + new NsUser(0L, "tempId", "5678", "이수찬", "email"), + new NsUser(1L, "wlscww", "1234", "이수찬", "email")); + } + + @Test + @DisplayName("강의를 저장한다.") + void save_session() { + // given + CreateSessionRequest request = createRequest(); + + // when + MockSessionRepository sessionRepository = new MockSessionRepository(); + CourseService courseService = new CourseService(new MockCourseRepository()); + SessionService sessionService = new SessionService( + sessionRepository, + new UserService(new MockUserRepository()), + courseService); + Long sessionId = sessionService.save(request); + + // then + Session findSession = sessionRepository.findById(sessionId); + assertThat(findSession).isEqualTo(createExpectedSession()); + + Course findCourse = courseService.findCourse(request.getCourseId()); + assertThat(findCourse.getSessions()).isEqualTo(List.of(findSession)); + } + + private CreateSessionRequest createRequest() { + return new CreateSessionRequest( + 1L, + LocalDate.of(2023, 12, 20), + LocalDate.of(2023, 12, 30), + 10, + ImageCapacityType.KB, + ImageType.JPEG, + 600, + 400, + SessionType.PAID, + SessionStatus.RECRUITING, + 10000 + ); + } + + private Session createExpectedSession() { + return new Session( + new SessionPeriod(LocalDate.of(2023, 12, 20), LocalDate.of(2023, 12, 30)), + new Image(new ImageCapacity(10, "kb"), ImageType.JPEG, new ImageSize(600, 400)), + SessionType.PAID, + SessionStatus.RECRUITING, + 10000 + ); + } +}