Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d0abcf2
docs: 수강 신청 기능 목록 정리
username0w Dec 12, 2025
ec941aa
feat: startDate/endDate 검증 구현
username0w Dec 12, 2025
5f35548
feat: 생성 시 기본 상태 PREPARING 설정
username0w Dec 12, 2025
b4b63b8
feat: 유료/무료 구분 및 최대 수강인원 검증 구현
username0w Dec 12, 2025
61d5b30
feat: 유료 강의 fee 필드 및 검증 추가
username0w Dec 12, 2025
0ed117d
feat: 상태 기반 수강 신청 가능 여부 구현
username0w Dec 12, 2025
5a09362
feat: 유료 강의 최대 수강 인원 초과 시 수강 불가 검증 추가
username0w Dec 12, 2025
f67bd43
feat: 수강 신청 기능 구현
username0w Dec 12, 2025
5901d8f
feat: SessionImage 생성 및 필수 검증 추가
username0w Dec 12, 2025
bce1fbd
test: SessionTestBuilder 추가 및 테스트에 적용
username0w Dec 12, 2025
3d04dca
feat: Session에 SessionImage 필드 추가 및 생성자 검증
username0w Dec 12, 2025
3d8f3a0
feat: 수강 기간, 수강 정원, 수강료 정보를 담는 VO 추가
username0w Dec 12, 2025
1de7c10
refactor: Session에 Period, Capacity, SessionPricing 적용
username0w Dec 13, 2025
3151975
feat: Session 관련 불변 정보 SessionInfo 추가
username0w Dec 13, 2025
35191a7
feat: EnrollmentPolicy 인터페이스 및 구현체 추가
username0w Dec 15, 2025
8340a2c
feat: SessionPricing을 Session으로 이동 and SessionInfo 필드 일부 외부 추출, Enrol…
username0w Dec 15, 2025
e834e6d
feat: ImageDimension 추가
username0w Dec 15, 2025
bbbc5b7
feat: ImageSize 추가
username0w Dec 15, 2025
23c29f0
feat: ImageSize, ImageDimension 적용
username0w Dec 15, 2025
61fd7b1
feat: FileName 추가
username0w Dec 15, 2025
cc4028c
feat: FileName 적용
username0w Dec 15, 2025
3d841cf
feat: Enrollment 추가
username0w Dec 15, 2025
9b6e440
feat: Enrollment, Enrollments 추가
username0w Dec 15, 2025
7766878
feat: Enrollment, Enrollments 적용, Capacity unlimited 필드 추가
username0w Dec 16, 2025
6e6c5fd
feat: 사용하지 않는 EnrollmentPolicy 제거
username0w Dec 16, 2025
ab7d29e
feat: 정적 팩토리 도입으로 불가능한 도메인 상태 생성 방지
username0w Dec 16, 2025
d4b5dd9
feat: Pricing과 Payment 비교 로직 이동
username0w Dec 16, 2025
61df420
refactor: Stream 사용, 예외 메시지 상수화
username0w Dec 16, 2025
b39dd60
refactor: Capacity에서 수강 인원 상태 제거
username0w Dec 21, 2025
f20a4b7
refactor: 수강신청 로직을 SessionEnrollment 객체로 분리
username0w Dec 22, 2025
a53b64e
refactor: sessions 패키지 분리
username0w Dec 22, 2025
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
53 changes: 46 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# 학습 관리 시스템(Learning Management System)

## 진행 방법

* 학습 관리 시스템의 수강신청 요구사항을 파악한다.
* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 온라인 코드 리뷰 과정

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

---
Expand All @@ -14,10 +17,46 @@

### 질문 삭제하기

- 질문자 = 로그인 사용자 아니면 삭제 불가
- 답변 중 다른 사람이 쓴 답변이 있으면 삭제 불가
- 답변이 없으면 삭제 가능
- 질문자와 답변자가 모두 동일하면 삭제 가능
- 삭제되면 질문 상태 변경
- 삭제되면 모든 답변 상태 변경
- 삭제 이력 생성됨
- 질문자 = 로그인 사용자 아니면 삭제 불가
- 답변 중 다른 사람이 쓴 답변이 있으면 삭제 불가
- 답변이 없으면 삭제 가능
- 질문자와 답변자가 모두 동일하면 삭제 가능
- 삭제되면 질문 상태 변경
- 삭제되면 모든 답변 상태 변경
- 삭제 이력 생성됨

### 수강 신청 기능

#### Course

- 기수 단위로 운영
- 여러 개의 Session(강의)을 가질 수 있음

#### Session (강의)

##### 속성

- 시작일(start_date), 종료일(end_date)
- 커버 이미지
- 최대 1MB
- 타입: gif, jpg/jpeg, png, svg
- 최소 사이즈: width 300px, height 200px
- 비율: 3:2
- 무료/유료 구분
- 최대 수강 인원 (유료 강의만 적용)
- 상태: 준비중, 모집중, 종료

##### 규칙

- 강의 상태가 `모집중`일 때만 수강 신청 가능
- 무료 강의는 수강 인원 제한 없음
- 유료 강의
- 최대 수강 인원을 초과할 수 없음
- 결제 금액과 수강료가 일치해야 수강 신청 가능
- 결제 정보는 `payments` 모듈의 Payment 객체를 통해 관리

#### Payment (결제)

- 유료 강의 결제 정보 관리
- Payment 객체 반환
- 결제 완료 여부 확인 후 수강 신청 가능
33 changes: 33 additions & 0 deletions src/main/java/nextstep/payments/domain/Payment.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package nextstep.payments.domain;

import java.time.LocalDateTime;
import java.util.Objects;
import nextstep.users.domain.NsUser;

public class Payment {
private String id;
Expand All @@ -26,4 +28,35 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) {
this.amount = amount;
this.createdAt = LocalDateTime.now();
}

public Long nsUserId() {
return nsUserId;
}

public boolean isPaidBy(NsUser user) {
return user.getId().equals(this.nsUserId);
}

public boolean isPaidFor(int fee) {
return this.amount == fee;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Payment payment = (Payment) o;
return Objects.equals(id, payment.id) && Objects.equals(sessionId, payment.sessionId)
&& Objects.equals(nsUserId, payment.nsUserId) && Objects.equals(amount, payment.amount)
&& Objects.equals(createdAt, payment.createdAt);
}

@Override
public int hashCode() {
return Objects.hash(id, sessionId, nsUserId, amount, createdAt);
}
}
25 changes: 25 additions & 0 deletions src/main/java/nextstep/sessions/domain/Period.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nextstep.sessions.domain;

import java.time.LocalDate;

public class Period {

private static final String ERROR_INVALID_DATE = "시작일이 종료일보다 빨라야 합니다";

private final LocalDate startDate;

private final LocalDate endDate;

public Period(LocalDate startDate, LocalDate endDate) {
validateDate(startDate, endDate);
this.startDate = startDate;
this.endDate = endDate;
}

private void validateDate(LocalDate startDate, LocalDate endDate) {
if (startDate.isAfter(endDate)) {
throw new IllegalArgumentException(ERROR_INVALID_DATE);
}
}

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

import java.time.LocalDate;
import nextstep.sessions.domain.enrollment.Enrollment;
import nextstep.sessions.domain.enrollment.SessionEnrollment;
import nextstep.sessions.domain.image.SessionImage;

public class 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의 필드 수가 많다.
Session의 기본 정보를 가지는 필드(현재 SessionInfo)와 수강신청에 관여하는 필드 값을 하나의 객체로 묶은 후 수강 신청 로직을 이 객체가 담당하도록 역할을 부여하는 것은 어떨까?


public static final String ERROR_SESSION_NOT_OPEN = "모집중인 강의만 수강 신청 가능합니다.";
public static final String ERROR_CAPACITY_EXCEEDED = "강의 정원이 초과되어 수강 신청할 수 없습니다.";
public static final String ERROR_PAYMENT_AMOUNT_MISMATCH = "결제 금액이 강의 수강료와 일치하지 않습니다.";

private Long id;

private SessionInfo sessionInfo;

private SessionEnrollment sessionEnrollment;

Session(Long id, SessionInfo sessionInfo, SessionEnrollment sessionEnrollment) {
this.id = id;
this.sessionInfo = sessionInfo;
this.sessionEnrollment = sessionEnrollment;
}

public static Session paidLimited(Long id, LocalDate startDate, LocalDate endDate, int fee, int maxCapacity,
SessionImage image) {
SessionInfo info = new SessionInfo(new Period(startDate, endDate), image);
SessionEnrollment enrollment = SessionEnrollment.paidLimited(fee, maxCapacity);
return new Session(id, info, enrollment);
}

public static Session freeUnlimited(Long id, LocalDate startDate, LocalDate endDate, SessionImage image) {
SessionInfo info = new SessionInfo(new Period(startDate, endDate), image);
SessionEnrollment enrollment = SessionEnrollment.freeUnlimited();
return new Session(id, info, enrollment);
}

public SessionStatus status() {
return sessionEnrollment.status();
}

public void startRecruiting() {
sessionEnrollment.startRecruiting();
}

public void enroll(Enrollment enrollment) {
sessionEnrollment.enroll(enrollment);
}

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

import nextstep.sessions.domain.image.SessionImage;

public class SessionInfo {

static final String ERROR_COVER_IMAGE_REQUIRED = "강의 커버 이미지는 필수입니다";

private final Period period;

private SessionImage image;

public SessionInfo(Period period, SessionImage image) {
validateImage(image);
this.period = period;
this.image = image;
}

private static void validateImage(SessionImage image) {
if (image == null) {
throw new IllegalArgumentException(ERROR_COVER_IMAGE_REQUIRED);
}
}

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

public class SessionPricing {

private static final String ERROR_PAID_FEE = "유료 강의는 0원 초과 여야 합니다";
private static final String ERROR_FREE_FEE = "무료 강의는 0원 이어야 합니다";

private final boolean isPaid;
private final int fee;

SessionPricing(boolean isPaid, int fee) {
validateFee(isPaid, fee);
this.isPaid = isPaid;
this.fee = fee;
}

public static SessionPricing paid(int fee) {
return new SessionPricing(true, fee);
}

public static SessionPricing free() {
return new SessionPricing(false, 0);
}

public boolean isPaid() {
return isPaid;
}

public int fee() {
return fee;
}

private void validateFee(boolean isPaid, int fee) {
if (isPaid && fee <= 0) {
throw new IllegalArgumentException(ERROR_PAID_FEE);
}
if (!isPaid && fee != 0) {
throw new IllegalArgumentException(ERROR_FREE_FEE);
}
}

}

7 changes: 7 additions & 0 deletions src/main/java/nextstep/sessions/domain/SessionStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nextstep.sessions.domain;

public enum SessionStatus {
PREPARING,
OPEN,
CLOSED
}
46 changes: 46 additions & 0 deletions src/main/java/nextstep/sessions/domain/enrollment/Capacity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package nextstep.sessions.domain.enrollment;

public class Capacity {

private static final String ERROR_MAX_CAPACITY_REQUIRED = "유료 강의는 최대 수강인원이 있어야 합니다";

private final Integer maxCapacity;
private final boolean unlimited;

Capacity(Integer maxCapacity, boolean unlimited) {
validateMaxCapacity(maxCapacity);
this.maxCapacity = maxCapacity;
this.unlimited = unlimited;
}

public static Capacity limited(int maxCapacity) {
return new Capacity(maxCapacity, false);
}

public static Capacity unlimited() {
return new Capacity(Integer.MAX_VALUE, true);
}

public Integer maxCapacity() {
return maxCapacity;
}

public boolean canEnroll(int enrollmentsSize) {
return unlimited || enrollmentsSize < maxCapacity;
}

public boolean isUnlimited() {
return unlimited;
}

public boolean isFull(int enrollmentsSize) {
return enrollmentsSize >= maxCapacity;
}

private void validateMaxCapacity(Integer maxCapacity) {
if (!unlimited && (maxCapacity == null || maxCapacity <= 0)) {
throw new IllegalArgumentException(ERROR_MAX_CAPACITY_REQUIRED);
}
}

}
57 changes: 57 additions & 0 deletions src/main/java/nextstep/sessions/domain/enrollment/Enrollment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package nextstep.sessions.domain.enrollment;

import java.time.LocalDateTime;
import java.util.Objects;
import nextstep.payments.domain.Payment;
import nextstep.sessions.domain.SessionPricing;
import nextstep.users.domain.NsUser;

public class Enrollment {

static final String ERROR_USER_PAYMENT_MISMATCH = "결제한 사용자와 신청자가 일치하지 않습니다";

private final NsUser user;
private final Payment payment;
private final LocalDateTime enrolledAt;

public Enrollment(NsUser user, Payment payment) {
validateUserPayMatch(user, payment);
this.user = user;
this.payment = payment;
this.enrolledAt = LocalDateTime.now();
}

public Payment payment() {
return payment;
}

public boolean canPayFor(SessionPricing pricing) {
return !pricing.isPaid() || payment.isPaidFor(pricing.fee());
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Enrollment that = (Enrollment) o;
return Objects.equals(user, that.user) && Objects.equals(payment, that.payment)
&& Objects.equals(enrolledAt, that.enrolledAt);
}

@Override
public int hashCode() {
return Objects.hash(user, payment, enrolledAt);
}

private static void validateUserPayMatch(NsUser user, Payment payment) {
if (payment.isPaidBy(user)) {
return;
}
throw new IllegalArgumentException(ERROR_USER_PAYMENT_MISMATCH);
}

}
23 changes: 23 additions & 0 deletions src/main/java/nextstep/sessions/domain/enrollment/Enrollments.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.sessions.domain.enrollment;

import java.util.HashSet;
import java.util.Set;

public class Enrollments {

static final String ERROR_ALREADY_ENROLLED = "이미 등록된 사용자입니다.";

private final Set<Enrollment> enrollments = new HashSet<>();

public void add(Enrollment enrollment) {
if (enrollments.contains(enrollment)) {
throw new IllegalArgumentException(ERROR_ALREADY_ENROLLED);
}
enrollments.add(enrollment);
}

public int size() {
return enrollments.size();
}
}

Loading