diff --git a/README.md b/README.md index 41a0b14f23..b82b599396 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # 학습 관리 시스템(Learning Management System) -## 질문 삭제하기 요구사항 +## 2단계 - 수강신청(도메인 모델) +### 수강 신청 기능 요구사항 +* 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다. +* 강의는 시작일과 종료일을 가진다. +* 강의는 강의 커버 이미지 정보를 가진다. +* 이미지 크기는 1MB 이하여야 한다. +* 이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다. +* 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다. +* 강의는 무료 강의와 유료 강의로 나뉜다. +* 무료 강의는 최대 수강 인원 제한이 없다. +* 유료 강의는 강의 최대 수강 인원을 초과할 수 없다. +* 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다. +* 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다. +* 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. +* 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. +* 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다. --- -* [x] 질문 데이터를 완전히 삭제하는 것이 아니라 데이터의 상태를 삭제 상태(deleted - boolean type)로 변경한다. -* [x] 로그인 사용자와 질문한 사람이 같은 경우 삭제 가능하다. -* [x] 답변이 없는 경우 삭제가 가능하다. -* [x] 답변을 삭제할 수 있다. -* [x] 질문자와 답변글의 모든 답변자가 같은 경우 삭제가 가능하다. -* [x] 질문을 삭제할 때 답변 또한 삭제해야 하며, 답변의 삭제 또한 삭제 상태(deleted)를 변경한다. -* [x] 질문과 답변 삭제 이력에 대한 정보를 DeleteHistory를 활용해 남긴다. ---- \ No newline at end of file diff --git a/gradlew.bat b/gradlew.bat index ac1b06f938..107acd32c4 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/Course.java index 0f69716043..165659d7fb 100644 --- a/src/main/java/nextstep/courses/domain/Course.java +++ b/src/main/java/nextstep/courses/domain/Course.java @@ -1,6 +1,7 @@ package nextstep.courses.domain; import java.time.LocalDateTime; +import java.util.List; public class Course { private Long id; @@ -13,9 +14,15 @@ public class Course { private LocalDateTime updatedAt; + private List sessions; + public Course() { } + public Course(List sessions) { + this.sessions = sessions; + } + public Course(String title, Long creatorId) { this(0L, title, creatorId, LocalDateTime.now(), null); } @@ -40,6 +47,10 @@ public LocalDateTime getCreatedAt() { return createdAt; } + public List getSessions() { + return sessions; + } + @Override public String toString() { return "Course{" + diff --git a/src/main/java/nextstep/courses/domain/Enrolment.java b/src/main/java/nextstep/courses/domain/Enrolment.java new file mode 100644 index 0000000000..f449fc0a78 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Enrolment.java @@ -0,0 +1,20 @@ +package nextstep.courses.domain; + +public class Enrolment { + private final Session session; + + public Enrolment(Session session) { + this.session = session; + } + + public void register() { + validateSessionStatus(); + session.addStudent(); + } + + private void validateSessionStatus() { + if (!session.isSessionStatus().isSessionResult()) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/FreeSession.java b/src/main/java/nextstep/courses/domain/FreeSession.java new file mode 100644 index 0000000000..f06bc61548 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/FreeSession.java @@ -0,0 +1,7 @@ +package nextstep.courses.domain; + +public class FreeSession extends Session { + public FreeSession(int studentCount, int tuition) { + super(studentCount, tuition); + } +} diff --git a/src/main/java/nextstep/courses/domain/Image.java b/src/main/java/nextstep/courses/domain/Image.java new file mode 100644 index 0000000000..4642bc52ec --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Image.java @@ -0,0 +1,51 @@ +package nextstep.courses.domain; + +public class Image { + private String type; + private int width; + private int height; + + public Image() { + } + + public Image(String type, int width, int height) { + validateImage(type, width, height); + this.type = type; + this.width = width; + this.height = height; + } + + public String getType() { + return type; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + private static void validateImage(String type, int width, int height) { + if (width * height > 1024 * 1024) { + throw new IllegalArgumentException("이미지 크기는 1MB 이하여야 한다."); + } + + if (!(type.contains("gif") || type.contains("jpg") || type.contains("jpeg") || type.contains("png") || type.contains("svg"))) { + throw new IllegalArgumentException("이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다."); + } + + if (width < 300) { + throw new IllegalArgumentException("이미지 넓이는 300px 이상이여야 한다."); + } + + if (height < 200) { + throw new IllegalArgumentException("이미지 높이는 200px 이상이여야 한다."); + } + + if (width / height != 3 / 2) { + throw new IllegalArgumentException("넓이와 높이의 비율은 3:2여야 한다."); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/PaidSession.java b/src/main/java/nextstep/courses/domain/PaidSession.java new file mode 100644 index 0000000000..b36f5c9ed8 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/PaidSession.java @@ -0,0 +1,32 @@ +package nextstep.courses.domain; + +public class PaidSession extends Session { + private static final int PAID_SESSION_TUITION_FEE = 10000; + private static final int MAX_SESSION_STUDENT_COUNT = 5; + + public PaidSession(int tuition) { + super(tuition); + validateTuitionFee(tuition); + } + + public PaidSession(SessionStatus sessionStatus) { + super(sessionStatus); + } + + public PaidSession(int studentCount, int tuition) { + super(studentCount, tuition); + validateStudentCount(studentCount); + } + + private void validateTuitionFee(int tuition) { + if (tuition != PAID_SESSION_TUITION_FEE) { + throw new IllegalArgumentException(); + } + } + + private void validateStudentCount(int studentCount) { + if (studentCount > MAX_SESSION_STUDENT_COUNT) { + throw new IllegalArgumentException(); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java new file mode 100644 index 0000000000..82ff03ad7a --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -0,0 +1,69 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class Session { + private int studentCount; + private int tuition; + private SessionStatus sessionStatus; + private Image image; + private LocalDateTime startDate; + private LocalDateTime endDate; + + public Session() { + } + + public Session(int tuition) { + this.tuition = tuition; + } + + public Session(Image image) { + this.image = image; + } + + public Session(SessionStatus sessionStatus) { + this.sessionStatus = sessionStatus; + } + + public Session(int studentCount, int tuition) { + this.studentCount = studentCount; + this.tuition = tuition; + } + + public Session(LocalDateTime startDate, LocalDateTime endDate) { + this.startDate = startDate; + this.endDate = endDate; + } + + public SessionStatus isSessionStatus() { + return sessionStatus; + } + + public void addStudent() { + studentCount++; + } + + public int getStudentCount() { + return studentCount; + } + + public int getTuition() { + return tuition; + } + + public SessionStatus getSessionStatus() { + return sessionStatus; + } + + public Image getImage() { + return image; + } + + public LocalDateTime getStartDate() { + return startDate; + } + + public LocalDateTime getEndDate() { + return endDate; + } +} diff --git a/src/main/java/nextstep/courses/domain/SessionStatus.java b/src/main/java/nextstep/courses/domain/SessionStatus.java new file mode 100644 index 0000000000..171b000f61 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/SessionStatus.java @@ -0,0 +1,23 @@ +package nextstep.courses.domain; + +public enum SessionStatus { + PREPARING("준비중", false), + RECRUITING("모집중", true), + CLOSE("종료", false); + + private final String sessionStatus; + private final boolean sessionResult; + + SessionStatus(String sessionStatus, boolean sessionResult) { + this.sessionStatus = sessionStatus; + this.sessionResult = sessionResult; + } + + public String getSessionStatus() { + return sessionStatus; + } + + public boolean isSessionResult() { + return sessionResult; + } +} diff --git a/src/test/java/nextstep/courses/domain/CourseTest.java b/src/test/java/nextstep/courses/domain/CourseTest.java new file mode 100644 index 0000000000..2ed95698f6 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/CourseTest.java @@ -0,0 +1,19 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class CourseTest { + + @Test + @DisplayName("과정(객체)는 여러 개의 강의(객체)를 가질 수 있다.") + void 과정_은_여러_개의_강의를_가질_수_있다() { + List sessions = List.of(new Session()); + Course course = new Course(sessions); + assertThat(course.getSessions()).isNotEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/EnrolmentTest.java b/src/test/java/nextstep/courses/domain/EnrolmentTest.java new file mode 100644 index 0000000000..6602ecce52 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/EnrolmentTest.java @@ -0,0 +1,46 @@ +package nextstep.courses.domain; + +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 EnrolmentTest { + + @Test + @DisplayName("강의 수강신청은 강의 상태가 준비중일 때 수강신청을 하면 예외가 발생한다.") + void 강의_수강신청은_강의_상태가_준비중일_때_수강신청을_하면_예외가_발생한다() { + Session session = new Session(SessionStatus.PREPARING); + Enrolment enrolment = new Enrolment(session); + assertThatThrownBy(enrolment::register) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("강의 수강신청은 강의 상태가 종료중일 때 수강신청을 하면 예외가 발생한다.") + void 강의_수강신청은_강의_상태가_종료중일_때_수강신청을_하면_예외가_발생한다() { + Session session = new Session(SessionStatus.CLOSE); + Enrolment enrolment = new Enrolment(session); + assertThatThrownBy(enrolment::register) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("강의 수강신청을 하면 강의 수강생 수가 1 증가한다.") + void 강의_수강신청을_하면_강의_수강생_수가_1_증가한다() { + Session session = new Session(SessionStatus.RECRUITING); + Enrolment enrolment = new Enrolment(session); + enrolment.register(); + assertThat(session.getStudentCount()).isEqualTo(1); + } + + @Test + @DisplayName("유료_강의 수강신청을 하면 강의 수강생 수가 1 증가한다.") + void 유료_강의_수강신청을_하면_강의_수강생_수가_1_증가한다() { + Session session = new PaidSession(SessionStatus.RECRUITING); + Enrolment enrolment = new Enrolment(session); + enrolment.register(); + assertThat(session.getStudentCount()).isEqualTo(1); + } +} diff --git a/src/test/java/nextstep/courses/domain/ImageTest.java b/src/test/java/nextstep/courses/domain/ImageTest.java new file mode 100644 index 0000000000..38e6d44c79 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/ImageTest.java @@ -0,0 +1,54 @@ +package nextstep.courses.domain; + +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 ImageTest { + + @Test + @DisplayName("이미지는 타입과 넓이와 높이를 가질 수 있다.") + void 이미지는_타입과_넓이와_높이를_가질_수_있다() { + Image image = new Image("png", 300, 200); + assertThat(image).hasFieldOrProperty("type"); + assertThat(image).hasFieldOrProperty("width"); + assertThat(image).hasFieldOrProperty("height"); + } + + @Test + @DisplayName("이미지 크기는 1MB 초과일 경우 예외가 발생한다.") + void 이미지_크기는_1MB_초과할_경우_예외가_발생한다() { + assertThatThrownBy(() -> new Image("png", 3000, 2000)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지 타입은 gif, jpg(jpeg 포함), png, svg만 허용한다.") + void 허용된_이미지가_아닐_경우_예외가_발생한다() { + assertThatThrownBy(() -> new Image("존윅", 300, 200)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지의 width는 300픽셀 이상이어야 한다.") + void 이미지의_width가_300px_미만일_경우_예외가_발생한다() { + assertThatThrownBy(() -> new Image("png", 299, 200)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지의 height는 200픽셀 이상이어야 한다.") + void 이미지의_height가_200px_미만일_경우_예외가_발생한다() { + assertThatThrownBy(() -> new Image("jpg", 300, 199)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("이미지의 width와 height의 비율은 3:2여야 한다.") + void 이미지의_width와_height의_비율이_3_대_2_아닐_경우_예외가_발생한다() { + assertThatThrownBy(() -> new Image("jpg", 500, 200)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/nextstep/courses/domain/PaidSessionTest.java b/src/test/java/nextstep/courses/domain/PaidSessionTest.java new file mode 100644 index 0000000000..4eeac6afa4 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/PaidSessionTest.java @@ -0,0 +1,23 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PaidSessionTest { + + @Test + @DisplayName("유료 강의는 최대 수강 인원 5명을 초과할 경우 예외를 발생한다.") + void 유료_강의는_최대_수강_인원_5명을_초과할_경우_예외를_발생한다() { + assertThatThrownBy(() -> new PaidSession(6, 1000)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("유료 강의는 수강생이 결제한 금액과 수강료가 다를 때 예외를 발생한다.") + void 유료_강의는_수강생이_결제한_금액과_수강료가_다를_경우_예외를_발생한다() { + assertThatThrownBy(() -> new PaidSession(10001)) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/SessionTest.java b/src/test/java/nextstep/courses/domain/SessionTest.java new file mode 100644 index 0000000000..14f385a035 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -0,0 +1,52 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SessionTest { + + @Test + @DisplayName("강의는 시작일과 종료일을 가진다.") + void 강의는_시작일과_종료일을_가진다() { + LocalDateTime startDate = LocalDateTime.of(2023, 11, 29, 9, 30); + LocalDateTime endDate = LocalDateTime.of(2023, 12, 29, 18, 30); + Session session = new Session(startDate, endDate); + + assertThat(session.getStartDate()).isBefore(endDate); + } + + @Test + @DisplayName("강의는 이미지를 가질 수 있다.") + void 강의는_이미지를_가질_수_있다() { + Session session = new Session(new Image("png", 300, 200)); + assertThat(session.getImage()).isNotNull(); + } + + @Test + @DisplayName("강의는 준비중 상태를 가진다.") + void 강의는_준비중_상태를_가질_수_있다() { + Session session = new Session(SessionStatus.PREPARING); + SessionStatus result = session.isSessionStatus(); + assertThat(result).isNotNull(); + } + + @Test + @DisplayName("강의는 모집중 상태를 가진다.") + void 강의는_모집중_상태를_가질_수_있다() { + Session session = new Session(SessionStatus.RECRUITING); + SessionStatus result = session.isSessionStatus(); + assertThat(result).isNotNull(); + } + + @Test + @DisplayName("강의는 종료 상태를 가진다.") + void 강의는_종료_상태를_가질_수_있다() { + Session session = new Session(SessionStatus.CLOSE); + SessionStatus result = session.isSessionStatus(); + assertThat(result).isNotNull(); + } +}