Skip to content

feat : course, session 로직 구현 #745

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: jhm9595
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,26 @@
* nextstep.qna.service.QnaService의 deleteQuestion()는 앞의 질문 삭제 기능을 구현한 코드이다. 이 메소드는 단위 테스트하기 어려운 코드와 단위 테스트 가능한 코드가 섞여 있다.
* QnaService의 deleteQuestion() 메서드에 단위 테스트 가능한 코드(핵심 비지니스 로직)를 도메인 모델 객체에 구현한다.
* QnaService의 비지니스 로직을 도메인 모델로 이동하는 리팩터링을 진행할 때 TDD로 구현한다.
* QnaService의 deleteQuestion() 메서드에 대한 단위 테스트는 src/test/java 폴더 nextstep.qna.service.QnaServiceTest이다. 도메인 모델로 로직을 이동한 후에도 QnaServiceTest의 모든 테스트는 통과해야 한다.
* QnaService의 deleteQuestion() 메서드에 대한 단위 테스트는 src/test/java 폴더 nextstep.qna.service.QnaServiceTest이다. 도메인 모델로 로직을 이동한 후에도 QnaServiceTest의 모든 테스트는 통과해야 한다.

## 수강 신청 기능 요구사항
* 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다.
* 강의는 시작일과 종료일을 가진다.
* 강의는 강의 커버 이미지 정보를 가진다.
* 이미지 크기는 1MB 이하여야 한다.
* 이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다.
* 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다.
* 강의는 무료 강의와 유료 강의로 나뉜다.
* 무료 강의는 최대 수강 인원 제한이 없다.
* 유료 강의는 강의 최대 수강 인원을 초과할 수 없다.
* 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다.
* 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다.
* 강의 수강신청은 강의 상태가 모집중일 때만 가능하다.
* 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. (???)
* 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다.

## 프로그래밍 요구사항
* DB 테이블 설계 없이 도메인 모델부터 구현한다.
* 도메인 모델은 TDD로 구현한다.
* 단, Service 클래스는 단위 테스트가 없어도 된다.
* 다음 동영상을 참고해 DB 테이블보다 도메인 모델을 먼저 설계하고 구현한다.
5 changes: 5 additions & 0 deletions src/main/java/nextstep/courses/domain/Conditional.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nextstep.courses.domain;

public interface Conditional {
public boolean test(int currentNum);
}
Comment on lines +3 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FunctionalInterface 어노테이션을 명세하는 것과 하지 않는 것에는 어떤 차이가 있을까요?

17 changes: 13 additions & 4 deletions src/main/java/nextstep/courses/domain/Course.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@

public class Course {
private Long id;

private String title;

private Long creatorId;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;

private int classNo;
private Sessions sessions;

public int getClassNo() {
return classNo;
}

public Sessions getSessions() {
return this.sessions;
}
Comment on lines +12 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다.

요구사항 속 기수는 하나의 과정에서 생성되는 N 개의 강의를 나타내는 걸로 보이네요. 😄

  • 과정: TDD, 클린 코드 with Java
  • 강의: TDD, 클린 코드 with Java 19기
  • 강의: TDD, 클린 코드 with Java 18기
  • 강의: TDD, 클린 코드 with Java 17기


public Course() {
classNo = 0;
sessions = new Sessions();
}

public Course(String title, Long creatorId) {
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/nextstep/courses/domain/CoverImage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package nextstep.courses.domain;

public class CoverImage {

private int width;
private int height;
private ImageType imageType;
private long size;

CoverImage(){

}
Comment on lines +10 to +12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기본 생성자를 default package private 접근제한자로 만드신 이유가 있을까요?


public CoverImage(String imageType, long size, int width, int height) {
if (size > 1_000) {
throw new IllegalArgumentException("이미지 크기는 1MB 이하여야 합니다.");
}
if (width < 300) {
throw new IllegalArgumentException("Width는 300픽셀 이상이여야 합니다.");
}
if (height < 200) {
throw new IllegalArgumentException("Height는 200픽셀 이상이여야 합니다.");
}
if ((double) width / (double) height < 1.5) {
throw new IllegalArgumentException("Width와 Height의 비율은 3:2여야 합니다.");
}
this.size = size;
this.width = width;
this.height = height;
this.imageType = ImageType.getCoverImageType(imageType);
}

public long getSize() {
return size;
}

public ImageType getImageType() {
return this.imageType;
}
}
35 changes: 35 additions & 0 deletions src/main/java/nextstep/courses/domain/ImageType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package nextstep.courses.domain;

import java.util.Arrays;

public enum ImageType {

GIF("GIF"),
JPG("JPG"),
JPEG("JPEG"),
PNG("PNG"),
SVG("SVG");

Comment on lines +5 to +12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수의 나열을 enum 으로 풀어내기 💯 👍

private String imageType;

ImageType(String imageType) {
this.imageType = imageType;
}

public static ImageType getCoverImageType(String imageType) {
imageType = imageType.toUpperCase();
isCoverImageType(imageType);
return valueOf(imageType);
}

public String getImageType() {
return imageType;
}

public static boolean isCoverImageType(String imageType) {
return Arrays.stream(values())
.anyMatch(type -> type.getImageType().equals(imageType.toUpperCase()));
}


}
85 changes: 85 additions & 0 deletions src/main/java/nextstep/courses/domain/Session.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package nextstep.courses.domain;

import nextstep.users.domain.NsUser;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public class Session {

private final CoverImage coverImage;
private final LocalDate startDt;
private final LocalDate endDt;

private int capacity;
private Conditional conditional;
private BigDecimal cost;
private SessionStatus status;
private List<NsUser> students;

public Session() {
this(new CoverImage(), LocalDate.now(), LocalDate.now());
}

public Session(CoverImage coverImage, LocalDate startDt, LocalDate endDt) {
this.coverImage = coverImage;
this.startDt = startDt;
this.endDt = endDt;
this.status = SessionStatus.READY;
this.students = new ArrayList<>();
}

Comment on lines +22 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자 체이닝 활용 👍

public void asFree() {
this.capacity = Integer.MAX_VALUE;
this.cost = BigDecimal.ZERO;
conditional = (currentNum) -> true;
}

public void asPaid(int capacity, BigDecimal cost) {
this.capacity = capacity;
this.cost = cost;
conditional = (currentNum) -> currentNum < this.capacity;
Comment on lines +34 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음 Session 이 생성될 때부터 유/무료가 나뉠 수 있도록 팩터리 메서드를 제공하는 것도 방법일거 같네요 😄

}

public boolean isAvailable() {
return conditional.test(students.size());
}

public boolean isExactCost(BigDecimal cost) {
return cost.compareTo(this.cost) == 0;
}

public CoverImage getCoverImage() {
return this.coverImage;
}

public LocalDate getStartDt() {
return startDt;
}

public LocalDate getEndDt() {
return endDt;
}

public SessionStatus getSessionStatus() {
return status;
}

public void apply(NsUser nsUser) {
SessionStatus.isReady(status);

if (this.isAvailable()) {
students.add(nsUser);
}
}

public void open() {
status = SessionStatus.open();
}

public void close() {
status = SessionStatus.close();
}
}
19 changes: 19 additions & 0 deletions src/main/java/nextstep/courses/domain/SessionStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nextstep.courses.domain;

public enum SessionStatus {
READY, OPEN, CLOSE;

public static void isReady(SessionStatus status) {
if(!status.equals(READY)){
throw new IllegalArgumentException("강의 수강신청은 강의 상태가 모집중일 때만 가능합니다");
}
}

public static SessionStatus open(){
return OPEN;
}

public static SessionStatus close(){
return CLOSE;
}
}
16 changes: 16 additions & 0 deletions src/main/java/nextstep/courses/domain/Sessions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package nextstep.courses.domain;

import java.util.ArrayList;
import java.util.List;

public class Sessions {
private List<Session> sessions;

public Sessions() {
sessions = new ArrayList<>();
}

public List<Session> getSessions() {
return sessions;
}
}
11 changes: 10 additions & 1 deletion src/main/java/nextstep/qna/domain/Question.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package nextstep.qna.domain;

import nextstep.qna.CannotDeleteException;
import nextstep.users.domain.NsUser;

import java.time.LocalDateTime;
Expand Down Expand Up @@ -90,7 +91,12 @@ public boolean hasOthersAnswers() {
return answers.hasOthersAnswers(writer);
}

public List<DeleteHistory> delete() {
public List<DeleteHistory> delete() throws CannotDeleteException {

if(this.hasOthersAnswers()){
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.");
}

List<DeleteHistory> deleteHistories = new ArrayList<>();
deleteHistories.add(new DeleteHistory(this));
if(!answers.isEmpty()) {
Expand All @@ -100,4 +106,7 @@ public List<DeleteHistory> delete() {
return deleteHistories;
}

public boolean isNotOwner(NsUser loginUser) {
return !this.isOwner(loginUser);
}
Comment on lines +109 to +111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부정형 질의문 제공 👍 👍

}
6 changes: 1 addition & 5 deletions src/main/java/nextstep/qna/service/QnAService.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,10 @@ public void deleteQuestion(NsUser loginUser, long questionId) throws CannotDelet

Question question = questionRepository.findById(questionId).orElseThrow(NotFoundException::new);

if (!question.isOwner(loginUser)) {
if (question.isNotOwner(loginUser)) {
throw new CannotDeleteException("질문을 삭제할 권한이 없습니다.");
}

if(question.hasOthersAnswers()){
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.");
}

deleteHistoryService.saveAll(question.delete());
}
}
22 changes: 22 additions & 0 deletions src/test/java/nextstep/courses/domain/CourseTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package nextstep.courses.domain;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.*;

class CourseTest {

Course course = new Course();

@Test
void 기수를_가진다() {
assertThat(course.getClassNo()).isNotNull();
}

@Test
void 여러개의_강의를_가진다() {
assertThat(course.getSessions()).isNotNull();
}
}
55 changes: 55 additions & 0 deletions src/test/java/nextstep/courses/domain/CoverImageTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package nextstep.courses.domain;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

class CoverImageTest {

@Test
void 이미지_크기는_1MB_이하() {
assertThat(new CoverImage("jpg", 1000, 300, 200).getSize()).isEqualTo(1000);
}

@Test
void 이미지_크기_1MB_초과() {
assertThatThrownBy(() -> {
new CoverImage("jpg", 1100, 300, 200);
}).isInstanceOf(IllegalArgumentException.class);
}

@Test
void 이미지_타입은_peg_입력_에러() {
assertThatThrownBy(() -> {
new CoverImage("peg", 300, 300, 200);
}).isInstanceOf(IllegalArgumentException.class);
}

@Test
void 이미지_타입은_jpg_타입_성공() {
assertThat(new CoverImage("jpg", 300, 300, 200).getImageType()).isEqualTo(ImageType.JPG);
}

@Test
void width_300_이상_height_200_이상() {
assertThat(new CoverImage("jpg", 300, 300, 200)).isNotNull();
}

@Test
void width_300_이하() {
assertThatThrownBy(() -> new CoverImage("jpg", 300, 299, 200))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void width_200_미만() {
assertThatThrownBy(() -> new CoverImage("jpg", 300, 300, 199))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void 비율이_3대2가_아닐때() {
assertThatThrownBy(() -> new CoverImage("jpg", 300, 300, 300))
.isInstanceOf(IllegalArgumentException.class);
}
}
Loading