Skip to content
Merged
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,25 @@
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)

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

## 프로그래밍 요구사항
- DB 테이블 설계 없이 도메인 모델부터 구현한다.
- 도메인 모델은 TDD로 구현한다.
- 단, Service 클래스는 단위 테스트가 없어도 된다.
7 changes: 7 additions & 0 deletions src/main/java/nextstep/courses/InvalidImageFileException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nextstep.courses;

public class InvalidImageFileException extends RuntimeException {
public InvalidImageFileException(String message) {
super(message);
}
}
10 changes: 10 additions & 0 deletions src/main/java/nextstep/courses/SessionUnregistrableException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nextstep.courses;

public class SessionUnregistrableException extends RuntimeException{
public SessionUnregistrableException(){
}

public SessionUnregistrableException(String message) {
super(message);
}
}
21 changes: 21 additions & 0 deletions src/main/java/nextstep/courses/domain/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package nextstep.courses.domain;

import nextstep.courses.domain.session.Period;

import java.time.LocalDate;

public class BaseEntity {
private final Long id;
private final String title;
private final Period period;

public BaseEntity(Long id, String title, LocalDate startDate, LocalDate endDate) {
this(id, title, new Period(startDate, endDate));
}

public BaseEntity(Long id, String title, Period period) {
this.id = id;
this.title = title;
this.period = period;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package nextstep.courses.domain;
package nextstep.courses.domain.course;

import java.time.LocalDateTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package nextstep.courses.domain;
package nextstep.courses.domain.course;

public interface CourseRepository {
int save(Course course);
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/nextstep/courses/domain/image/Image.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package nextstep.courses.domain.image;

public class Image {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

private final ImageSize size;
private final ImageDimension imageDimension;
private final ImageFileExtension fileExtension;

public Image(int size, int width, int height, String fileExtension) {
this.size = new ImageSize(size);
this.imageDimension = new ImageDimension(width, height);
this.fileExtension = ImageFileExtension.from(fileExtension);
}
}
28 changes: 28 additions & 0 deletions src/main/java/nextstep/courses/domain/image/ImageDimension.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nextstep.courses.domain.image;

import nextstep.courses.InvalidImageFileException;

public class ImageDimension {
private final static int MIN_WIDTH = 300; // 300 px
private final static int MIN_HEIGHT = 200; // 200 px
private final static double ASPECT_RATIO = 1.5; // width : height = 3 : 2

private final int width;
private final int height;

public ImageDimension(int width, int height) {
if (!isValidWidthAndHeight(width, height)) {
throw new InvalidImageFileException(String.format("이미지는 가로 %spx 이상, 세로 %spx 이상, 가로 세로 비율이 %s:1 이어야 합니다.", MIN_WIDTH, MIN_HEIGHT, ASPECT_RATIO));
}
this.width = width;
this.height = height;
}

private boolean isValidWidthAndHeight(int width, int height) {
return width >= MIN_WIDTH && height >= MIN_HEIGHT && isValidRatio(width, height);
}

private boolean isValidRatio(int width, int height) {
return (double) width / height == ASPECT_RATIO;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package nextstep.courses.domain.image;

import nextstep.courses.InvalidImageFileException;

import java.util.Arrays;
import java.util.stream.Collectors;

public enum ImageFileExtension {
GIF("gif"),
JPG("jpg"),
JPEG("jpeg"),
PNG("png"),
SVG("svg");

private final String value;

ImageFileExtension(String value) {
this.value = value;
}

private String value() {
return value;
}

static ImageFileExtension from(String value) {
return Arrays.stream(ImageFileExtension.values())
.filter(fileExtension -> fileExtension.value.equals(value))
.findFirst()
.orElseThrow(() -> new InvalidImageFileException(String.format("이미지 파일은 %s 중에 하나의 형태여야 합니다.", possibleFileExtensions())));
}

private static String possibleFileExtensions() {
return Arrays.stream(ImageFileExtension.values())
.map(ImageFileExtension::value)
.collect(Collectors.joining(", "));
}
}
19 changes: 19 additions & 0 deletions src/main/java/nextstep/courses/domain/image/ImageSize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package nextstep.courses.domain.image;

import nextstep.courses.InvalidImageFileException;

class ImageSize {
private final static int MAX_SIZE = 1_024; // 1024 Byte
private final int size;

ImageSize(int size) {
if (!isValidSize(size)) {
throw new InvalidImageFileException(String.format("이미지 사이즈는 %sB 이하여야 합니다.", MAX_SIZE));
}
this.size = size;
}

private boolean isValidSize(int size) {
return size <= MAX_SIZE;
}
}
22 changes: 22 additions & 0 deletions src/main/java/nextstep/courses/domain/session/Capacity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package nextstep.courses.domain.session;

public class Capacity {
private final static int UNLIMITED = -1;
private final int capacity;

public Capacity() {
this(UNLIMITED);
}

public Capacity(int capacity) {
this.capacity = capacity;
}

public boolean isUnlimited() {
return capacity == UNLIMITED;
}

public boolean isGreaterThan(int value) {
return capacity > value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nextstep.courses.domain.session;

import nextstep.payments.domain.Payment;
import nextstep.users.domain.NsUser;

/**
* 개인별 수강 신청 가능 조건 값을 관리하는 도메인
*/
public class EnrollmentCondition {
private final NsUser user;
private final Payment payment;

public EnrollmentCondition(NsUser user, Payment payment) {
if (!paidBySameUser(user, payment)) {
throw new IllegalArgumentException("수강 신청자와 결제자 정보가 불일치합니다.");
}
this.user = user;
this.payment = payment;
}

private boolean paidBySameUser(NsUser user, Payment payment) {
return payment.isPaidBy(user.getId());
}

public NsUser getUser() {
return user;
}

public boolean hasPaid(Long sessionId, long price) {
return payment.isPaidFor(sessionId) && payment.isSameAmount(price);
}
}
13 changes: 13 additions & 0 deletions src/main/java/nextstep/courses/domain/session/Period.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package nextstep.courses.domain.session;

import java.time.LocalDate;

public class Period {
private final LocalDate startDate;
private final LocalDate endDate;

public Period(LocalDate startDate, LocalDate endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
}
52 changes: 52 additions & 0 deletions src/main/java/nextstep/courses/domain/session/Session.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package nextstep.courses.domain.session;

import nextstep.courses.SessionUnregistrableException;
import nextstep.courses.domain.*;
import nextstep.courses.domain.image.Image;
import nextstep.courses.domain.session.type.Free;
import nextstep.courses.domain.session.type.Paid;
import nextstep.courses.domain.session.type.SessionType;

import java.time.LocalDate;

public class Session {
private final BaseEntity baseEntity;
private final Image image;
private SessionStatus status;
private final SessionType type;

public Session(Long id, String title, Image image, SessionStatus status, SessionType type, LocalDate startDate, LocalDate endDate) {
this.baseEntity = new BaseEntity(id, title, new Period(startDate, endDate));
this.image = image;
this.status = status;
this.type = type;
}

public void openRecruiting() {
this.status = SessionStatus.RECRUITING;
}

public boolean isRecruiting() {
return this.status == SessionStatus.RECRUITING;
}

public boolean canEnroll(EnrollmentCondition request) {
return this.type.canEnroll(request);
}

public String currentStatusToHumanReadable() {
return this.status.name();
}

public String typeToHumanReadable() {
return this.type.toHumanReadableTypeName();
}

public static Session createFreeSession(String title, Image image) {
return new Session(null, title, image, SessionStatus.PREPARING, new Free(), LocalDate.now(), LocalDate.MAX);
}

public static Session createPaidSession(long id, String title, Image image, long price) {
return new Session(null, title, image, SessionStatus.PREPARING, new Paid(id, price), LocalDate.now(), LocalDate.MAX);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package nextstep.courses.domain.session;

import nextstep.courses.SessionUnregistrableException;
import nextstep.users.domain.NsUsers;

/**
* 강의 정원 및 현재 등록 인원을 관리하는 도메인
*/
public class SessionEnrollment {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

private final Capacity maxCapacity;
private final NsUsers enrolledUsers;
private final Session session;
Copy link
Contributor

Choose a reason for hiding this comment

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

Session과 의존관계를 가지기보다 수강 신청 가능 여부를 판단할 때 필요한 아래 두 값을 가지도록 구현하는 것은 어떨까?
private SessionStatus status;
private final SessionType type;

Session에 의존할 경우 테스트 코드 구현할 때 Session 객체를 생성하는 등의 번거로운 작업도 많을 것으로 보여짐


public SessionEnrollment(Session session) {
this(new Capacity(), new NsUsers(), session);
}

public SessionEnrollment(int maxCapacity, Session session) {
this(new Capacity(maxCapacity), new NsUsers(), session);
}

public SessionEnrollment(Capacity maxCapacity, NsUsers enrolledUsers, Session session) {
this.maxCapacity = maxCapacity;
this.enrolledUsers = enrolledUsers;
this.session = session;
}

public void enroll(EnrollmentCondition condition) {
checkEnrollable(condition);
this.enrolledUsers.addUser(condition.getUser());
}

private void checkEnrollable(EnrollmentCondition condition) {
if (!session.isRecruiting()) {
throw new SessionUnregistrableException(String.format("%s 상태인 강의는 수강신청할 수 없습니다.", session.currentStatusToHumanReadable()));
}

if (!session.canEnroll(condition)) {
throw new SessionUnregistrableException(String.format("%s 강의 수강 신청 조건 미달로 신청할 수 없습니다.", session.typeToHumanReadable()));
}

if (!hasCapacity()) {
throw new SessionUnregistrableException("정원 초과로 수강신청할 수 없습니다.");
}
}

private boolean hasCapacity() {
if (maxCapacity.isUnlimited()) {
return true;
}
return enrolledUsers.isLessThan(maxCapacity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package nextstep.courses.domain.session;

public enum SessionStatus {
PREPARING, RECRUITING, COMPLETED
}
18 changes: 18 additions & 0 deletions src/main/java/nextstep/courses/domain/session/type/Free.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package nextstep.courses.domain.session.type;

import nextstep.courses.domain.session.EnrollmentCondition;

public class Free implements SessionType {
public Free() {
}

@Override
public boolean canEnroll(EnrollmentCondition request) {
return true;
}

@Override
public String toHumanReadableTypeName() {
return "무료";
}
}
Loading