diff --git a/README.md b/README.md index 4985061663..4c316037b8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ # 학습 관리 시스템(Learning Management System) -## 진행 방법 -* 학습 관리 시스템의 수강신청 요구사항을 파악한다. -* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. -* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. -* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. -## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) \ No newline at end of file +## 4단계 - 수강신청(요구사항 변경) + +### 변경된 기능 요구사항 +- 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. + - 강의가 진행 중인 상태에서도 수강신청이 가능해야 한다. + - 강의 진행 상태(준비중, 진행중, 종료)와 모집 상태(비모집중, 모집중)로 상태 값을 분리해야 한다. +- 강의는 강의 커버 이미지 정보를 가진다. + - 강의는 하나 이상의 커버 이미지를 가질 수 있다. +- 강사가 승인하지 않아도 수강 신청하는 모든 사람이 수강 가능하다. + - 우아한테크코스(무료), 우아한테크캠프 Pro(유료)와 같이 선발된 인원만 수강 가능해야 한다. + - 강사는 수강신청한 사람 중 선발된 인원에 대해서만 수강 승인이 가능해야 한다. + - 강사는 수강신청한 사람 중 선발되지 않은 사람은 수강을 취소할 수 있어야 한다. + +### 프로그래밍 요구사항 +- 리팩터링할 때 컴파일 에러와 기존의 단위 테스트의 실패를 최소화하면서 점진적인 리팩터링이 가능하도록 한다. +- DB 테이블에 데이터가 존재한다는 가정하에 리팩터링해야 한다. + - 즉, 기존에 쌓인 데이터를 제거하지 않은 상태로 리팩터링 해야 한다. \ No newline at end of file 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..7548ea4651 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/BaseEntity.java @@ -0,0 +1,29 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class BaseEntity { + private final Long creatorId; + + private final LocalDateTime createdAt; + + private final LocalDateTime updatedAt; + + public BaseEntity(Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + this.creatorId = creatorId; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public Long creatorId() { + return creatorId; + } + + public LocalDateTime createdAt() { + return createdAt; + } + + public LocalDateTime updatedAt() { + return updatedAt; + } +} 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/CourseRepository.java b/src/main/java/nextstep/courses/domain/CourseRepository.java deleted file mode 100644 index 6aaeb638d1..0000000000 --- a/src/main/java/nextstep/courses/domain/CourseRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package nextstep.courses.domain; - -public interface CourseRepository { - int save(Course course); - - Course findById(Long id); -} 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..42c42a19bb --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/Course.java @@ -0,0 +1,60 @@ +package nextstep.courses.domain.course; + +import nextstep.courses.domain.BaseEntity; +import nextstep.courses.domain.course.session.Session; +import nextstep.courses.domain.course.session.Sessions; + +import java.time.LocalDateTime; + +public class Course extends BaseEntity { + private final Long id; + + private final String title; + + private final int ordering; + + private final Sessions sessions; + + public Course(String title, int ordering, Long creatorId, LocalDateTime date) { + this(0L, title, ordering, new Sessions(), creatorId, date, null); + } + + public Course(Long id, String title, int ordering, Sessions sessions, + Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + super(creatorId, createdAt, updatedAt); + this.id = id; + this.title = title; + this.ordering = ordering; + this.sessions = sessions; + } + + public void addSession(Session session) { + this.sessions.add(session); + } + + public Long id() { + return id; + } + + public String title() { + return title; + } + + public int ordering() { + return this.ordering; + } + + public Sessions sessions() { + return this.sessions; + } + + @Override + public String toString() { + return "Course{" + + "id=" + id + + ", title='" + title + '\'' + + ", ordering=" + ordering + + ", sessions=" + sessions + + '}'; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/CourseRepository.java b/src/main/java/nextstep/courses/domain/course/CourseRepository.java new file mode 100644 index 0000000000..b11dd8d46b --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/CourseRepository.java @@ -0,0 +1,7 @@ +package nextstep.courses.domain.course; + +public interface CourseRepository { + Course save(Course course); + + Course findById(Long id); +} diff --git a/src/main/java/nextstep/courses/domain/course/session/Enrollment.java b/src/main/java/nextstep/courses/domain/course/session/Enrollment.java new file mode 100644 index 0000000000..4c1b64f621 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/Enrollment.java @@ -0,0 +1,74 @@ +package nextstep.courses.domain.course.session; + +import nextstep.courses.domain.course.session.apply.Applies; +import nextstep.courses.domain.course.session.apply.Apply; +import nextstep.courses.domain.course.session.apply.ApprovalStatus; +import nextstep.payments.domain.Payment; + +import java.time.LocalDateTime; + +public class Enrollment { + private final Long sessionId; + + private final Applies applies; + + private final SessionDetail sessionDetail; + + public Enrollment(Long sessionId, Applies applies, SessionDetail sessionDetail) { + this.sessionId = sessionId; + this.applies = applies; + this.sessionDetail = sessionDetail; + } + + public Apply apply(Long nsUserId, Payment payment, LocalDateTime date) { + checkPaymentIsPaid(nsUserId, payment); + checkStatusOnRecruit(); + checkStatusOnReadyOrOnGoing(); + checkChargedAndApplySizeIsValid(); + checkApplicantAlreadyExisted(nsUserId); + + return new Apply(sessionId, nsUserId, ApprovalStatus.WAIT, date); + } + + private void checkPaymentIsPaid(Long nsUserId, Payment payment) { + if (this.sessionDetail.charged()) { + checkPaymentIsValid(nsUserId, payment); + } + } + + private void checkPaymentIsValid(Long nsUserId, Payment payment) { + if (payment == null || + !payment.isPaid( + nsUserId, + sessionId, + this.sessionDetail.getAmount() + ) + ) { + throw new IllegalArgumentException("결제를 다시 확인하세요."); + } + } + + private void checkStatusOnRecruit() { + if (this.sessionDetail.notRecruiting()) { + throw new IllegalArgumentException("강의 신청은 모집 중일 때만 가능 합니다."); + } + } + + private void checkStatusOnReadyOrOnGoing() { + if (this.sessionDetail.notReadyOrOnGoing()) { + throw new IllegalArgumentException("강의 신청은 준비, 진행중일 때만 가능 합니다."); + } + } + + private void checkChargedAndApplySizeIsValid() { + if(this.sessionDetail.chargedAndFull(applies.size())) { + throw new IllegalArgumentException("수강 신청 인원이 초과 되었습니다."); + } + } + + private void checkApplicantAlreadyExisted(Long nsUserId) { + if (this.applies.containsUserId(nsUserId)) { + throw new IllegalArgumentException("이미 수강 신청 이력이 있습니다."); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/Session.java b/src/main/java/nextstep/courses/domain/course/session/Session.java new file mode 100644 index 0000000000..e6f7e31a17 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/Session.java @@ -0,0 +1,131 @@ +package nextstep.courses.domain.course.session; + +import nextstep.courses.domain.BaseEntity; +import nextstep.courses.domain.course.session.apply.Applies; +import nextstep.courses.domain.course.session.apply.ApproveCancel; +import nextstep.courses.domain.course.session.image.Images; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Objects; + +public class Session extends BaseEntity { + private final Long id; + + private final Images images; + + private final Applies applies; + + private final SessionDetail sessionDetail; + + public Session(Images images, SessionDuration sessionDuration, SessionState sessionState, + Long creatorId, LocalDateTime date) { + this(0L, images, new Applies(), sessionDuration, sessionState, SessionRecruitStatus.NOT_RECRUIT, + SessionProgressStatus.READY, creatorId, date, null); + } + + public Session(Long id, Images images, Applies applies, SessionDuration sessionDuration, SessionState sessionState, + SessionRecruitStatus sessionRecruitStatus, SessionProgressStatus sessionProgressStatus, + Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + this(id, images, applies, new SessionDetail(sessionDuration, sessionState, sessionProgressStatus, sessionRecruitStatus), + creatorId, createdAt, updatedAt); + } + + public Session(Long id, Images images, Applies applies, SessionDetail sessionDetail, + Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + super(creatorId, createdAt, updatedAt); + if (images == null) { + throw new IllegalArgumentException("이미지를 추가해야 합니다."); + } + + if (applies == null) { + throw new IllegalArgumentException("지원자를 추가해야 합니다."); + } + + if (sessionDetail == null) { + throw new IllegalArgumentException("강의 정보를 추가해야 합니다."); + } + + this.id = id; + this.images = images; + this.applies = applies; + this.sessionDetail = sessionDetail; + } + + public Enrollment enrollment() { + return new Enrollment(this.id, this.applies, this.sessionDetail); + } + + public ApproveCancel approve() { + return new ApproveCancel(this.applies); + } + + public ApproveCancel cancel() { + return new ApproveCancel(this.applies); + } + + public Session changeOnReady(LocalDate date) { + SessionDetail changedSessionDetail = this.sessionDetail.changeOnReady(date); + return new Session(id, images, applies, changedSessionDetail, creatorId(), createdAt(), updatedAt()); + } + + public Session changeOnGoing(LocalDate date) { + SessionDetail changedSessionDetail = this.sessionDetail.changeOnGoing(date); + return new Session(id, images, applies, changedSessionDetail, creatorId(), createdAt(), updatedAt()); + } + + public Session changeOnEnd(LocalDate date) { + SessionDetail changedSessionDetail = this.sessionDetail.changeOnEnd(date); + return new Session(id, images, applies, changedSessionDetail, creatorId(), createdAt(), updatedAt()); + } + + public Long id() { + return this.id; + } + + public Images images() { + return images; + } + + public SessionDetail sessionDetail() { + return sessionDetail; + } + + public SessionDuration sessionDuration() { + return sessionDetail.sessionDuration(); + } + + public SessionState sessionState() { + return sessionDetail.sessionState(); + } + + public SessionProgressStatus sessionProgressStatus() { + return sessionDetail.sessionProgressStatus(); + } + + public SessionRecruitStatus sessionRecruitStatus() { + return sessionDetail.sessionRecruitStatus(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Session session = (Session) o; + return Objects.equals(id, session.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "Session{" + + "id=" + id + + ", images=" + images + + ", sessionDetail=" + sessionDetail + + '}'; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionDetail.java b/src/main/java/nextstep/courses/domain/course/session/SessionDetail.java new file mode 100644 index 0000000000..8a95c546a4 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionDetail.java @@ -0,0 +1,128 @@ +package nextstep.courses.domain.course.session; + +import java.time.LocalDate; +import java.util.Objects; + +public class SessionDetail { + private final SessionDuration sessionDuration; + + private final SessionState sessionState; + + private final SessionProgressStatus sessionProgressStatus; + + private final SessionRecruitStatus sessionRecruitStatus; + + public SessionDetail(SessionDuration sessionDuration, SessionState sessionState, + SessionProgressStatus sessionProgressStatus, SessionRecruitStatus sessionRecruitStatus) { + if (sessionDuration == null) { + throw new IllegalArgumentException("기간이 추가되어야 합니다."); + } + + if (sessionState == null) { + throw new IllegalArgumentException("강의 정보가 추가되어야 합니다."); + } + + if (sessionProgressStatus == null) { + throw new IllegalArgumentException("강의 현황 상태가 추가되어야 합니다."); + } + + if (sessionRecruitStatus == null) { + throw new IllegalArgumentException("강의 모집 여부가 추가되어야 합니다."); + } + + this.sessionDuration = sessionDuration; + this.sessionState = sessionState; + this.sessionProgressStatus = sessionProgressStatus; + this.sessionRecruitStatus = sessionRecruitStatus; + } + + public SessionDuration sessionDuration() { + return sessionDuration; + } + + public SessionState sessionState() { + return sessionState; + } + + public SessionProgressStatus sessionProgressStatus() { + return sessionProgressStatus; + } + + public SessionRecruitStatus sessionRecruitStatus() { + return sessionRecruitStatus; + } + + public SessionDetail changeOnReady(LocalDate date) { + checkChangeDateIsSameOrAfterWithEndDate(date); + return new SessionDetail(sessionDuration, sessionState, SessionProgressStatus.READY, sessionRecruitStatus); + } + + public SessionDetail changeOnGoing(LocalDate date) { + checkChangeDateIsSameOrAfterWithEndDate(date); + return new SessionDetail(sessionDuration, sessionState, SessionProgressStatus.ONGOING, sessionRecruitStatus); + } + + private void checkChangeDateIsSameOrAfterWithEndDate(LocalDate date) { + if (this.sessionDuration.changeDateIsSameOrAfterWithEndDate(date)) { + throw new IllegalArgumentException("강의 종료일 이전에 변경 가능 합니다."); + } + } + + public SessionDetail changeOnEnd(LocalDate date) { + checkChangeDateIsBeforeOrSameWithEndDate(date); + return new SessionDetail(sessionDuration, sessionState, SessionProgressStatus.END, sessionRecruitStatus); + } + + private void checkChangeDateIsBeforeOrSameWithEndDate(LocalDate date) { + if (this.sessionDuration.changeDateIsBeforeOrSameWithEndDate(date)) { + throw new IllegalArgumentException("강의 종료일 이후에 변경 가능합니다."); + } + } + + public boolean charged() { + return this.sessionState.sessionType().charged(); + } + + public Long getAmount() { + return this.sessionState.amount(); + } + + public boolean notRecruiting() { + return this.sessionRecruitStatus.notRecruiting(); + } + + public boolean notReadyOrOnGoing() { + return this.sessionProgressStatus.notReadyOrOnGoing(); + } + + public boolean chargedAndFull(int applySize) { + return this.charged() && applySizeFull(applySize); + } + + private boolean applySizeFull(int applySize) { + return this.sessionState.quota() == applySize; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SessionDetail that = (SessionDetail) o; + return Objects.equals(sessionDuration, that.sessionDuration) && Objects.equals(sessionState, that.sessionState) && sessionProgressStatus == that.sessionProgressStatus && sessionRecruitStatus == that.sessionRecruitStatus; + } + + @Override + public int hashCode() { + return Objects.hash(sessionDuration, sessionState, sessionProgressStatus, sessionRecruitStatus); + } + + @Override + public String toString() { + return "SessionDetail{" + + "sessionDuration=" + sessionDuration + + ", sessionState=" + sessionState + + ", sessionProgressStatus=" + sessionProgressStatus + + ", sessionRecruitStatus=" + sessionRecruitStatus + + '}'; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionDuration.java b/src/main/java/nextstep/courses/domain/course/session/SessionDuration.java new file mode 100644 index 0000000000..cd8231a739 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionDuration.java @@ -0,0 +1,70 @@ +package nextstep.courses.domain.course.session; + +import java.time.LocalDate; +import java.util.Objects; + +public class SessionDuration { + private final LocalDate startDate; + + private final LocalDate endDate; + + public SessionDuration(LocalDate startDate, LocalDate endDate) { + validate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validate(LocalDate startDate, LocalDate endDate) { + checkStartDateOrEndDateNull(startDate, endDate); + checkDurationIsValid(startDate, endDate); + } + + private void checkStartDateOrEndDateNull(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + throw new IllegalArgumentException("시작 날짜와 종료 일자를 모두 입력해주세요."); + } + } + + private void checkDurationIsValid(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { + throw new IllegalArgumentException("시작 날짜가 종료 날짜보다 늦을 수 없습니다."); + } + } + + public boolean changeDateIsSameOrAfterWithEndDate(LocalDate date) { + return date == this.endDate || date.isAfter(this.endDate); + } + + public boolean changeDateIsBeforeOrSameWithEndDate(LocalDate date) { + return date.isBefore(this.endDate) || date == this.endDate; + } + + public LocalDate startDate() { + return startDate; + } + + public LocalDate endDate() { + return endDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SessionDuration sessionDuration = (SessionDuration) o; + return Objects.equals(startDate, sessionDuration.startDate) && Objects.equals(endDate, sessionDuration.endDate); + } + + @Override + public int hashCode() { + return Objects.hash(startDate, endDate); + } + + @Override + public String toString() { + return "Duration{" + + "startDate=" + startDate + + ", endDate=" + endDate + + '}'; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionProgressStatus.java b/src/main/java/nextstep/courses/domain/course/session/SessionProgressStatus.java new file mode 100644 index 0000000000..6f3816d4d0 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionProgressStatus.java @@ -0,0 +1,40 @@ +package nextstep.courses.domain.course.session; + +import java.util.Arrays; + +public enum SessionProgressStatus { + READY("준비중"), + ONGOING("진행중"), + END("종료"); + + private final String description; + + SessionProgressStatus(String description) { + this.description = description; + } + + public static SessionProgressStatus find(String name) { + return Arrays.stream(values()) + .filter(status -> status.name().equals(name)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("허용하는 값은 다음과 같습니다.\n %s", descriptions()) + ) + ); + } + + public static String descriptions() { + StringBuilder sb = new StringBuilder(); + for (SessionProgressStatus status : values()) { + sb.append(status.description).append(", "); + } + sb.setLength(sb.length() - 2); + + return sb.toString(); + } + + public boolean notReadyOrOnGoing() { + return this == SessionProgressStatus.END; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionRecruitStatus.java b/src/main/java/nextstep/courses/domain/course/session/SessionRecruitStatus.java new file mode 100644 index 0000000000..65c22de704 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionRecruitStatus.java @@ -0,0 +1,39 @@ +package nextstep.courses.domain.course.session; + +import java.util.Arrays; + +public enum SessionRecruitStatus { + RECRUIT("모집중"), + NOT_RECRUIT("비모집중"); + + private final String description; + + SessionRecruitStatus(String description) { + this.description = description; + } + + public static SessionRecruitStatus find(String name) { + return Arrays.stream(values()) + .filter(status -> status.name().equals(name)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("허용하는 값은 다음과 같습니다.\n %s", descriptions()) + ) + ); + } + + public static String descriptions() { + StringBuilder sb = new StringBuilder(); + for (SessionRecruitStatus sessionRecruitStatus : values()) { + sb.append(sessionRecruitStatus.description).append(", "); + } + sb.setLength(sb.length() - 2); + + return sb.toString(); + } + + public boolean notRecruiting() { + return this == SessionRecruitStatus.NOT_RECRUIT; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionRepository.java b/src/main/java/nextstep/courses/domain/course/session/SessionRepository.java new file mode 100644 index 0000000000..dd780874a7 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionRepository.java @@ -0,0 +1,15 @@ +package nextstep.courses.domain.course.session; + +import java.util.Optional; + +public interface SessionRepository { + Optional findById(Long id); + + Session save(Long courseId, Session session); + + int update(Long sessionId, Session session); + + Sessions findAllByCourseId(Long courseId); + + int updateCourseId(Long courseId, Session session); +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionState.java b/src/main/java/nextstep/courses/domain/course/session/SessionState.java new file mode 100644 index 0000000000..8db571d903 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionState.java @@ -0,0 +1,82 @@ +package nextstep.courses.domain.course.session; + +import java.util.Objects; + +public class SessionState { + private static final int MAX_APPLY = Integer.MAX_VALUE; + + private final SessionType sessionType; + + private final Long amount; + + private final int quota; + + public SessionState() { + this.sessionType = SessionType.FREE; + this.amount = 0L; + this.quota = MAX_APPLY; + } + + public SessionState(SessionType sessionType, Long amount, int quota) { + validate(sessionType, amount, quota); + this.sessionType = sessionType; + this.amount = amount; + this.quota = quota; + } + + private void validate(SessionType sessionType, Long amount, int quota) { + if (sessionType.free()) { + checkTypeisFree(amount, quota); + } + + if (sessionType.charged()) { + checkTypeisCharged(amount, quota); + } + } + + private void checkTypeisFree(Long amount, int quota) { + if(amount != 0L || quota != MAX_APPLY) { + throw new IllegalArgumentException("무료 강의는 0원, 정원 수가 최대여야 합니다."); + } + } + + private void checkTypeisCharged(Long amount, int quota) { + if(amount == 0L || quota == 0) { + throw new IllegalArgumentException("유료 강의는 0원보다 크고 정원 수가 0보다 커야 합니다."); + } + } + + public SessionType sessionType() { + return sessionType; + } + + public Long amount() { + return amount; + } + + public int quota() { + return quota; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SessionState that = (SessionState) o; + return quota == that.quota && sessionType == that.sessionType && Objects.equals(amount, that.amount); + } + + @Override + public int hashCode() { + return Objects.hash(sessionType, amount, quota); + } + + @Override + public String toString() { + return "SessionState{" + + "sessionType=" + sessionType + + ", amount=" + amount + + ", quota=" + quota + + '}'; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/SessionType.java b/src/main/java/nextstep/courses/domain/course/session/SessionType.java new file mode 100644 index 0000000000..26050db23e --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/SessionType.java @@ -0,0 +1,43 @@ +package nextstep.courses.domain.course.session; + +import java.util.Arrays; + +public enum SessionType { + FREE("무료"), + CHARGE("유료"); + + private final String description; + + SessionType(String description) { + this.description = description; + } + + public static SessionType find(String name) { + return Arrays.stream(values()) + .filter(sessionType -> sessionType.name().equals(name)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("허용하는 값은 다음과 같습니다.\n %s", descriptions()) + ) + ); + } + + public static String descriptions() { + StringBuilder sb = new StringBuilder(); + for (SessionType sessionType : values()) { + sb.append(sessionType.description).append(", "); + } + sb.setLength(sb.length() - 2); + + return sb.toString(); + } + + public boolean charged() { + return this == CHARGE; + } + + public boolean free() { + return this == FREE; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/Sessions.java b/src/main/java/nextstep/courses/domain/course/session/Sessions.java new file mode 100644 index 0000000000..6777975d3e --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/Sessions.java @@ -0,0 +1,49 @@ +package nextstep.courses.domain.course.session; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Sessions implements Iterable { + private final List sessions; + + public Sessions() { + this(new ArrayList<>()); + } + + public Sessions(List sessions) { + validate(sessions); + + this.sessions = sessions; + } + + private void validate(List sessions) { + checkSessionsSizeIsValid(sessions); + } + + private void checkSessionsSizeIsValid(List sessions) { + if(sessions == null) { + throw new IllegalArgumentException("강의는 빈 값이 아니어야 합니다."); + } + } + + public int size() { + return this.sessions.size(); + } + + public void add(Session session) { + checkSessionAlreadyExisted(session); + this.sessions.add(session); + } + + private void checkSessionAlreadyExisted(Session session) { + if (this.sessions.contains(session)) { + throw new IllegalArgumentException("이미 강의를 추가 하였습니다."); + } + } + + @Override + public Iterator iterator() { + return this.sessions.iterator(); + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/apply/Applies.java b/src/main/java/nextstep/courses/domain/course/session/apply/Applies.java new file mode 100644 index 0000000000..6153ea1b2a --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/apply/Applies.java @@ -0,0 +1,63 @@ +package nextstep.courses.domain.course.session.apply; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Applies implements Iterable { + private final List applies; + + public Applies() { + this(new ArrayList<>()); + } + + public Applies(List applicants) { + this.applies = applicants; + } + + public int size() { + return this.applies.size(); + } + + public boolean containsUserId(Long nsUserId) { + return this.applies.stream() + .anyMatch(apply -> apply.isSameWithUserId(nsUserId)); + } + + public Apply approve(Apply apply, LocalDateTime date) { + return this.applies.stream() + .filter(savedApply -> matchesNotApproved(apply, savedApply)) + .findAny() + .map(savedApply -> savedApply.approve(date)) + .orElseThrow(() -> new IllegalArgumentException("지원자가 대기, 취소 상태인지 확인하세요.")); + } + + private static boolean matchesNotApproved(Apply apply, Apply savedApply) { + return savedApply.isSame(apply) && savedApply.notApproved(); + } + + public Apply cancel(Apply apply, LocalDateTime date) { + return this.applies.stream() + .filter(savedApply -> matchesNotCanceled(apply, savedApply)) + .findAny() + .map(savedApply -> savedApply.cancel(date)) + .orElseThrow(() -> new IllegalArgumentException("지원자가 대기, 승인 상태인지 확인하세요.")); + } + + private boolean matchesNotCanceled(Apply apply, Apply savedApply) { + return savedApply.isSame(apply) && savedApply.notCanceled(); + } + + @Override + public String toString() { + return "Applies{" + + "applies=" + applies + + '}'; + } + + @Override + public Iterator iterator() { + return this.applies.iterator(); + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/apply/Apply.java b/src/main/java/nextstep/courses/domain/course/session/apply/Apply.java new file mode 100644 index 0000000000..71d963a6c8 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/apply/Apply.java @@ -0,0 +1,86 @@ +package nextstep.courses.domain.course.session.apply; + +import nextstep.courses.domain.BaseEntity; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class Apply extends BaseEntity { + private final Long sessionId; + + private final Long nsUserId; + + private final ApprovalStatus approvalStatus; + + public Apply(Long sessionId, Long nsUserId, ApprovalStatus approvalStatus, LocalDateTime createdAt) { + this(sessionId, nsUserId, approvalStatus, nsUserId, createdAt, null); + } + + public Apply(Long sessionId, Long nsUserId, ApprovalStatus approvalStatus, Long creatorId, + LocalDateTime createdAt, LocalDateTime updatedAt) { + super(creatorId, createdAt, updatedAt); + this.sessionId = sessionId; + this.nsUserId = nsUserId; + this.approvalStatus = approvalStatus; + } + + public boolean isSame(Apply apply) { + return Objects.equals(this.nsUserId, apply.nsUserId); + } + + public boolean isSameWithUserId(Long nsUserId) { + return Objects.equals(this.nsUserId, nsUserId); + } + + public Long sessionId() { + return sessionId; + } + + public Long nsUserId() { + return nsUserId; + } + + public ApprovalStatus approval() { + return approvalStatus; + } + + public Apply approve(LocalDateTime date) { + return new Apply(this.sessionId, this.nsUserId, ApprovalStatus.APPROVED, + this.creatorId(), this.createdAt(), date); + } + + public Apply cancel(LocalDateTime date) { + return new Apply(this.sessionId, this.nsUserId, ApprovalStatus.CANCELED, + this.creatorId(), this.createdAt(), date); + } + + public boolean notApproved() { + return !this.approvalStatus.approved(); + } + + public boolean notCanceled() { + return !this.approvalStatus.canceled(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Apply apply = (Apply) o; + return approvalStatus == apply.approvalStatus && Objects.equals(sessionId, apply.sessionId) && Objects.equals(nsUserId, apply.nsUserId); + } + + @Override + public int hashCode() { + return Objects.hash(sessionId, nsUserId, approvalStatus); + } + + @Override + public String toString() { + return "Apply{" + + "sessionId=" + sessionId + + ", nsUserId=" + nsUserId + + ", approved=" + approvalStatus + + '}'; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/apply/ApplyRepository.java b/src/main/java/nextstep/courses/domain/course/session/apply/ApplyRepository.java new file mode 100644 index 0000000000..d9ac760722 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/apply/ApplyRepository.java @@ -0,0 +1,13 @@ +package nextstep.courses.domain.course.session.apply; + +import java.util.Optional; + +public interface ApplyRepository { + Applies findAllBySessionId(Long SessionId); + + Apply save(Apply apply); + + Apply update(Apply apply); + + Optional findApplyByNsUserIdAndSessionId(Long NsUserId, Long sessionId); +} diff --git a/src/main/java/nextstep/courses/domain/course/session/apply/ApprovalStatus.java b/src/main/java/nextstep/courses/domain/course/session/apply/ApprovalStatus.java new file mode 100644 index 0000000000..b0286bc993 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/apply/ApprovalStatus.java @@ -0,0 +1,44 @@ +package nextstep.courses.domain.course.session.apply; + +import java.util.Arrays; + +public enum ApprovalStatus { + WAIT("대기중"), + APPROVED("승인"), + CANCELED("취소"); + + private final String description; + + ApprovalStatus(String description) { + this.description = description; + } + + public static ApprovalStatus find(String name) { + return Arrays.stream(values()) + .filter(status -> status.name().equals(name)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("허용하는 값은 다음과 같습니다.\n %s", descriptions()) + ) + ); + } + + public static String descriptions() { + StringBuilder sb = new StringBuilder(); + for (ApprovalStatus approvalStatus : values()) { + sb.append(approvalStatus.description).append(", "); + } + sb.setLength(sb.length() - 2); + + return sb.toString(); + } + + public boolean approved() { + return this == APPROVED; + } + + public boolean canceled() { + return this == CANCELED; + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/apply/ApproveCancel.java b/src/main/java/nextstep/courses/domain/course/session/apply/ApproveCancel.java new file mode 100644 index 0000000000..41b42b7d25 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/apply/ApproveCancel.java @@ -0,0 +1,31 @@ +package nextstep.courses.domain.course.session.apply; + +import nextstep.users.domain.NsUser; + +import java.time.LocalDateTime; + +public class ApproveCancel { + private final Applies applies; + + public ApproveCancel(Applies applies) { + this.applies = applies; + } + + public Apply approve(NsUser loginUser, Apply apply, LocalDateTime date) { + checkUserHasAuthor(loginUser); + + return this.applies.approve(apply, date); + } + + public Apply cancel(NsUser loginUser, Apply apply, LocalDateTime date) { + checkUserHasAuthor(loginUser); + + return this.applies.cancel(apply, date); + } + + private void checkUserHasAuthor(NsUser loginUser) { + if(!loginUser.hasAuthor()) { + throw new IllegalArgumentException("신청을 승인 할 권한이 없습니다."); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/image/Image.java b/src/main/java/nextstep/courses/domain/course/session/image/Image.java new file mode 100644 index 0000000000..b9f7082faf --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/image/Image.java @@ -0,0 +1,106 @@ +package nextstep.courses.domain.course.session.image; + +import nextstep.courses.domain.BaseEntity; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class Image extends BaseEntity { + public static final int MB = 1024 * 1024; + public static final int WIDTH_MIN = 300; + public static final int HEIGHT_MIN = 200; + public static final double WIDTH_HEIGHT_RATIO = 1.5; + + private final Long id; + + private final int imageSize; + + private final ImageType imageType; + + private final int imageWidth; + + private final int imageHeight; + + public Image(int imageSize, ImageType type, int imageWidth, int imageHeight, Long creatorId, LocalDateTime date) { + this(0L, imageSize, type, imageWidth, imageHeight, creatorId, date, null); + } + + public Image(Long id, int imageSize, ImageType imageType, int imageWidth, int imageHeight, + Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + super(creatorId, createdAt, updatedAt); + checkImageSizeIsValid(imageSize); + checkWidthAndHeightSizeIsValid(imageWidth, imageHeight); + checkWidthAndHeightRatioIsValid(imageWidth, imageHeight); + + this.id = id; + this.imageSize = imageSize; + this.imageType = imageType; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + } + + private static void checkImageSizeIsValid(int imageSize) { + if (imageSize > MB) { + throw new IllegalArgumentException("사진 크기는 1MB를 넘을 수 없습니다."); + } + } + + private static void checkWidthAndHeightSizeIsValid(int imageWidth, int imageHeight) { + if (imageWidth < WIDTH_MIN || imageHeight < HEIGHT_MIN) { + throw new IllegalArgumentException( + String.format("가로 픽셀은 %d, 세로 픽셀은 %d 이상이어야 합니다.", WIDTH_MIN, HEIGHT_MIN) + ); + } + } + + private static void checkWidthAndHeightRatioIsValid(int imageWidth, int imageHeight) { + if ((double) imageWidth / imageHeight != WIDTH_HEIGHT_RATIO) { + throw new IllegalArgumentException("가로 세로 비율은 3:2여야 합니다."); + } + } + + public Long id() { + return id; + } + + public int imageSize() { + return imageSize; + } + + public ImageType imageType() { + return imageType; + } + + public int imageWidth() { + return imageWidth; + } + + public int imageHeight() { + return imageHeight; + } + + @Override + public String toString() { + return "Image{" + + "id=" + id + + ", imageSize=" + imageSize + + ", imageType=" + imageType + + ", imageWidth=" + imageWidth + + ", imageHeight=" + imageHeight + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Image image = (Image) o; + return imageSize == image.imageSize && imageWidth == image.imageWidth && + imageHeight == image.imageHeight && Objects.equals(id, image.id) && imageType == image.imageType; + } + + @Override + public int hashCode() { + return Objects.hash(id, imageSize, imageType, imageWidth, imageHeight); + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/image/ImageRepository.java b/src/main/java/nextstep/courses/domain/course/session/image/ImageRepository.java new file mode 100644 index 0000000000..2c296a3571 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/image/ImageRepository.java @@ -0,0 +1,11 @@ +package nextstep.courses.domain.course.session.image; + +import java.util.Optional; + +public interface ImageRepository { + Optional findById(Long id); + + Image save(Long sessionId, Image image); + + Images findAllBySessionId(Long sessionId); +} diff --git a/src/main/java/nextstep/courses/domain/course/session/image/ImageType.java b/src/main/java/nextstep/courses/domain/course/session/image/ImageType.java new file mode 100644 index 0000000000..727887c8f6 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/image/ImageType.java @@ -0,0 +1,38 @@ +package nextstep.courses.domain.course.session.image; + +import java.util.Arrays; + +public enum ImageType { + GIF("gif"), + JPG("jpg"), + JPEG("jpeg"), + PNG("png"), + SVG("svg"); + + ImageType(String description) { + this.description = description; + } + + private final String description; + + public static ImageType find(String name) { + return Arrays.stream(values()) + .filter(imageImageType -> imageImageType.name().equals(name)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("허용하는 확장자는 다음과 같습니다.\n %s", descriptions()) + ) + ); + } + + public static String descriptions() { + StringBuilder sb = new StringBuilder(); + for (ImageType imageType : values()) { + sb.append(imageType.description).append(", "); + } + sb.setLength(sb.length() - 2); + + return sb.toString(); + } +} diff --git a/src/main/java/nextstep/courses/domain/course/session/image/Images.java b/src/main/java/nextstep/courses/domain/course/session/image/Images.java new file mode 100644 index 0000000000..8f108cb72d --- /dev/null +++ b/src/main/java/nextstep/courses/domain/course/session/image/Images.java @@ -0,0 +1,34 @@ +package nextstep.courses.domain.course.session.image; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Images implements Iterable { + private final List images; + + public Images() { + this(new ArrayList<>()); + } + + public Images(List images) { + validate(images); + + this.images = images; + } + + private void validate(List images) { + checkImagesIsNull(images); + } + + private void checkImagesIsNull(List images) { + if (images == null || images.isEmpty()) { + throw new IllegalArgumentException("이미지는 최소 1개 이상 존재해야 합니다."); + } + } + + @Override + public Iterator iterator() { + return this.images.iterator(); + } +} diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcApplyRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcApplyRepository.java new file mode 100644 index 0000000000..1fb04c6c54 --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/JdbcApplyRepository.java @@ -0,0 +1,84 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.course.session.apply.Applies; +import nextstep.courses.domain.course.session.apply.Apply; +import nextstep.courses.domain.course.session.apply.ApplyRepository; +import nextstep.courses.domain.course.session.apply.ApprovalStatus; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository("applyRepository") +public class JdbcApplyRepository implements ApplyRepository { + private final JdbcOperations jdbcTemplate; + + public JdbcApplyRepository(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Applies findAllBySessionId(Long sessionId) { + String sql = "select " + + "session_id, ns_user_id, approval_status, creator_id, created_at, updated_at " + + "from apply where session_id = ?"; + RowMapper rowMapper = (rs, rowNum) -> new Apply( + rs.getLong(1), + rs.getLong(2), + ApprovalStatus.find(rs.getString(3)), + rs.getLong(4), + rs.getTimestamp(5).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(6)) + ); + + List applies = jdbcTemplate.query(sql, rowMapper, sessionId); + return new Applies(applies); + } + + @Override + public Apply save(Apply apply) { + String sql = "insert into apply " + + "(session_id, ns_user_id, approval_status, creator_id, created_at, updated_at) " + + "values(?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update(sql, apply.sessionId(), apply.nsUserId(), apply.approval(), + apply.creatorId(), apply.createdAt(), apply.updatedAt()); + + return apply; + } + + @Override + public Apply update(Apply apply) { + String sql = "update apply set approval_status = ? where session_id = ? and ns_user_id = ?"; + + jdbcTemplate.update(sql, apply.approval(), apply.sessionId(), apply.nsUserId()); + + return apply; + } + + @Override + public Optional findApplyByNsUserIdAndSessionId(Long nsUserId, Long sessionId) { + String sql = "select " + + "session_id, ns_user_id, approval_status, creator_id, created_at, updated_at " + + "from apply where ns_user_id = ? and session_id = ?"; + RowMapper rowMapper = (rs, rowNum) -> new Apply( + rs.getLong(1), + rs.getLong(2), + ApprovalStatus.find(rs.getString(3)), + rs.getLong(4), + rs.getTimestamp(5).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(6))); + return Optional.ofNullable(jdbcTemplate.queryForObject(sql, rowMapper, nsUserId, sessionId)); + } + + private LocalDateTime toLocalDateTime(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime(); + } +} diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java index f9122cbe33..96d325db82 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java @@ -1,40 +1,74 @@ 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 nextstep.courses.domain.course.session.SessionRepository; +import nextstep.courses.domain.course.session.Sessions; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; import java.sql.Timestamp; import java.time.LocalDateTime; +import java.util.Objects; @Repository("courseRepository") public class JdbcCourseRepository implements CourseRepository { - private JdbcOperations jdbcTemplate; + private final JdbcOperations jdbcTemplate; + private final SessionRepository sessionRepository; + KeyHolder keyHolder = new GeneratedKeyHolder(); public JdbcCourseRepository(JdbcOperations jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; + this.sessionRepository = new JdbcSessionRepository(jdbcTemplate); } @Override - public int save(Course course) { - String sql = "insert into course (title, creator_id, created_at) values(?, ?, ?)"; - return jdbcTemplate.update(sql, course.getTitle(), course.getCreatorId(), course.getCreatedAt()); + public Course save(Course course) { + String sql = "insert into course " + + "(title, ordering, creator_id, created_at) " + + "values(?, ?, ?, ?)"; + + jdbcTemplate.update((Connection connection) -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, course.title()); + ps.setInt(2, course.ordering()); + ps.setLong(3, course.creatorId()); + ps.setTimestamp(4, Timestamp.valueOf(course.createdAt())); + return ps; + }, keyHolder); + + Long courseId = Objects.requireNonNull(keyHolder.getKey()).longValue(); + + return new Course(courseId, course.title(), course.ordering(), course.sessions(), + course.creatorId(), course.createdAt(), course.updatedAt()); } @Override public Course findById(Long id) { - String sql = "select id, title, creator_id, created_at, updated_at from course where id = ?"; + String sql = "select " + + "id, title, ordering, creator_id, created_at, updated_at " + + "from course where id = ?"; RowMapper rowMapper = (rs, rowNum) -> new Course( rs.getLong(1), rs.getString(2), - rs.getLong(3), - toLocalDateTime(rs.getTimestamp(4)), - toLocalDateTime(rs.getTimestamp(5))); + rs.getInt(3), + findAllByCourseId(rs.getLong(1)), + rs.getLong(4), + rs.getTimestamp(5).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(6))); return jdbcTemplate.queryForObject(sql, rowMapper, id); } + private Sessions findAllByCourseId(Long id) { + return this.sessionRepository.findAllByCourseId(id); + } + private LocalDateTime toLocalDateTime(Timestamp timestamp) { if (timestamp == null) { return null; diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcImageRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcImageRepository.java new file mode 100644 index 0000000000..9ecb4f9324 --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/JdbcImageRepository.java @@ -0,0 +1,107 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.course.session.image.Image; +import nextstep.courses.domain.course.session.image.ImageRepository; +import nextstep.courses.domain.course.session.image.ImageType; +import nextstep.courses.domain.course.session.image.Images; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Repository("imageRepository") +public class JdbcImageRepository implements ImageRepository { + private final JdbcOperations jdbcTemplate; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + public JdbcImageRepository(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public Optional findById(Long id) { + String sql = "select " + + "id, image_size, image_type, image_width, image_height, session_id, creator_id, created_at, updated_at " + + "from image where id = ?"; + RowMapper rowMapper = (rs, rowNum) -> new Image( + rs.getLong(1), + rs.getInt(2), + ImageType.find(rs.getString(3)), + rs.getInt(4), + rs.getInt(5), + rs.getLong(7), + rs.getTimestamp(8).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(9))); + return Optional.ofNullable(jdbcTemplate.queryForObject(sql, rowMapper, id)); + } + + @Override + public Image save(Long sessionId, Image image) { + ImageType imageType = image.imageType(); + + String sql = "insert into image " + + "(image_size, image_type, image_width, image_height, session_id, creator_id, created_at, updated_at) " + + "values(?, ?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update((Connection connection) -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setInt(1, image.imageSize()); + ps.setString(2, imageType.name()); + ps.setInt(3, image.imageWidth()); + ps.setInt(4, image.imageHeight()); + ps.setLong(5, sessionId); + ps.setLong(6, image.creatorId()); + ps.setTimestamp(7, Timestamp.valueOf(image.createdAt())); + ps.setTimestamp(8, toTimeStamp(image.createdAt())); + return ps; + }, keyHolder); + + Long imageId = Objects.requireNonNull(keyHolder.getKey()).longValue(); + + return new Image(imageId, image.imageSize(), image.imageType(), image.imageWidth(), + image.imageHeight(), image.creatorId(), image.createdAt(), image.updatedAt()); + } + + @Override + public Images findAllBySessionId(Long sessionId) { + String sql = "select " + + "id, image_size, image_type, image_width, image_height, session_id, creator_id, created_at, updated_at " + + "from image where session_id = ?"; + RowMapper rowMapper = (rs, rowNum) -> new Image( + rs.getLong(1), + rs.getInt(2), + ImageType.find(rs.getString(3)), + rs.getInt(4), + rs.getInt(5), + rs.getLong(7), + rs.getTimestamp(8).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(9))); + + List images = jdbcTemplate.query(sql, rowMapper, sessionId); + return new Images(images); + } + + private LocalDateTime toLocalDateTime(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime(); + } + + private Timestamp toTimeStamp(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return Timestamp.valueOf(localDateTime); + } +} diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java new file mode 100644 index 0000000000..a72431bf83 --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java @@ -0,0 +1,183 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.course.session.*; +import nextstep.courses.domain.course.session.apply.Applies; +import nextstep.courses.domain.course.session.apply.ApplyRepository; +import nextstep.courses.domain.course.session.image.Image; +import nextstep.courses.domain.course.session.image.ImageRepository; +import nextstep.courses.domain.course.session.image.Images; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Repository("sessionRepository") +public class JdbcSessionRepository implements SessionRepository { + private final JdbcOperations jdbcTemplate; + private final ImageRepository imageRepository; + private final ApplyRepository applyRepository; + KeyHolder keyHolder = new GeneratedKeyHolder(); + + public JdbcSessionRepository(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.imageRepository = new JdbcImageRepository(jdbcTemplate); + this.applyRepository = new JdbcApplyRepository(jdbcTemplate); + } + + @Override + public Optional findById(Long id) { + String sql = "select " + + "id, start_date, end_date, session_type, amount, quota, recruit_status, " + + "session_status, course_id, creator_id, created_at, updated_at " + + "from session where id = ?"; + + RowMapper rowMapper = (rs, rowNum) -> new Session( + rs.getLong(1), + findAllImagesBySessionId(rs.getLong(1)), + findAllAppliesBySessionId(rs.getLong(1)), + new SessionDuration( + rs.getTimestamp(2).toLocalDateTime().toLocalDate(), + rs.getTimestamp(3).toLocalDateTime().toLocalDate() + ), + new SessionState( + SessionType.find(rs.getString(4)), + rs.getLong(5), + rs.getInt(6) + ), + SessionRecruitStatus.find(rs.getString(7)), + SessionProgressStatus.find(rs.getString(8)), + rs.getLong(10), + rs.getTimestamp(11).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(12))); + return Optional.ofNullable(jdbcTemplate.queryForObject(sql, rowMapper, id)); + } + + @Override + public Session save(Long courseId, Session session) { + Session savedSession = saveSession(courseId, session); + + List images = new ArrayList<>(); + for (Image image : session.images()) { + Image savedImage = imageRepository.save(savedSession.id(), image); + images.add(savedImage); + } + + return new Session(savedSession.id(), new Images(images), new Applies(), session.sessionDetail(), + session.creatorId(), session.createdAt(), session.updatedAt()); + } + + private Session saveSession(Long courseId, Session session) { + SessionDuration sessionDuration = session.sessionDuration(); + SessionRecruitStatus sessionRecruitStatus = session.sessionRecruitStatus(); + SessionState sessionState = session.sessionState(); + SessionType sessionType = sessionState.sessionType(); + SessionProgressStatus sessionProgressStatus = session.sessionProgressStatus(); + String sql = "insert into session " + + "(start_date, end_date, session_type, session_status, amount, " + + "recruit_status, quota, course_id, creator_id, created_at, updated_at) " + + "values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.update((Connection connection) -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setTimestamp(1, Timestamp.valueOf(sessionDuration.startDate().atStartOfDay())); + ps.setTimestamp(2, Timestamp.valueOf(sessionDuration.endDate().atStartOfDay())); + ps.setString(3, sessionType.name()); + ps.setString(4, sessionProgressStatus.name()); + ps.setLong(5, sessionState.amount()); + ps.setString(6, sessionRecruitStatus.name()); + ps.setInt(7, sessionState.quota()); + ps.setLong(8, courseId); + ps.setLong(9, session.creatorId()); + ps.setTimestamp(10, Timestamp.valueOf(session.createdAt())); + ps.setTimestamp(11, toTimeStamp(session.updatedAt())); + return ps; + }, keyHolder); + + Long sessionId = Objects.requireNonNull(keyHolder.getKey()).longValue(); + + return new Session(sessionId, new Images(), new Applies(), session.sessionDetail(), + session.creatorId(), session.createdAt(), session.updatedAt()); + } + + @Override + public int update(Long sessionId, Session session) { + SessionDuration sessionDuration = session.sessionDuration(); + SessionRecruitStatus sessionRecruitStatus = session.sessionRecruitStatus(); + SessionState sessionState = session.sessionState(); + SessionType sessionType = sessionState.sessionType(); + SessionProgressStatus sessionProgressStatus = session.sessionProgressStatus(); + String sql = "update session set " + + "start_date = ?, end_date = ?, session_type = ?, recruit_status = ?, amount = ?, quota = ?, session_status = ? " + + "where id = ?"; + return jdbcTemplate.update(sql, sessionDuration.startDate(), sessionDuration.endDate(), sessionType.name(), + sessionRecruitStatus.name(), sessionState.amount(), sessionState.quota(), sessionProgressStatus.name(), sessionId); + } + + @Override + public Sessions findAllByCourseId(Long courseId) { + String sql = "select " + + "id, start_date, end_date, session_type, amount, quota, " + + "recruit_status, session_status, course_id, creator_id, created_at, updated_at " + + "from session where course_id = ?"; + RowMapper rowMapper = (rs, rowNum) -> new Session( + rs.getLong(1), + findAllImagesBySessionId(rs.getLong(1)), + findAllAppliesBySessionId(rs.getLong(1)), + new SessionDuration( + rs.getTimestamp(2).toLocalDateTime().toLocalDate(), + rs.getTimestamp(3).toLocalDateTime().toLocalDate() + ), + new SessionState( + SessionType.find(rs.getString(4)), + rs.getLong(5), + rs.getInt(6) + ), + SessionRecruitStatus.find(rs.getString(8)), + SessionProgressStatus.find(rs.getString(9)), + rs.getLong(10), + rs.getTimestamp(11).toLocalDateTime(), + toLocalDateTime(rs.getTimestamp(12))); + + List sessions = jdbcTemplate.query(sql, rowMapper, courseId); + return new Sessions(sessions); + } + + @Override + public int updateCourseId(Long courseId, Session session) { + String sql = "update session set course_id = ? where id = ?"; + return jdbcTemplate.update(sql, session.id(), courseId); + } + + private Images findAllImagesBySessionId(Long id) { + return this.imageRepository.findAllBySessionId(id); + } + + private Applies findAllAppliesBySessionId(Long id) { + return this.applyRepository.findAllBySessionId(id); + } + + private LocalDateTime toLocalDateTime(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime(); + } + + private Timestamp toTimeStamp(LocalDateTime localDateTime) { + if (localDateTime == null) { + return null; + } + return Timestamp.valueOf(localDateTime); + } +} diff --git a/src/main/java/nextstep/courses/service/CourseService.java b/src/main/java/nextstep/courses/service/CourseService.java new file mode 100644 index 0000000000..dfb7b8c171 --- /dev/null +++ b/src/main/java/nextstep/courses/service/CourseService.java @@ -0,0 +1,32 @@ +package nextstep.courses.service; + +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.course.CourseRepository; +import nextstep.courses.domain.course.session.Session; +import nextstep.courses.domain.course.session.SessionRepository; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +@Service("courseService") +public class CourseService { + @Resource(name = "courseRepository") + private CourseRepository courseRepository; + + @Resource(name = "sessionRepository") + private SessionRepository sessionRepository; + + public void create(Course course) { + courseRepository.save(course); + } + + public void addSession(long courseId, Session session) { + Course course = getCourse(courseId); + course.addSession(session); + sessionRepository.updateCourseId(courseId, session); + } + + private Course getCourse(long courseId) { + return courseRepository.findById(courseId); + } +} diff --git a/src/main/java/nextstep/courses/service/SessionService.java b/src/main/java/nextstep/courses/service/SessionService.java new file mode 100644 index 0000000000..b5d25a4061 --- /dev/null +++ b/src/main/java/nextstep/courses/service/SessionService.java @@ -0,0 +1,76 @@ +package nextstep.courses.service; + +import nextstep.courses.domain.course.session.*; +import nextstep.courses.domain.course.session.apply.Apply; +import nextstep.courses.domain.course.session.apply.ApplyRepository; +import nextstep.courses.domain.course.session.apply.ApproveCancel; +import nextstep.payments.domain.Payment; +import nextstep.qna.NotFoundException; +import nextstep.users.domain.NsUser; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Service("sessionService") +public class SessionService { + @Resource(name = "sessionRepository") + private SessionRepository sessionRepository; + + @Resource(name = "applyRepository") + private ApplyRepository applyRepository; + + public void create(Long courseId, Session session) { + sessionRepository.save(courseId, session); + } + + public void apply(NsUser loginUser, Long sessionId, Payment payment, LocalDateTime date) { + Session session = getSession(sessionId); + Enrollment enrollment = session.enrollment(); + Apply apply = enrollment.apply(loginUser.getId(), payment, date); + applyRepository.save(apply); + } + + public void approve(NsUser loginUser, Long applicantId, Long sessionId, LocalDateTime date) { + Session session = getSession(sessionId); + Apply savedApply = getApply(sessionId, applicantId); + ApproveCancel approveCancel = session.approve(); + Apply apply = approveCancel.approve(loginUser, savedApply, date); + applyRepository.update(apply); + } + + public void cancel(NsUser loginUser, Long applicantId, Long sessionId, LocalDateTime date) { + Session session = getSession(sessionId); + Apply savedApply = getApply(sessionId, applicantId); + ApproveCancel approveCancel = session.cancel(); + Apply apply = approveCancel.cancel(loginUser, savedApply, date); + applyRepository.update(apply); + } + + public void changeOnReady(Long sessionId, LocalDate date) { + Session session = getSession(sessionId); + Session updatedSession = session.changeOnReady(date); + sessionRepository.update(sessionId, updatedSession); + } + + public void changeOnGoing(Long sessionId, LocalDate date) { + Session session = getSession(sessionId); + Session updatedSession = session.changeOnGoing(date); + sessionRepository.update(sessionId, updatedSession); + } + + public void changeOnEnd(Long sessionId, LocalDate date) { + Session session = getSession(sessionId); + Session updatedSession = session.changeOnEnd(date); + sessionRepository.update(sessionId, updatedSession); + } + + private Session getSession(Long sessionId) { + return sessionRepository.findById(sessionId).orElseThrow(NotFoundException::new); + } + + private Apply getApply(Long sessionId, Long nsUserId) { + return applyRepository.findApplyByNsUserIdAndSessionId(sessionId, nsUserId).orElseThrow(NotFoundException::new); + } +} diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 57d833f851..f637724c63 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -1,6 +1,7 @@ package nextstep.payments.domain; import java.time.LocalDateTime; +import java.util.Objects; public class Payment { private String id; @@ -26,4 +27,10 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.amount = amount; this.createdAt = LocalDateTime.now(); } + + public boolean isPaid(Long userId, Long sessionId, Long amount) { + return (Objects.equals(this.nsUserId, userId)) + && (Objects.equals(this.sessionId, sessionId)) + && (Objects.equals(this.amount, amount)); + } } diff --git a/src/main/java/nextstep/qna/CannotDeleteException.java b/src/main/java/nextstep/qna/CannotDeleteException.java index a8d9d2832e..492369d2f7 100644 --- a/src/main/java/nextstep/qna/CannotDeleteException.java +++ b/src/main/java/nextstep/qna/CannotDeleteException.java @@ -1,6 +1,6 @@ package nextstep.qna; -public class CannotDeleteException extends Exception { +public class CannotDeleteException extends RuntimeException { private static final long serialVersionUID = 1L; public CannotDeleteException(String message) { diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index cf681811e7..999ab85e51 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -1,5 +1,6 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.qna.NotFoundException; import nextstep.qna.UnAuthorizedException; import nextstep.users.domain.NsUser; @@ -17,9 +18,7 @@ public class Answer { private boolean deleted = false; - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; + private TimeStamped timeStamped; public Answer() { } @@ -43,13 +42,21 @@ public Answer(Long id, NsUser writer, Question question, String contents) { this.contents = contents; } + public DeleteHistory toDeleteHistory(LocalDateTime time) { + return new DeleteHistory( + ContentType.ANSWER, + this.id, + this.writer, + time + ); + } + public Long getId() { return id; } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + private void setDeleted() { + this.deleted = true; } public boolean isDeleted() { @@ -72,6 +79,23 @@ public void toQuestion(Question question) { this.question = question; } + public Answer delete(NsUser loginUser) { + validate(loginUser); + + this.setDeleted(); + return this; + } + + private void validate(NsUser loginUser) { + checkLoginUserIsWriter(loginUser); + } + + private void checkLoginUserIsWriter(NsUser loginUser) { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + } + @Override public String toString() { return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]"; diff --git a/src/main/java/nextstep/qna/domain/AnswerRepository.java b/src/main/java/nextstep/qna/domain/AnswerRepository.java index bb894f84cd..3d3e203467 100644 --- a/src/main/java/nextstep/qna/domain/AnswerRepository.java +++ b/src/main/java/nextstep/qna/domain/AnswerRepository.java @@ -4,4 +4,6 @@ public interface AnswerRepository { List findByQuestion(Long questionId); + + void update(Long answerId, Answer answer); } diff --git a/src/main/java/nextstep/qna/domain/Answers.java b/src/main/java/nextstep/qna/domain/Answers.java new file mode 100644 index 0000000000..c97f7d5594 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/Answers.java @@ -0,0 +1,46 @@ +package nextstep.qna.domain; + +import nextstep.users.domain.NsUser; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class Answers implements Iterable { + private final List answers; + + public Answers() { + this(new ArrayList<>()); + } + + public Answers(List answers) { + this.answers = answers; + } + + public Answers delete(NsUser loginUser) { + List deletedAnswers = new ArrayList<>(); + for (Answer answer : answers) { + deletedAnswers.add(answer.delete(loginUser)); + } + + return new Answers(deletedAnswers); + } + + public void add(Answer answer) { + this.answers.add(answer); + } + + public DeleteHistories toDeleteHistories(LocalDateTime time) { + DeleteHistories deleteHistories = new DeleteHistories(); + for (Answer answer : answers) { + deleteHistories.add(answer.toDeleteHistory(time)); + } + return deleteHistories; + } + + @Override + public Iterator iterator() { + return this.answers.iterator(); + } +} diff --git a/src/main/java/nextstep/qna/domain/DeleteHistories.java b/src/main/java/nextstep/qna/domain/DeleteHistories.java new file mode 100644 index 0000000000..071d627298 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/DeleteHistories.java @@ -0,0 +1,46 @@ +package nextstep.qna.domain; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +public class DeleteHistories implements Iterable { + private final List deleteHistories; + + public DeleteHistories() { + this(new ArrayList<>()); + } + + public DeleteHistories(List deleteHistories) { + this.deleteHistories = deleteHistories; + } + + public void add(DeleteHistories inputDeleteHistories) { + for(DeleteHistory deleteHistory : inputDeleteHistories) { + this.add(deleteHistory); + }; + } + + public void add(DeleteHistory deleteHistory) { + this.deleteHistories.add(deleteHistory); + } + + @Override + public Iterator iterator() { + return this.deleteHistories.iterator(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeleteHistories that = (DeleteHistories) o; + return Objects.equals(deleteHistories, that.deleteHistories); + } + + @Override + public int hashCode() { + return Objects.hash(deleteHistories); + } +} diff --git a/src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java b/src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java index e8671add77..54f9fe446c 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java @@ -4,5 +4,5 @@ public interface DeleteHistoryRepository { - void saveAll(List deleteHistories); + void saveAll(DeleteHistories deleteHistories); } diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index b623c52c76..54b39258de 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -1,10 +1,9 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUser; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; public class Question { private Long id; @@ -15,13 +14,11 @@ public class Question { private NsUser writer; - private List answers = new ArrayList<>(); + private Answers answers = new Answers(); private boolean deleted = false; - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; + private TimeStamped timeStamped; public Question() { } @@ -72,16 +69,15 @@ public boolean isOwner(NsUser loginUser) { return writer.equals(loginUser); } - public Question setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + private void setDeleted() { + this.deleted = true; } public boolean isDeleted() { return deleted; } - public List getAnswers() { + public Answers getAnswers() { return answers; } @@ -89,4 +85,40 @@ public List getAnswers() { public String toString() { return "Question [id=" + getId() + ", title=" + title + ", contents=" + contents + ", writer=" + writer + "]"; } + + public Question delete(NsUser loginUser) { + validate(loginUser); + + this.setDeleted(); + this.answers = answers.delete(loginUser); + return this; + } + + private void validate(NsUser loginUser) { + checkLoginUserIsWriter(loginUser); + } + + private void checkLoginUserIsWriter(NsUser loginUser) { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } + } + + public DeleteHistories toDeleteHistories(NsUser loginUser, LocalDateTime date) { + validate(loginUser); + + DeleteHistories deleteHistories = new DeleteHistories(); + deleteHistories.add(toDeleteHistory(date)); + deleteHistories.add(answers.toDeleteHistories(date)); + return deleteHistories; + } + + private DeleteHistory toDeleteHistory(LocalDateTime date) { + return new DeleteHistory( + ContentType.QUESTION, + this.id, + this.writer, + date + ); + } } diff --git a/src/main/java/nextstep/qna/domain/QuestionRepository.java b/src/main/java/nextstep/qna/domain/QuestionRepository.java index ec354bb3f8..0d96b5af59 100644 --- a/src/main/java/nextstep/qna/domain/QuestionRepository.java +++ b/src/main/java/nextstep/qna/domain/QuestionRepository.java @@ -4,4 +4,6 @@ public interface QuestionRepository { Optional findById(Long id); + + void update(Long questionId, Question question); } diff --git a/src/main/java/nextstep/qna/domain/TimeStamped.java b/src/main/java/nextstep/qna/domain/TimeStamped.java new file mode 100644 index 0000000000..0374e63a58 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/TimeStamped.java @@ -0,0 +1,25 @@ +package nextstep.qna.domain; + +import java.time.LocalDateTime; + +public class TimeStamped { + private LocalDateTime createdDate = LocalDateTime.now(); + + private LocalDateTime updatedDate; + + public LocalDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + public LocalDateTime getUpdatedDate() { + return updatedDate; + } + + public void setUpdatedDate(LocalDateTime updatedDate) { + this.updatedDate = updatedDate; + } +} diff --git a/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java b/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java index 7c27c6dd86..d77374626f 100644 --- a/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java +++ b/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java @@ -12,4 +12,9 @@ public class JdbcAnswerRepository implements AnswerRepository { public List findByQuestion(Long questionId) { return null; } + + @Override + public void update(Long answerId, Answer answer) { + + } } diff --git a/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java b/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java index 7ff0188875..2dc6f9e13a 100644 --- a/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java +++ b/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java @@ -1,5 +1,6 @@ package nextstep.qna.infrastructure; +import nextstep.qna.domain.DeleteHistories; import nextstep.qna.domain.DeleteHistory; import nextstep.qna.domain.DeleteHistoryRepository; import org.springframework.stereotype.Repository; @@ -9,7 +10,7 @@ @Repository("deleteHistoryRepository") public class JdbcDeleteHistoryRepository implements DeleteHistoryRepository { @Override - public void saveAll(List deleteHistories) { + public void saveAll(DeleteHistories deleteHistories) { } } diff --git a/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java b/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java index a6e4062dca..122194437c 100644 --- a/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java +++ b/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java @@ -12,4 +12,9 @@ public class JdbcQuestionRepository implements QuestionRepository { public Optional findById(Long id) { return Optional.empty(); } + + @Override + public void update(Long questionId, Question question) { + + } } diff --git a/src/main/java/nextstep/qna/service/DeleteHistoryService.java b/src/main/java/nextstep/qna/service/DeleteHistoryService.java index 7599dca96b..9cc42cb817 100644 --- a/src/main/java/nextstep/qna/service/DeleteHistoryService.java +++ b/src/main/java/nextstep/qna/service/DeleteHistoryService.java @@ -1,5 +1,6 @@ package nextstep.qna.service; +import nextstep.qna.domain.DeleteHistories; import nextstep.qna.domain.DeleteHistory; import nextstep.qna.domain.DeleteHistoryRepository; import org.springframework.stereotype.Service; @@ -15,7 +16,7 @@ public class DeleteHistoryService { private DeleteHistoryRepository deleteHistoryRepository; @Transactional(propagation = Propagation.REQUIRES_NEW) - public void saveAll(List deleteHistories) { + public void saveAll(DeleteHistories deleteHistories) { deleteHistoryRepository.saveAll(deleteHistories); } } diff --git a/src/main/java/nextstep/qna/service/QnAService.java b/src/main/java/nextstep/qna/service/QnAService.java index 5741c84d65..51207d9666 100644 --- a/src/main/java/nextstep/qna/service/QnAService.java +++ b/src/main/java/nextstep/qna/service/QnAService.java @@ -9,8 +9,6 @@ import javax.annotation.Resource; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; @Service("qnaService") public class QnAService { @@ -24,26 +22,16 @@ public class QnAService { private DeleteHistoryService deleteHistoryService; @Transactional - public void deleteQuestion(NsUser loginUser, long questionId) throws CannotDeleteException { - Question question = questionRepository.findById(questionId).orElseThrow(NotFoundException::new); - if (!question.isOwner(loginUser)) { - throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); - } - - List answers = question.getAnswers(); - for (Answer answer : answers) { - if (!answer.isOwner(loginUser)) { - throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); - } - } - - List deleteHistories = new ArrayList<>(); - question.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.QUESTION, questionId, question.getWriter(), LocalDateTime.now())); - for (Answer answer : answers) { - answer.setDeleted(true); - deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); - } + public void deleteQuestion(NsUser loginUser, long questionId, LocalDateTime date) { + Question question = getQuestion(questionId); + question = question.delete(loginUser); + questionRepository.update(questionId, question); + + DeleteHistories deleteHistories = question.toDeleteHistories(loginUser, date); deleteHistoryService.saveAll(deleteHistories); } + + private Question getQuestion(long questionId) throws CannotDeleteException { + return questionRepository.findById(questionId).orElseThrow(NotFoundException::new); + } } diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java old mode 100755 new mode 100644 index 62ec5138cd..731b0252d5 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -18,6 +18,8 @@ public class NsUser { private String email; + private Type type; + private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -25,16 +27,18 @@ public class NsUser { public NsUser() { } - public NsUser(Long id, String userId, String password, String name, String email) { - this(id, userId, password, name, email, LocalDateTime.now(), null); + public NsUser(Long id, String userId, String password, String name, String email, Type type) { + this(id, userId, password, name, email, type, LocalDateTime.now(), null); } - public NsUser(Long id, String userId, String password, String name, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { + public NsUser(Long id, String userId, String password, String name, String email, + Type type, LocalDateTime createdAt, LocalDateTime updatedAt) { this.id = id; this.userId = userId; this.password = password; this.name = name; this.email = email; + this.type = type; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -117,6 +121,14 @@ public boolean isGuestUser() { return false; } + public boolean isSame(Long nsUserId) { + return Objects.equals(this.id, nsUserId); + } + + public boolean hasAuthor() { + return this.type.isTeacher(); + } + private static class GuestNsUser extends NsUser { @Override public boolean isGuestUser() { diff --git a/src/main/java/nextstep/users/domain/Type.java b/src/main/java/nextstep/users/domain/Type.java new file mode 100644 index 0000000000..a8cc1efa66 --- /dev/null +++ b/src/main/java/nextstep/users/domain/Type.java @@ -0,0 +1,38 @@ +package nextstep.users.domain; + +import java.util.Arrays; + +public enum Type { + TEACHER("강사"), STUDENT("학생"); + + private final String description; + + Type(String description) { + this.description = description; + } + + public static Type find(String name) { + return Arrays.stream(values()) + .filter(status -> status.name().equals(name)) + .findAny() + .orElseThrow( + () -> new IllegalArgumentException( + String.format("허용하는 값은 다음과 같습니다.\n %s", descriptions()) + ) + ); + } + + public boolean isTeacher() { + return this == TEACHER; + } + + public static String descriptions() { + StringBuilder sb = new StringBuilder(); + for (Type type : values()) { + sb.append(type.description).append(", "); + } + sb.setLength(sb.length() - 2); + + return sb.toString(); + } +} diff --git a/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java b/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java index 005f04fc5a..0be3c24494 100644 --- a/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java +++ b/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java @@ -1,6 +1,7 @@ package nextstep.users.infrastructure; import nextstep.users.domain.NsUser; +import nextstep.users.domain.Type; import nextstep.users.domain.UserRepository; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.RowMapper; @@ -20,15 +21,16 @@ public JdbcUserRepository(JdbcOperations jdbcTemplate) { @Override public Optional findByUserId(String userId) { - String sql = "select id, user_id, password, name, email, created_at, updated_at from ns_user where user_id = ?"; + String sql = "select id, user_id, password, name, email, type, created_at, updated_at from ns_user where user_id = ?"; RowMapper rowMapper = (rs, rowNum) -> new NsUser( rs.getLong(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), - toLocalDateTime(rs.getTimestamp(6)), - toLocalDateTime(rs.getTimestamp(7))); + Type.find(rs.getString(6)), + toLocalDateTime(rs.getTimestamp(7)), + toLocalDateTime(rs.getTimestamp(8))); return Optional.of(jdbcTemplate.queryForObject(sql, rowMapper, userId)); } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 4a6bc5a665..5895985264 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,5 +1,5 @@ -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, type, created_at) values (1, 'javajigi', 'test', '자바지기', 'javajigi@slipp.net', 'TEACHER', CURRENT_TIMESTAMP()); +INSERT INTO ns_user (id, user_id, password, name, email, type, created_at) values (2, 'sanjigi', 'test', '산지기', 'sanjigi@slipp.net', 'TEACHER', 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); @@ -8,3 +8,11 @@ INSERT INTO answer (writer_id, contents, created_at, question_id, deleted) VALUE 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 apply (session_id, ns_user_id, approved, creator_id, created_at, updated_at) VALUES (10, 1, false, 1, CURRENT_TIMESTAMP(), null); + +INSERT INTO apply (session_id, ns_user_id, approved, creator_id, created_at, updated_at) VALUES (10, 2, false, 2, CURRENT_TIMESTAMP(), null); + +INSERT INTO apply (session_id, ns_user_id, approved, creator_id, created_at, updated_at) VALUES (4, 4, true, 2, CURRENT_TIMESTAMP(), null); + +--INSERT INTO image (id, image_size, image_type, image_width, image_height, creator_id, created_at, updated_at) VALUES (10, 1024, 'JPG', 300, 200, 1, CURRENT_TIMESTAMP(), null); \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8d5a988c8b..f257a5cde2 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,7 +1,47 @@ +create table image ( + id bigint generated by default as identity, + image_size bigint not null, + image_type varchar(20) not null, + image_width bigint not null, + image_height bigint not null, + session_id bigint not null, + creator_id bigint not null, + created_at timestamp not null, + updated_at timestamp, + primary key (id) +); + +create table apply ( + session_id bigint not null, + ns_user_id bigint not null, + approval_status varchar(20) not null, + creator_id bigint not null, + created_at timestamp not null, + updated_at timestamp, + primary key(session_id, ns_user_id) +); + +create table session ( + id bigint generated by default as identity, + start_date DATETIME not null, + end_date DATETIME not null, + session_type varchar(20) not null, + session_status varchar(20) not null, + recruit_status varchar(20) not null, + amount bigint not null, + quota bigint not null, + course_id bigint not null, + creator_id bigint not null, + created_at timestamp not null, + updated_at timestamp, + primary key (id) +); + create table course ( id bigint generated by default as identity, title varchar(255) not null, creator_id bigint not null, + ordering bigint not null, created_at timestamp not null, updated_at timestamp, primary key (id) @@ -13,6 +53,7 @@ create table ns_user ( password varchar(20) not null, name varchar(20) not null, email varchar(50), + type varchar(20) not null, created_at timestamp not null, updated_at timestamp, primary key (id) @@ -47,4 +88,4 @@ create table delete_history ( created_date timestamp, deleted_by_id bigint, primary key (id) -); +); \ No newline at end of file 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..ef655e7294 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/CourseTest.java @@ -0,0 +1,4 @@ +package nextstep.courses.domain.course; + +public class CourseTest { +} diff --git a/src/test/java/nextstep/courses/domain/course/service/CourseServiceTest.java b/src/test/java/nextstep/courses/domain/course/service/CourseServiceTest.java new file mode 100644 index 0000000000..fbb2b3da3a --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/service/CourseServiceTest.java @@ -0,0 +1,43 @@ +package nextstep.courses.domain.course.service; + +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.course.CourseRepository; +import nextstep.courses.domain.course.session.Session; +import nextstep.courses.domain.course.session.SessionRepository; +import nextstep.courses.fixture.CourseFixtures; +import nextstep.courses.fixture.SessionFixtures; +import nextstep.courses.service.CourseService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CourseServiceTest { + @Mock + private CourseRepository courseRepository; + + @Mock + private SessionRepository sessionRepository; + + @InjectMocks + private CourseService courseService; + + @Test + @DisplayName("주어진 강의를 과정에 추가하면 과정에 강의가 추가된다.") + void addSession_success() { + Course course = CourseFixtures.course(); + + when(courseRepository.findById(course.id())).thenReturn(course); + + Session session = SessionFixtures.createdFreeSession(); + courseService.addSession(course.id(), session); + + verify(sessionRepository).updateCourseId(course.id(), session); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/service/SessionServiceTest.java b/src/test/java/nextstep/courses/domain/course/service/SessionServiceTest.java new file mode 100644 index 0000000000..9a6069e74b --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/service/SessionServiceTest.java @@ -0,0 +1,139 @@ +package nextstep.courses.domain.course.service; + +import nextstep.courses.domain.course.session.Session; +import nextstep.courses.domain.course.session.SessionProgressStatus; +import nextstep.courses.domain.course.session.SessionRecruitStatus; +import nextstep.courses.domain.course.session.SessionRepository; +import nextstep.courses.domain.course.session.apply.ApplyRepository; +import nextstep.courses.fixture.ApplyFixtures; +import nextstep.courses.fixture.SessionFixtures; +import nextstep.courses.service.SessionService; +import nextstep.payments.fixture.PaymentFixtures; +import nextstep.qna.NotFoundException; +import nextstep.users.fixtures.NsUserFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class SessionServiceTest { + @Mock + private SessionRepository sessionRepository; + + @Mock + private ApplyRepository applyRepository; + + @InjectMocks + private SessionService sessionService; + + @Test + @DisplayName("주어진 강의 정보로 강의를 생성한다.") + void create_success() { + Session savedSession = SessionFixtures.createdFreeSession(); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.create(1L, savedSession); + Session findSession = sessionRepository.findById(1L).orElseThrow(NotFoundException::new); + + assertThat(findSession.id()).isEqualTo(1L); + assertThat(findSession.sessionDetail()).isEqualTo(savedSession.sessionDetail()); + } + + @Test + @DisplayName("수강 신청은 수강 신청 인원에 해당 인원이 추가된다.") + void apply_success() { + Session savedSession = SessionFixtures.createdChargedSession( + SessionRecruitStatus.RECRUIT, SessionProgressStatus.ONGOING + ); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.apply(NsUserFixtures.TEACHER_JAVAJIGI_1L, + savedSession.id(), + PaymentFixtures.payment(), + SessionFixtures.DATETIME_2023_12_5 + ); + + verify(applyRepository).save(ApplyFixtures.apply_one_wait()); + } + + @Test + @DisplayName("changeOnReady 는 강의 종료일보다 빠르면 강의를 준비중으로 변경 한다.") + void changeOnReady_success() { + Session savedSession = SessionFixtures.createdFreeSession(SessionRecruitStatus.RECRUIT, SessionProgressStatus.ONGOING); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.changeOnReady(savedSession.id(), SessionFixtures.DATE_2023_12_5); + + Session updatedSession = savedSession.changeOnReady(SessionFixtures.DATE_2023_12_5); + verify(sessionRepository).update(savedSession.id(), updatedSession); + } + + @Test + @DisplayName("changeOnGoing 는 강의 종료일보다 빠르면 강의를 진행중으로 변경 한다.") + void changeOnGoing_success() { + Session savedSession = SessionFixtures.createdFreeSession(SessionRecruitStatus.RECRUIT, SessionProgressStatus.READY); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.changeOnGoing(savedSession.id(), SessionFixtures.DATE_2023_12_6); + + Session updatedSession = savedSession.changeOnGoing(SessionFixtures.DATE_2023_12_6); + verify(sessionRepository).update(savedSession.id(), updatedSession); + } + + @Test + @DisplayName("changeOnEnd 는 강의 종료일보다 늦으면 강의를 종료로 변경 한다.") + void changeOnEnd_success() { + Session savedSession = SessionFixtures.createdFreeSession(SessionRecruitStatus.RECRUIT, SessionProgressStatus.READY); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.changeOnEnd(savedSession.id(), SessionFixtures.DATE_2023_12_12); + + Session updatedSession = savedSession.changeOnEnd(SessionFixtures.DATE_2023_12_12); + verify(sessionRepository).update(savedSession.id(), updatedSession); + } + + @Test + @DisplayName("approve 는 수강 신청을 승인 상태로 변경한다.") + void approve_success() { + Session savedSession = SessionFixtures.chargedSessionFullCanceled(); + when(applyRepository.findApplyByNsUserIdAndSessionId(1L, 1L)) + .thenReturn(Optional.of(ApplyFixtures.apply_one_canceled())); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.approve(NsUserFixtures.TEACHER_JAVAJIGI_1L, + savedSession.id(), + 1L, + SessionFixtures.DATETIME_2023_12_5 + ); + + assertThat(savedSession.id()).isEqualTo(1L); + verify(applyRepository).update(ApplyFixtures.apply_one_approved()); + } + + @Test + @DisplayName("cancel 는 수강 신청을 취소 상태로 변경한다.") + void cancel_success() { + Session savedSession = SessionFixtures.chargedSessionFullApproved(); + when(applyRepository.findApplyByNsUserIdAndSessionId(1L, 1L)) + .thenReturn(Optional.of(ApplyFixtures.apply_one_approved())); + when(sessionRepository.findById(savedSession.id())).thenReturn(Optional.of(savedSession)); + + sessionService.cancel(NsUserFixtures.TEACHER_JAVAJIGI_1L, + savedSession.id(), + 1L, + SessionFixtures.DATETIME_2023_12_5 + ); + + assertThat(savedSession.id()).isEqualTo(1L); + verify(applyRepository).update(ApplyFixtures.apply_one_canceled()); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/EnrollmentTest.java b/src/test/java/nextstep/courses/domain/course/session/EnrollmentTest.java new file mode 100644 index 0000000000..12f8abd7d8 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/EnrollmentTest.java @@ -0,0 +1,131 @@ +package nextstep.courses.domain.course.session; + +import nextstep.courses.domain.course.session.apply.Apply; +import nextstep.courses.fixture.SessionFixtures; +import nextstep.payments.fixture.PaymentFixtures; +import nextstep.users.fixtures.NsUserFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EnrollmentTest { + private Session session; + private Enrollment enrollment; + + @Test + @DisplayName("수강 신청은 모집 중, 준비 중이면 새로운 신청을 반환 한다.") + void apply_recruit_ready_success() { + session = SessionFixtures.createdChargedSession( + SessionRecruitStatus.RECRUIT, SessionProgressStatus.READY + ); + + enrollment = session.enrollment(); + Apply newApply = enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + PaymentFixtures.payment(1L, 3L), + SessionFixtures.DATETIME_2023_12_5 + ); + + assertThat(newApply.sessionId()).isEqualTo(session.id()); + assertThat(newApply.nsUserId()).isEqualTo(NsUserFixtures.TEACHER_APPLE_3L.getId()); + } + + @Test + @DisplayName("수강 신청은 모집 중, 진행 중이면 새로운 신청을 반환 한다.") + void apply_recruit_ongoing_success() { + session = SessionFixtures.createdChargedSession( + SessionRecruitStatus.RECRUIT, SessionProgressStatus.ONGOING + ); + + enrollment = session.enrollment(); + Apply newApply = enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + PaymentFixtures.payment(1L, 3L), + SessionFixtures.DATETIME_2023_12_5 + ); + + assertThat(newApply.sessionId()).isEqualTo(session.id()); + assertThat(newApply.nsUserId()).isEqualTo(NsUserFixtures.TEACHER_APPLE_3L.getId()); + } + + @Test + @DisplayName("수강 신청은 비 모집 중이면 신청할 수 없다는 예외를 반환 한다.") + void apply_notRecruitStatus_throwsException() { + session = SessionFixtures.createdChargedSession( + SessionRecruitStatus.NOT_RECRUIT, SessionProgressStatus.ONGOING + ); + enrollment = session.enrollment(); + + assertThatThrownBy( + () -> enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + PaymentFixtures.payment(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("수강 신청은 모집 중, 종료 라면 신청할 수 없다는 예외를 반환 한다.") + void apply_recruitStatus_endStatus_throwsException() { + session = SessionFixtures.createdChargedSession( + SessionRecruitStatus.RECRUIT, SessionProgressStatus.END + ); + enrollment = session.enrollment(); + + assertThatThrownBy( + () -> enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + PaymentFixtures.payment(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("수강 신청은 유료 강의의 경우, 수강 인원 정원을 초과 하면 신청할 수 없다는 예외를 반환 한다.") + void apply_chargeSession_overQuota_throwsException() { + session = SessionFixtures.chargedSessionFullCanceled(); + enrollment = session.enrollment(); + + assertThatThrownBy( + () -> enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + PaymentFixtures.payment(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("수강 신청은 유료 강의 결제가 안 되었다면 신청할 수 없다는 예외를 반환 한다.") + void apply_chargeSession_notPaid_throwsException() { + session = SessionFixtures.createdChargedSession(); + enrollment = session.enrollment(); + + assertThatThrownBy( + () -> enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + null, + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("수강 신청은 수강 금액과 지불 금액이 다르면 신청할 수 없다는 예외를 던진다.") + void apply_chargeSession_differentAmount_throwsException() { + session = SessionFixtures.createdChargedSession(); + enrollment = session.enrollment(); + + assertThatThrownBy( + () -> enrollment.apply( + NsUserFixtures.TEACHER_APPLE_3L.getId(), + PaymentFixtures.differentPayment(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionDurationTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionDurationTest.java new file mode 100644 index 0000000000..039c2e9b59 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionDurationTest.java @@ -0,0 +1,42 @@ +package nextstep.courses.domain.course.session; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionDurationTest { + private LocalDate localDate; + + @BeforeEach + void setUp() { + localDate = LocalDate.of(2023, 12, 5); + } + + @Test + @DisplayName("Duration 은 시작 혹은 종료 날짜에 빈 값이 주어지면 예외를 던진다.") + void newObject_nullAndEmpty_throwsException() { + assertThatThrownBy( + () -> new SessionDuration(null, null) + ).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy( + () -> new SessionDuration(localDate, null) + ).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy( + () -> new SessionDuration(null, localDate) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("Duration 은 시작 날짜가 종료날짜보다 늦으면 예외를 던진다.") + void newObject_startDateIsAfterBeforeDate_throwsException() { + assertThatThrownBy( + () -> new SessionDuration(localDate.plusDays(1), localDate) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionProgressStatusTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionProgressStatusTest.java new file mode 100644 index 0000000000..823233a85b --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionProgressStatusTest.java @@ -0,0 +1,16 @@ +package nextstep.courses.domain.course.session; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionProgressStatusTest { + @Test + @DisplayName("find는 존재하지 않는 값을 입력하면 찾을 수 없다는 예외를 던진다.") + void find_notExistedName_throwsException() { + assertThatThrownBy( + () -> SessionProgressStatus.find("abcd") + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionRecruitStatusTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionRecruitStatusTest.java new file mode 100644 index 0000000000..62e35073bb --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionRecruitStatusTest.java @@ -0,0 +1,16 @@ +package nextstep.courses.domain.course.session; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionRecruitStatusTest { + @Test + @DisplayName("find는 존재하지 않는 값을 입력하면 찾을 수 없다는 예외를 던진다.") + void find_notExistedName_throwsException() { + assertThatThrownBy( + () -> SessionRecruitStatus.find("abcd") + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionStateTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionStateTest.java new file mode 100644 index 0000000000..3d9aa43a63 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionStateTest.java @@ -0,0 +1,40 @@ +package nextstep.courses.domain.course.session; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionStateTest { + @Test + @DisplayName("SessionState 는 무료 강의가 0원이 아니면 예외를 던진다") + void newObject_freeType_overZeroAmount_throwsException() { + assertThatThrownBy( + () -> new SessionState(SessionType.FREE, 1000L, Integer.MAX_VALUE) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("SessionState 는 무료 강의가 정원이 최대가 아니면 예외를 던진다.") + void newObject_freeType_lessThanMaxQuota_throwsException() { + assertThatThrownBy( + () -> new SessionState(SessionType.FREE, 0L, 100) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("SessionState 는 유료 강의가 0원이면 예외를 던진다.") + void newObject_chargedType_zeroAmount_throwsException() { + assertThatThrownBy( + () -> new SessionState(SessionType.CHARGE, 0L, 100) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("SessionState 는 유료 강의가 정원 수가 0명이면 예외를 던진다.") + void newObject_chargedType_zeroQuota_throwsException() { + assertThatThrownBy( + () -> new SessionState(SessionType.CHARGE, 100L, 0) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionTest.java new file mode 100644 index 0000000000..ec7507ad86 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionTest.java @@ -0,0 +1,127 @@ +package nextstep.courses.domain.course.session; + +import nextstep.courses.fixture.ImageFixtures; +import nextstep.courses.fixture.SessionFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionTest { + private Session session; + + @Test + @DisplayName("강의는 이미지가 없으면 이미지를 추가하라는 예외를 반환 한다.") + void newObject_imageNull_throwsException() { + assertThatThrownBy( + () -> new Session( + null, + SessionFixtures.duration(), + SessionFixtures.freeSessionStateZero(), + 1L, + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("강의는 기간이 없으면 기간을 추가하라는 예외를 반환 한다.") + void newObject_durationNull_throwsException() { + assertThatThrownBy( + () -> new Session( + ImageFixtures.images(), + null, + SessionFixtures.freeSessionStateZero(), + 1L, + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("강의는 강의 상태가 없으면 상태를 추가하라는 예외를 반환 한다.") + void newObject_sessionStateNull_throwsException() { + assertThatThrownBy( + () -> new Session( + ImageFixtures.images(), + SessionFixtures.duration(), + null, + 1L, + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("changeOnReady 는 변경할 날짜가 강의 종료일과 같거나 늦으면 예외를 던진다.") + void changeOnReady_changeDateIsSameOrAfterWithEndDate_throwsException() { + session = SessionFixtures.createdFreeSession(); + + assertThatThrownBy( + () -> session.changeOnReady(SessionFixtures.DATE_2023_12_10) + ).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy( + () -> session.changeOnReady(SessionFixtures.DATE_2023_12_12) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("changeOnReady 는 강의를 준비중으로 변경한다.") + void changeOnReady_success() { + session = SessionFixtures.createdFreeSession(SessionRecruitStatus.NOT_RECRUIT, SessionProgressStatus.ONGOING); + + Session updatedSession = session.changeOnReady(SessionFixtures.DATE_2023_12_5); + + assertThat(updatedSession.sessionProgressStatus()).isEqualTo(SessionProgressStatus.READY); + } + + @Test + @DisplayName("changeOnGoing 는 변경할 날짜가 강의 종료일과 같거나 늦으면 예외를 던진다.") + void changeOnGoing_changeDateIsSameOrAfterWithEndDate_throwsException() { + session = SessionFixtures.createdFreeSession(); + + assertThatThrownBy( + () -> session.changeOnGoing(SessionFixtures.DATE_2023_12_10) + ).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy( + () -> session.changeOnGoing(SessionFixtures.DATE_2023_12_12) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("changeOnGoing 는 강의를 진행중으로 변경한다.") + void changeOnGoing_success() { + session = SessionFixtures.createdFreeSession(SessionRecruitStatus.NOT_RECRUIT, SessionProgressStatus.READY); + + Session updatedSession = session.changeOnGoing(SessionFixtures.DATE_2023_12_5); + + assertThat(updatedSession.sessionProgressStatus()).isEqualTo(SessionProgressStatus.ONGOING); + } + + @Test + @DisplayName("changeOnEnd 는 변경할 날짜가 강의 종료일보다 빠르거나 같다면 예외를 던진다.") + void changeOnEnd_changeDateIsBeforeOrSameWithEndDate_throwsException() { + session = SessionFixtures.createdFreeSession(); + + assertThatThrownBy( + () -> session.changeOnEnd(SessionFixtures.DATE_2023_12_6) + ).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy( + () -> session.changeOnEnd(SessionFixtures.DATE_2023_12_10) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("changeOnEnd 는 강의를 종료로 변경한다.") + void changeOnEnd_success() { + session = SessionFixtures.createdFreeSession(SessionRecruitStatus.NOT_RECRUIT, SessionProgressStatus.READY); + + Session updatedSession = session.changeOnEnd(SessionFixtures.DATE_2023_12_12); + + assertThat(updatedSession.sessionProgressStatus()).isEqualTo(SessionProgressStatus.END); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionTypeTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionTypeTest.java new file mode 100644 index 0000000000..5a452587f4 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionTypeTest.java @@ -0,0 +1,16 @@ +package nextstep.courses.domain.course.session; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionTypeTest { + @Test + @DisplayName("find는 존재하지 않는 값을 입력하면 찾을 수 없다는 예외를 던진다.") + void find_notExistedName_throwsException() { + assertThatThrownBy( + () -> SessionType.find("abcd") + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/SessionsTest.java b/src/test/java/nextstep/courses/domain/course/session/SessionsTest.java new file mode 100644 index 0000000000..b2cd9a5249 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/SessionsTest.java @@ -0,0 +1,35 @@ +package nextstep.courses.domain.course.session; + +import nextstep.courses.fixture.SessionFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SessionsTest { + @ParameterizedTest + @NullSource + @DisplayName("Sessions 은 빈 값이 주어지면 예외를 던진다.") + void newObject_null_throwsException(List sessions) { + assertThatThrownBy( + () -> new Sessions(sessions) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("add 는 이미 강의가 추가 되었으면 예외를 던진다.") + void add_alreadyExistedSession_throwsException() { + List sessionList = new ArrayList<>(); + sessionList.add(SessionFixtures.createdChargedSession()); + Sessions sessions = new Sessions(sessionList); + + assertThatThrownBy( + () -> sessions.add(SessionFixtures.createdChargedSession()) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/apply/ApprovalCancelTest.java b/src/test/java/nextstep/courses/domain/course/session/apply/ApprovalCancelTest.java new file mode 100644 index 0000000000..4f965b663c --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/apply/ApprovalCancelTest.java @@ -0,0 +1,106 @@ +package nextstep.courses.domain.course.session.apply; + +import nextstep.courses.domain.course.session.Session; +import nextstep.courses.fixture.ApplyFixtures; +import nextstep.courses.fixture.SessionFixtures; +import nextstep.users.fixtures.NsUserFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ApprovalCancelTest { + private Session session; + private ApproveCancel approveCancel; + + @Test + @DisplayName("approve 는 선생님인 경우 수강생의 강의 신청을 승인한다.") + void approve_teacher_changeApproveTrue() { + session = SessionFixtures.chargedSessionFullCanceled(); + approveCancel = session.approve(); + + Apply changedApply = approveCancel.approve + (NsUserFixtures.TEACHER_JAVAJIGI_1L, + ApplyFixtures.apply_one_canceled(), + SessionFixtures.DATETIME_2023_12_5 + ); + + assertThat(changedApply.approval()).isEqualTo(ApprovalStatus.APPROVED); + } + + @Test + @DisplayName("approve 는 학생인 경우 권한이 없다는 예외를 던진다.") + void approve_student_throwsException() { + session = SessionFixtures.chargedSessionFullCanceled(); + approveCancel = session.approve(); + + assertThatThrownBy( + () -> approveCancel.approve( + NsUserFixtures.STUDENT_ERIC_4L, + ApplyFixtures.apply_one_canceled(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("approve 는 이미 수강 승인이 되었으면 예외를 던진다.") + void approve_alreadyApproved_throwsException() { + session = SessionFixtures.chargedSessionFullApproved(); + approveCancel = session.approve(); + + assertThatThrownBy( + () -> approveCancel.approve( + NsUserFixtures.TEACHER_JAVAJIGI_1L, + ApplyFixtures.apply_one_approved(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("cancel 는 선생님인 경우 수강생의 강의 신청을 취소한다.") + void cancel_teacher_changeApproveTrue() { + session = SessionFixtures.chargedSessionFullApproved(); + approveCancel = session.approve(); + + Apply changedApply = approveCancel.cancel( + NsUserFixtures.TEACHER_JAVAJIGI_1L, + ApplyFixtures.apply_one_canceled(), + SessionFixtures.DATETIME_2023_12_5 + ); + + assertThat(changedApply.approval()).isEqualTo(ApprovalStatus.CANCELED); + } + + @Test + @DisplayName("cancel 는 학생인 경우 권한이 없다는 예외를 던진다.") + void cancel_student_throwsException() { + session = SessionFixtures.chargedSessionFullApproved(); + approveCancel = session.approve(); + + assertThatThrownBy( + () -> approveCancel.cancel( + NsUserFixtures.STUDENT_ERIC_4L, + ApplyFixtures.apply_one_approved(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("cancel 는 이미 수강 취소가 되었으면 예외를 던진다.") + void cancel_alreadyCanceled_throwsException() { + session = SessionFixtures.chargedSessionFullCanceled(); + approveCancel = session.approve(); + + assertThatThrownBy( + () -> approveCancel.cancel( + NsUserFixtures.TEACHER_JAVAJIGI_1L, + ApplyFixtures.apply_one_canceled(), + SessionFixtures.DATETIME_2023_12_5 + ) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/apply/ApprovalStatusTest.java b/src/test/java/nextstep/courses/domain/course/session/apply/ApprovalStatusTest.java new file mode 100644 index 0000000000..8c45aa4012 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/apply/ApprovalStatusTest.java @@ -0,0 +1,16 @@ +package nextstep.courses.domain.course.session.apply; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ApprovalStatusTest { + @Test + @DisplayName("find는 존재하지 않는 값을 입력하면 찾을 수 없다는 예외를 던진다.") + void find_notExistedName_throwsException() { + assertThatThrownBy( + () -> ApprovalStatus.find("abcd") + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/image/ImageTest.java b/src/test/java/nextstep/courses/domain/course/session/image/ImageTest.java new file mode 100644 index 0000000000..d2f1177cf5 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/image/ImageTest.java @@ -0,0 +1,42 @@ +package nextstep.courses.domain.course.session.image; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ImageTest { + @Test + @DisplayName("이미지는 1 MB 를 초과하면 사이즈가 크다는 예외를 반환한다.") + void newObject_over1MBSize_throwsException() { + assertThatThrownBy( + () -> new Image(2 * Image.MB, ImageType.GIF, 300, 200, 1L, LocalDateTime.now()) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지는 가로 픽셀이 300 미만이면 길이가 작아는 예외를 반환한다.") + void newObject_lessThanMinWidthSize_throwsException() { + assertThatThrownBy( + () -> new Image(Image.MB, ImageType.GIF, Image.WIDTH_MIN - 1, Image.HEIGHT_MIN, 1L, LocalDateTime.now()) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지는 세로 픽셀이 200 미만이면 길이가 작아는 예외를 반환한다.") + void newObject_lessThanMinHeightSize_throwsException() { + assertThatThrownBy( + () -> new Image(Image.MB, ImageType.GIF, Image.WIDTH_MIN, Image.HEIGHT_MIN - 1, 1L, LocalDateTime.now()) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지는 가로 세로 비율이 3:2가 아니면 비율이 틀리다는 예외를 반환한다.") + void newObject_inValidWidthHeightRatio_throwsException() { + assertThatThrownBy( + () -> new Image(Image.MB, ImageType.GIF, 600, 500, 1L, LocalDateTime.now()) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/image/ImageTypeTest.java b/src/test/java/nextstep/courses/domain/course/session/image/ImageTypeTest.java new file mode 100644 index 0000000000..dc757815a4 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/image/ImageTypeTest.java @@ -0,0 +1,16 @@ +package nextstep.courses.domain.course.session.image; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ImageTypeTest { + @Test + @DisplayName("find는 존재하지 않는 값을 입력하면 찾을 수 없다는 예외를 던진다.") + void find_notExistedName_throwsException() { + assertThatThrownBy( + () -> ImageType.find("abcd") + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/course/session/image/ImagesTest.java b/src/test/java/nextstep/courses/domain/course/session/image/ImagesTest.java new file mode 100644 index 0000000000..e767cac0a8 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/course/session/image/ImagesTest.java @@ -0,0 +1,20 @@ +package nextstep.courses.domain.course.session.image; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ImagesTest { + @ParameterizedTest + @NullAndEmptySource + @DisplayName("Images 은 빈 값이 주어지면 예외를 던진다.") + void newObject_null_throwsException(List images) { + assertThatThrownBy( + () -> new Images(images) + ).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/fixture/ApplyFixtures.java b/src/test/java/nextstep/courses/fixture/ApplyFixtures.java new file mode 100644 index 0000000000..19017a1327 --- /dev/null +++ b/src/test/java/nextstep/courses/fixture/ApplyFixtures.java @@ -0,0 +1,37 @@ +package nextstep.courses.fixture; + +import nextstep.courses.domain.course.session.apply.Applies; +import nextstep.courses.domain.course.session.apply.Apply; +import nextstep.courses.domain.course.session.apply.ApprovalStatus; + +import java.util.List; + +public class ApplyFixtures { + public static Apply apply_one_wait() { + return new Apply(1L, 1L, ApprovalStatus.WAIT, SessionFixtures.DATETIME_2023_12_5); + } + + public static Applies applies_two_canceled() { + return new Applies(List.of(apply_one_canceled(), apply_two_canceled())); + } + + public static Apply apply_one_canceled() { + return new Apply(1L, 1L, ApprovalStatus.CANCELED, SessionFixtures.DATETIME_2023_12_5); + } + + public static Apply apply_two_canceled() { + return new Apply(1L, 2L, ApprovalStatus.CANCELED, SessionFixtures.DATETIME_2023_12_5); + } + + public static Applies applies_two_approved() { + return new Applies(List.of(apply_one_approved(), apply_two_approved())); + } + + public static Apply apply_one_approved() { + return new Apply(1L, 1L, ApprovalStatus.APPROVED, SessionFixtures.DATETIME_2023_12_5); + } + + public static Apply apply_two_approved() { + return new Apply(1L, 2L, ApprovalStatus.APPROVED, SessionFixtures.DATETIME_2023_12_5); + } +} diff --git a/src/test/java/nextstep/courses/fixture/CourseFixtures.java b/src/test/java/nextstep/courses/fixture/CourseFixtures.java new file mode 100644 index 0000000000..645b8cfe88 --- /dev/null +++ b/src/test/java/nextstep/courses/fixture/CourseFixtures.java @@ -0,0 +1,12 @@ +package nextstep.courses.fixture; + +import nextstep.courses.domain.course.Course; +import nextstep.courses.domain.course.session.Sessions; + +import java.time.LocalDateTime; + +public class CourseFixtures { + public static Course course() { + return new Course(1L, "math", 1, new Sessions(), 1L, LocalDateTime.now(), null); + } +} diff --git a/src/test/java/nextstep/courses/fixture/ImageFixtures.java b/src/test/java/nextstep/courses/fixture/ImageFixtures.java new file mode 100644 index 0000000000..cc57cf72f1 --- /dev/null +++ b/src/test/java/nextstep/courses/fixture/ImageFixtures.java @@ -0,0 +1,18 @@ +package nextstep.courses.fixture; + +import nextstep.courses.domain.course.session.image.Image; +import nextstep.courses.domain.course.session.image.ImageType; +import nextstep.courses.domain.course.session.image.Images; + +import java.time.LocalDateTime; +import java.util.List; + +public class ImageFixtures { + public static Image image() { + return new Image(1000, ImageType.GIF, Image.WIDTH_MIN, Image.HEIGHT_MIN, 1L, LocalDateTime.now()); + } + + public static Images images() { + return new Images(List.of(image())); + } +} diff --git a/src/test/java/nextstep/courses/fixture/SessionFixtures.java b/src/test/java/nextstep/courses/fixture/SessionFixtures.java new file mode 100644 index 0000000000..5b96642e67 --- /dev/null +++ b/src/test/java/nextstep/courses/fixture/SessionFixtures.java @@ -0,0 +1,96 @@ +package nextstep.courses.fixture; + +import nextstep.courses.domain.course.session.*; +import nextstep.courses.domain.course.session.apply.Applies; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public class SessionFixtures { + public static final LocalDate DATE_2023_12_5 = LocalDate.of(2023, 12, 5); + public static final LocalDate DATE_2023_12_6 = LocalDate.of(2023, 12, 6); + public static final LocalDate DATE_2023_12_10 = LocalDate.of(2023, 12, 10); + public static final LocalDate DATE_2023_12_12 = LocalDate.of(2023, 12, 12); + public static final LocalDateTime DATETIME_2023_12_5 = LocalDateTime.of(2023, 12, 5, 0, 0); + + public static SessionDuration duration() { + return new SessionDuration(DATE_2023_12_5, DATE_2023_12_10); + } + + public static Session createdFreeSession() { + return createdFreeSession(SessionRecruitStatus.NOT_RECRUIT, SessionProgressStatus.READY); + } + + public static Session createdFreeSession(SessionRecruitStatus sessionRecruitStatus, SessionProgressStatus sessionProgressStatus) { + return new Session( + 1L, + ImageFixtures.images(), + new Applies(), + new SessionDuration(DATE_2023_12_5, DATE_2023_12_10), + freeSessionStateZero(), + sessionRecruitStatus, + sessionProgressStatus, + 1L, + DATETIME_2023_12_5, + null); + } + + public static Session createdChargedSession() { + return createdChargedSession(SessionRecruitStatus.NOT_RECRUIT, SessionProgressStatus.READY); + } + + public static Session createdChargedSession( + SessionRecruitStatus sessionRecruitStatus, SessionProgressStatus sessionProgressStatus + ) { + return new Session( + 1L, + ImageFixtures.images(), + new Applies(), + new SessionDuration(DATE_2023_12_5, DATE_2023_12_10), + chargedSessionStateZero(1000L, 2), + sessionRecruitStatus, + sessionProgressStatus, + 1L, + DATETIME_2023_12_5, + null + ); + } + + public static Session chargedSessionFullCanceled() { + return new Session( + 1L, + ImageFixtures.images(), + ApplyFixtures.applies_two_canceled(), + new SessionDuration(DATE_2023_12_5, DATE_2023_12_10), + chargedSessionStateZero(1000L, 2), + SessionRecruitStatus.RECRUIT, + SessionProgressStatus.READY, + 1L, + DATETIME_2023_12_5, + null + ); + } + + public static Session chargedSessionFullApproved() { + return new Session( + 1L, + ImageFixtures.images(), + ApplyFixtures.applies_two_approved(), + new SessionDuration(DATE_2023_12_5, DATE_2023_12_10), + chargedSessionStateZero(1000L, 2), + SessionRecruitStatus.RECRUIT, + SessionProgressStatus.READY, + 1L, + DATETIME_2023_12_5, + null + ); + } + + public static SessionState freeSessionStateZero() { + return new SessionState(); + } + + public static SessionState chargedSessionStateZero(Long amount, int quota) { + return new SessionState(SessionType.CHARGE, amount, quota); + } +} diff --git a/src/test/java/nextstep/courses/infrastructure/ApplyRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/ApplyRepositoryTest.java new file mode 100644 index 0000000000..70c23b29ad --- /dev/null +++ b/src/test/java/nextstep/courses/infrastructure/ApplyRepositoryTest.java @@ -0,0 +1,70 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.course.session.apply.*; +import nextstep.courses.fixture.ApplyFixtures; +import nextstep.courses.fixture.SessionFixtures; +import nextstep.qna.NotFoundException; +import nextstep.users.fixtures.NsUserFixtures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +public class ApplyRepositoryTest { + private static final Logger LOGGER = LoggerFactory.getLogger(ApplyRepositoryTest.class); + + @Autowired + private JdbcTemplate jdbcTemplate; + + private ApplyRepository applyRepository; + + @BeforeEach + void setUp() { + applyRepository = new JdbcApplyRepository(jdbcTemplate); + } + + /* data.sql 파일에 이미 저장한 데이터로 테스트 합니다. */ + @Test + void find_success() { + Applies applies = applyRepository.findAllBySessionId(10L); + + assertThat(applies.size()).isEqualTo(2); + LOGGER.debug("Applies: {}", applies); + } + + @Test + void saveApply_success() { + Apply savedApply = applyRepository.save(ApplyFixtures.apply_one_canceled()); + + Apply findApply = applyRepository + .findApplyByNsUserIdAndSessionId( + NsUserFixtures.TEACHER_JAVAJIGI_1L.getId(), + SessionFixtures.createdFreeSession().id() + ) + .orElseThrow(NotFoundException::new); + + assertThat(findApply.nsUserId()).isEqualTo(savedApply.nsUserId()); + assertThat(findApply.sessionId()).isEqualTo(savedApply.sessionId()); + } + + @Test + void updateApply_success() { + Apply savedApply = applyRepository.save(ApplyFixtures.apply_one_canceled()); + Apply updatedApply = savedApply.approve(SessionFixtures.DATETIME_2023_12_5); + applyRepository.update(updatedApply); + + Apply findApply = applyRepository + .findApplyByNsUserIdAndSessionId( + NsUserFixtures.TEACHER_JAVAJIGI_1L.getId(), + SessionFixtures.createdFreeSession().id() + ) + .orElseThrow(NotFoundException::new); + assertThat(findApply.approval()).isEqualTo(ApprovalStatus.APPROVED); + } +} diff --git a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java index f087fc0ad2..4b79a43c29 100644 --- a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java +++ b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java @@ -1,7 +1,8 @@ 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 nextstep.courses.domain.course.session.SessionRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -10,6 +11,8 @@ import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.jdbc.core.JdbcTemplate; +import java.time.LocalDateTime; + import static org.assertj.core.api.Assertions.assertThat; @JdbcTest @@ -21,18 +24,22 @@ public class CourseRepositoryTest { private CourseRepository courseRepository; + private SessionRepository sessionRepository; + @BeforeEach void setUp() { courseRepository = new JdbcCourseRepository(jdbcTemplate); + sessionRepository = new JdbcSessionRepository(jdbcTemplate); } @Test void crud() { - Course course = new Course("TDD, 클린 코드 with Java", 1L); - int count = courseRepository.save(course); - assertThat(count).isEqualTo(1); + Course course = new Course("TDD, 클린 코드 with Java", 1, 1L, LocalDateTime.now()); + courseRepository.save(course); Course savedCourse = courseRepository.findById(1L); - assertThat(course.getTitle()).isEqualTo(savedCourse.getTitle()); + assertThat(course.title()).isEqualTo(savedCourse.title()); + assertThat(course.ordering()).isEqualTo(savedCourse.ordering()); + assertThat(course.creatorId()).isEqualTo(savedCourse.creatorId()); LOGGER.debug("Course: {}", savedCourse); } } diff --git a/src/test/java/nextstep/courses/infrastructure/ImageRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/ImageRepositoryTest.java new file mode 100644 index 0000000000..21d92ab7f6 --- /dev/null +++ b/src/test/java/nextstep/courses/infrastructure/ImageRepositoryTest.java @@ -0,0 +1,42 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.course.session.image.Image; +import nextstep.courses.domain.course.session.image.ImageRepository; +import nextstep.courses.domain.course.session.image.ImageType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +public class ImageRepositoryTest { + private static final Logger LOGGER = LoggerFactory.getLogger(ImageRepositoryTest.class); + + @Autowired + private JdbcTemplate jdbcTemplate; + + private ImageRepository imageRepository; + + @BeforeEach + void setUp() { + imageRepository = new JdbcImageRepository(jdbcTemplate); + } + + @Test + void save_success() { + Image image = new Image(1024, ImageType.JPG, 300, 200, 1L, LocalDateTime.now()); + Image savedImage = imageRepository.save(1L, image); + assertThat(image.imageSize()).isEqualTo(savedImage.imageSize()); + assertThat(image.imageType()).isEqualTo(savedImage.imageType()); + assertThat(image.imageWidth()).isEqualTo(savedImage.imageWidth()); + assertThat(image.imageHeight()).isEqualTo(savedImage.imageHeight()); + LOGGER.debug("Image: {}", savedImage); + } +} diff --git a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java new file mode 100644 index 0000000000..46254bc799 --- /dev/null +++ b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java @@ -0,0 +1,58 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.course.session.*; +import nextstep.courses.fixture.SessionFixtures; +import nextstep.qna.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +public class SessionRepositoryTest { + private static final Logger LOGGER = LoggerFactory.getLogger(SessionRepositoryTest.class); + + @Autowired + private JdbcTemplate jdbcTemplate; + + private SessionRepository sessionRepository; + + @BeforeEach + void setUp() { + sessionRepository = new JdbcSessionRepository(jdbcTemplate); + } + + @Test + void save_success() { + Session session = SessionFixtures.createdFreeSession(); + Session savedSession = sessionRepository.save(1L, session); + Session findSession = sessionRepository.findById(savedSession.id()).orElseThrow(NotFoundException::new); + + assertThat(findSession.id()).isEqualTo(1L); + assertThat(findSession.sessionDuration()).isEqualTo(session.sessionDuration()); + assertThat(findSession.sessionState()).isEqualTo(session.sessionState()); + + LOGGER.debug("Session: {}", savedSession); + } + + @Test + void update_success() { + Session session = SessionFixtures.createdChargedSession(); + Session savedSession = sessionRepository.save(1L, session); + + SessionState updateSessionState = new SessionState(SessionType.CHARGE, 2000L, 30); + Session updatedSession = new Session(savedSession.images(), savedSession.sessionDuration(), + updateSessionState, savedSession.creatorId(), savedSession.createdAt()); + sessionRepository.update(savedSession.id(), updatedSession); + Session findUpdatedSession = sessionRepository.findById(savedSession.id()).orElseThrow(NotFoundException::new); + + assertThat(findUpdatedSession.id()).isEqualTo(savedSession.id()); + assertThat(findUpdatedSession.sessionState()).isEqualTo(updateSessionState); + LOGGER.debug("Session: {}", findUpdatedSession); + } +} diff --git a/src/test/java/nextstep/payments/fixture/PaymentFixtures.java b/src/test/java/nextstep/payments/fixture/PaymentFixtures.java new file mode 100644 index 0000000000..7699ed54fa --- /dev/null +++ b/src/test/java/nextstep/payments/fixture/PaymentFixtures.java @@ -0,0 +1,17 @@ +package nextstep.payments.fixture; + +import nextstep.payments.domain.Payment; + +public class PaymentFixtures { + public static Payment payment() { + return payment(1L, 1L); + } + + public static Payment differentPayment() { + return payment(999L, 999L); + } + + public static Payment payment(Long sessionId, Long nsUserId) { + return new Payment("1", sessionId, nsUserId, 1000L); + } +} diff --git a/src/test/java/nextstep/qna/domain/AnswerTest.java b/src/test/java/nextstep/qna/domain/AnswerTest.java index 8e80ffb429..bebdc5c86f 100644 --- a/src/test/java/nextstep/qna/domain/AnswerTest.java +++ b/src/test/java/nextstep/qna/domain/AnswerTest.java @@ -1,8 +1,31 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUserTest; +import nextstep.qna.CannotDeleteException; +import nextstep.users.fixtures.NsUserFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class AnswerTest { - public static final Answer A1 = new Answer(NsUserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); - public static final Answer A2 = new Answer(NsUserTest.SANJIGI, QuestionTest.Q1, "Answers Contents2"); + private static final Question Q1 = new Question(NsUserFixtures.TEACHER_JAVAJIGI_1L, "title1", "contents1"); + private static final Answer A1 = new Answer(NsUserFixtures.TEACHER_JAVAJIGI_1L, Q1, "Answers Contents1"); + private static final Answer A2 = new Answer(NsUserFixtures.TEACHER_SANJIGI_2L, Q1, "Answers Contents2"); + + @Test + @DisplayName("답변 작성자가 로그인한 사용자면 답변을 삭제 상태로 변경하고 해당 답변을 반환한다") + void setDeleted_success() throws CannotDeleteException { + Answer deletedAnswer = A1.delete(NsUserFixtures.TEACHER_JAVAJIGI_1L); + + assertThat(deletedAnswer.isDeleted()).isTrue(); + } + + @Test + @DisplayName("질문 작성자가 로그인한 사용자가 아니면 삭제할 수 없다는 예외를 던진다") + void setDeleted_different_answerWriter_loginUser_throwsException() { + assertThatThrownBy( + () -> A2.delete(NsUserFixtures.TEACHER_JAVAJIGI_1L) + ).isInstanceOf(CannotDeleteException.class); + } } diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 3b87823963..009080bd95 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -1,8 +1,37 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUserTest; +import nextstep.qna.CannotDeleteException; +import nextstep.users.fixtures.NsUserFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class QuestionTest { - public static final Question Q1 = new Question(NsUserTest.JAVAJIGI, "title1", "contents1"); - public static final Question Q2 = new Question(NsUserTest.SANJIGI, "title2", "contents2"); + private static final Question Q1 = new Question(NsUserFixtures.TEACHER_JAVAJIGI_1L, "title1", "contents1"); + private static final Answer A1 = new Answer(NsUserFixtures.TEACHER_JAVAJIGI_1L, Q1, "Answers Contents1"); + private static final Answer A2 = new Answer(NsUserFixtures.TEACHER_SANJIGI_2L, Q1, "Answers Contents2"); + + private static final Question Q2 = new Question(NsUserFixtures.TEACHER_SANJIGI_2L, "title2", "contents2"); + + @Test + @DisplayName("질문 작성자가 로그인한 사용자면 질문을 삭제 상태로 변경하고 해당 질문을 반환한다") + void delete_success() throws CannotDeleteException { + Question deletedQuestion = Q1.delete(NsUserFixtures.TEACHER_JAVAJIGI_1L); + assertThat(deletedQuestion.isDeleted()).isTrue(); + + Answers answers = deletedQuestion.getAnswers(); + for (Answer answer : answers) { + assertThat(answer.isDeleted()).isTrue(); + } + } + + @Test + @DisplayName("질문 작성자가 로그인한 사용자가 아니면 삭제할 수 없다는 예외를 던진다") + void delete_different_questionWriter_loginUser_throwsException() { + assertThatThrownBy( + () -> Q2.delete(NsUserFixtures.TEACHER_JAVAJIGI_1L) + ).isInstanceOf(CannotDeleteException.class); + } } diff --git a/src/test/java/nextstep/qna/service/QnaServiceTest.java b/src/test/java/nextstep/qna/service/QnaServiceTest.java index e1e943c23a..761654f357 100644 --- a/src/test/java/nextstep/qna/service/QnaServiceTest.java +++ b/src/test/java/nextstep/qna/service/QnaServiceTest.java @@ -2,7 +2,7 @@ import nextstep.qna.CannotDeleteException; import nextstep.qna.domain.*; -import nextstep.users.domain.NsUserTest; +import nextstep.users.fixtures.NsUserFixtures; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -11,7 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -28,16 +28,23 @@ public class QnaServiceTest { @Mock private DeleteHistoryService deleteHistoryService; + @Mock + private AnswerRepository answerRepository; + @InjectMocks private QnAService qnAService; private Question question; + private List answers = new ArrayList<>(); private Answer answer; + private LocalDateTime date = LocalDateTime.of(2023, 12, 4, 23, 0); + @BeforeEach public void setUp() throws Exception { - question = new Question(1L, NsUserTest.JAVAJIGI, "title1", "contents1"); - answer = new Answer(11L, NsUserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + question = new Question(1L, NsUserFixtures.TEACHER_JAVAJIGI_1L, "title1", "contents1"); + answer = new Answer(11L, NsUserFixtures.TEACHER_JAVAJIGI_1L, question, "Answers Contents1"); + answers.add(answer); question.addAnswer(answer); } @@ -46,7 +53,7 @@ public void setUp() throws Exception { when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); assertThat(question.isDeleted()).isFalse(); - qnAService.deleteQuestion(NsUserTest.JAVAJIGI, question.getId()); + qnAService.deleteQuestion(NsUserFixtures.TEACHER_JAVAJIGI_1L, question.getId(), date); assertThat(question.isDeleted()).isTrue(); verifyDeleteHistories(); @@ -57,7 +64,7 @@ public void setUp() throws Exception { when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); assertThatThrownBy(() -> { - qnAService.deleteQuestion(NsUserTest.SANJIGI, question.getId()); + qnAService.deleteQuestion(NsUserFixtures.TEACHER_SANJIGI_2L, question.getId(), date); }).isInstanceOf(CannotDeleteException.class); } @@ -65,7 +72,7 @@ public void setUp() throws Exception { public void delete_성공_질문자_답변자_같음() throws Exception { when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); - qnAService.deleteQuestion(NsUserTest.JAVAJIGI, question.getId()); + qnAService.deleteQuestion(NsUserFixtures.TEACHER_SANJIGI_2L, question.getId(), date); assertThat(question.isDeleted()).isTrue(); assertThat(answer.isDeleted()).isTrue(); @@ -77,14 +84,15 @@ public void setUp() throws Exception { when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); assertThatThrownBy(() -> { - qnAService.deleteQuestion(NsUserTest.SANJIGI, question.getId()); + qnAService.deleteQuestion(NsUserFixtures.TEACHER_SANJIGI_2L, question.getId(), date); }).isInstanceOf(CannotDeleteException.class); } private void verifyDeleteHistories() { - List deleteHistories = Arrays.asList( - new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()), - new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); + DeleteHistories deleteHistories = new DeleteHistories(); + deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), date)); + deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), date)); + verify(deleteHistoryService).saveAll(deleteHistories); } } diff --git a/src/test/java/nextstep/users/domain/NsUserTest.java b/src/test/java/nextstep/users/domain/NsUserTest.java deleted file mode 100644 index ffac9f9a7d..0000000000 --- a/src/test/java/nextstep/users/domain/NsUserTest.java +++ /dev/null @@ -1,6 +0,0 @@ -package nextstep.users.domain; - -public class NsUserTest { - public static final NsUser JAVAJIGI = new NsUser(1L, "javajigi", "password", "name", "javajigi@slipp.net"); - public static final NsUser SANJIGI = new NsUser(2L, "sanjigi", "password", "name", "sanjigi@slipp.net"); -} diff --git a/src/test/java/nextstep/users/fixtures/NsUserFixtures.java b/src/test/java/nextstep/users/fixtures/NsUserFixtures.java new file mode 100644 index 0000000000..358e389c60 --- /dev/null +++ b/src/test/java/nextstep/users/fixtures/NsUserFixtures.java @@ -0,0 +1,11 @@ +package nextstep.users.fixtures; + +import nextstep.users.domain.NsUser; +import nextstep.users.domain.Type; + +public class NsUserFixtures { + public static final NsUser TEACHER_JAVAJIGI_1L = new NsUser(1L, "javajigi", "password", "name", "javajigi@slipp.net" , Type.TEACHER); + public static final NsUser TEACHER_SANJIGI_2L = new NsUser(2L, "sanjigi", "password", "name", "sanjigi@slipp.net",Type.TEACHER); + public static final NsUser TEACHER_APPLE_3L = new NsUser(3L, "apple", "password", "name", "apple@slipp.net",Type.TEACHER); + public static final NsUser STUDENT_ERIC_4L = new NsUser(4L, "apple", "password", "name", "apple@slipp.net",Type.STUDENT); +}