From 330def7e1c696380162f4fda1ac559720870c311 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 21:01:20 +0900 Subject: [PATCH 01/37] =?UTF-8?q?docs=20step1=20=EC=9A=94=EA=B5=AC?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step1.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/step1.md diff --git a/docs/step1.md b/docs/step1.md new file mode 100644 index 0000000000..d042a9e7c0 --- /dev/null +++ b/docs/step1.md @@ -0,0 +1,15 @@ +## 요구사항 + +- 질문 데이터를 완전히 삭제하는 것이 아니라 데이터의 상태를 삭제 상태(deleted - boolean type)로 변경한다. +- 로그인 상요자와 질문한 사람이 같은 경우 삭제 가능하다. +- 답변이 없는 경우 삭제가 가능하다. +- 질문자와 답변 글의 모든 답변자가 같은 경우 삭제가 가능하다. +- 질문자를 삭제할 때 답변 또한 삭제해야 하며, 답변의 삭제 또한 삭제 상태(deleted)를 변경한다. +- 질문자와 답변자가 다른 경우 답변을 삭제할 수 없다. +- 질문과 답변 삭제 이력에 대한 정보를 DeleteHistory를 활용해 넘긴다. + +## 리팩터링 요구사항 +- nextstep.qna.service.QnaService의 deleteQuestion()는 앞의 질문 삭제 기능을 구현한 코드이다. 이 메소드는 단위 테스트하기 어려운 코드와 단위 테스트 가능한 코드가 섞여 있다. +- QnaService의 deleteQuestion() 메서드에 단위 테스트 가능한 코드(핵심 비지니스 로직)를 도메인 모델 객체에 구현한다. +- QnaService의 비지니스 로직을 도메인 모델로 이동하는 리팩터링을 진행할 때 TDD로 구현한다. +- QnaService의 deleteQuestion() 메서드에 대한 단위 테스트는 src/test/java 폴더 nextstep.qna.service.QnaServiceTest이다. 도메인 모델로 로직을 이동한 후에도 QnaServiceTest의 모든 테스트는 통과해야 한다. \ No newline at end of file From 45603af6c519c819d5b30b3d0ff2beacd1bc3d46 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 21:16:22 +0900 Subject: [PATCH 02/37] =?UTF-8?q?test=20=EC=A7=88=EB=AC=B8=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/qna/domain/QuestionTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 3b87823963..15f57d1ceb 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -1,8 +1,23 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUserTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; 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"); + + @Test + void 질문_작성자가_자신일_경우_예외가_발생하지_않는다() throws CannotDeleteException { + Q1.validateOwnership(NsUserTest.JAVAJIGI); + } + + @Test + void 질문_작성자가_타인일_경우_예외가_발생한다() { + Assertions.assertThatThrownBy(() -> Q1.validateOwnership(NsUserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("질문을 삭제할 권한이 없습니다."); + } } From 7bc74cad52dcf2daaa8bdf0b3c338433be6de360 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 21:17:29 +0900 Subject: [PATCH 03/37] =?UTF-8?q?refactor=20=EC=A7=88=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9E=90=EA=B0=80=20=EB=B3=B8=EC=9D=B8?= =?UTF-8?q?=EC=9D=B8=EC=A7=80=20=EA=B2=80=EC=82=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=A7=88=EB=AC=B8=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/qna/domain/Question.java | 9 ++++++++- src/main/java/nextstep/qna/service/QnAService.java | 4 +--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index b623c52c76..9c0b5bcb8a 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -1,5 +1,6 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUser; import java.time.LocalDateTime; @@ -37,6 +38,12 @@ public Question(Long id, NsUser writer, String title, String contents) { this.contents = contents; } + public void validateOwnership(NsUser loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } + } + public Long getId() { return id; } @@ -68,7 +75,7 @@ public void addAnswer(Answer answer) { answers.add(answer); } - public boolean isOwner(NsUser loginUser) { + private boolean isOwner(NsUser loginUser) { return writer.equals(loginUser); } diff --git a/src/main/java/nextstep/qna/service/QnAService.java b/src/main/java/nextstep/qna/service/QnAService.java index 5741c84d65..4bf1cb6e63 100644 --- a/src/main/java/nextstep/qna/service/QnAService.java +++ b/src/main/java/nextstep/qna/service/QnAService.java @@ -26,9 +26,7 @@ public class QnAService { @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("질문을 삭제할 권한이 없습니다."); - } + question.validateOwnership(loginUser); List answers = question.getAnswers(); for (Answer answer : answers) { From 90f69fa818daa90d07b3db712eec8b8796155a4f Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:33:36 +0900 Subject: [PATCH 04/37] =?UTF-8?q?test=20=EB=8B=B5=EB=B3=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/nextstep/qna/domain/AnswerTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/nextstep/qna/domain/AnswerTest.java b/src/test/java/nextstep/qna/domain/AnswerTest.java index 8e80ffb429..6558877335 100644 --- a/src/test/java/nextstep/qna/domain/AnswerTest.java +++ b/src/test/java/nextstep/qna/domain/AnswerTest.java @@ -1,8 +1,23 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUserTest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; 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"); + + @Test + void 답변_작성자가_자신일_경우_예외가_발생하지_않는다() throws CannotDeleteException { + A1.validateOwnership(NsUserTest.JAVAJIGI); + } + + @Test + void 답변_작성자가_타인일_경우_예외가_발생한다() { + Assertions.assertThatThrownBy(() -> A1.validateOwnership(NsUserTest.SANJIGI)) + .isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } } From 1434fba01cffb533abd3937af812f9a68feb5b4e Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:35:18 +0900 Subject: [PATCH 05/37] =?UTF-8?q?refactor=20=EB=8B=B5=EB=B3=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=9E=90=EB=A5=BC=20=EA=B2=80=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20Answer=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/qna/domain/Answer.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index cf681811e7..d6f8f262c5 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; @@ -43,6 +44,12 @@ public Answer(Long id, NsUser writer, Question question, String contents) { this.contents = contents; } + public void validateOwnership(NsUser loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + } + public Long getId() { return id; } From 2104de2f455bfc0743d5abedefe6d7d237bf5555 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:36:24 +0900 Subject: [PATCH 06/37] =?UTF-8?q?feat=20Answer=EC=9D=98=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=83=81=ED=83=9C=EB=A5=BC=20true=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B3=A0=20DeleteHistory=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/qna/domain/Answer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index d6f8f262c5..624e4d5519 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -50,6 +50,11 @@ public void validateOwnership(NsUser loginUser) throws CannotDeleteException { } } + public DeleteHistory delete() { + deleted = true; + return DeleteHistory.ofAnswer(this); + } + public Long getId() { return id; } From 8c94a3d99ac18eb0317e6a7f67a4b3f911a94bd6 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:36:56 +0900 Subject: [PATCH 07/37] =?UTF-8?q?refactor=20Answer=20=EC=9D=BC=EA=B8=89?= =?UTF-8?q?=EC=BB=AC=EB=A0=89=EC=85=98=20Answers=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20answers=20=EA=B4=80=EB=A0=A8=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/qna/domain/Answers.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/nextstep/qna/domain/Answers.java 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..7a0c46fe43 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/Answers.java @@ -0,0 +1,32 @@ +package nextstep.qna.domain; + +import nextstep.qna.CannotDeleteException; +import nextstep.users.domain.NsUser; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class Answers { + private final List values; + + public Answers() { + this.values = new ArrayList<>(); + } + + public void add(Answer answer) { + values.add(answer); + } + + public void validateOwnerships(NsUser loginUser) throws CannotDeleteException { + for (Answer answer : values) { + answer.validateOwnership(loginUser); + } + } + + public List deleteAnswers() { + return values.stream() + .map(Answer::delete) + .collect(Collectors.toList()); + } +} From 14635ef1c2eec0875f260cca66f8ab920053b373 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:37:22 +0900 Subject: [PATCH 08/37] =?UTF-8?q?feat=20DeleteHistory=EC=97=90=EC=84=9C=20?= =?UTF-8?q?answer=EC=99=80=20question=EC=9D=84=20=EB=B0=9B=EC=95=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/qna/domain/DeleteHistory.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/nextstep/qna/domain/DeleteHistory.java b/src/main/java/nextstep/qna/domain/DeleteHistory.java index 43c37e5e5c..c6169d327f 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistory.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistory.java @@ -19,6 +19,14 @@ public class DeleteHistory { public DeleteHistory() { } + public static DeleteHistory ofQuestion(Question q) { + return new DeleteHistory(ContentType.QUESTION, q.getId(), q.getWriter(), LocalDateTime.now()); + } + + public static DeleteHistory ofAnswer(Answer a) { + return new DeleteHistory(ContentType.ANSWER, a.getId(), a.getWriter(), LocalDateTime.now()); + } + public DeleteHistory(ContentType contentType, Long contentId, NsUser deletedBy, LocalDateTime createdDate) { this.contentType = contentType; this.contentId = contentId; From 163d9030acdd3eb4b6ac10098625d5e5f2a68976 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:38:10 +0900 Subject: [PATCH 09/37] =?UTF-8?q?test=20=EC=A7=88=EB=AC=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=9D=98=20=EB=AA=A8=EB=93=A0=20=EA=B3=BC=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20delete=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/nextstep/qna/domain/QuestionTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 15f57d1ceb..bd0e93c220 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -11,12 +11,12 @@ public class QuestionTest { @Test void 질문_작성자가_자신일_경우_예외가_발생하지_않는다() throws CannotDeleteException { - Q1.validateOwnership(NsUserTest.JAVAJIGI); + Q1.delete(NsUserTest.JAVAJIGI); } @Test void 질문_작성자가_타인일_경우_예외가_발생한다() { - Assertions.assertThatThrownBy(() -> Q1.validateOwnership(NsUserTest.SANJIGI)) + Assertions.assertThatThrownBy(() -> Q1.delete(NsUserTest.SANJIGI)) .isInstanceOf(CannotDeleteException.class) .hasMessageContaining("질문을 삭제할 권한이 없습니다."); } From 622012cb5fc8dec52fbd71ea237b6501b652775d Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 9 Apr 2025 22:39:02 +0900 Subject: [PATCH 10/37] =?UTF-8?q?refactor=20=EC=A7=88=EB=AC=B8=20=EB=B0=8F?= =?UTF-8?q?=20=EB=8B=B5=EB=B3=80=20=EC=82=AD=EC=A0=9C=20=EA=B3=BC=EC=A0=95?= =?UTF-8?q?=20=EC=A7=88=EB=AC=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A7=84=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/qna/domain/Question.java | 25 ++++++++++++++++--- .../java/nextstep/qna/service/QnAService.java | 18 +------------ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index 9c0b5bcb8a..eca6f685f6 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -16,7 +16,7 @@ public class Question { private NsUser writer; - private List answers = new ArrayList<>(); + private Answers answers; private boolean deleted = false; @@ -36,14 +36,32 @@ public Question(Long id, NsUser writer, String title, String contents) { this.writer = writer; this.title = title; this.contents = contents; + this.answers = new Answers(); } - public void validateOwnership(NsUser loginUser) throws CannotDeleteException { + public List delete(NsUser loginUser) throws CannotDeleteException { + validateOwnership(loginUser); + validateAnswersOwnership(loginUser); + + this.deleted = true; + + List deleteHistories = new ArrayList<>(); + deleteHistories.add(DeleteHistory.ofQuestion(this)); + deleteHistories.addAll(answers.deleteAnswers()); + + return deleteHistories; + } + + private void validateOwnership(NsUser loginUser) throws CannotDeleteException { if (!isOwner(loginUser)) { throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); } } + private void validateAnswersOwnership(NsUser loginUser) throws CannotDeleteException { + answers.validateOwnerships(loginUser); + } + public Long getId() { return id; } @@ -80,7 +98,6 @@ private boolean isOwner(NsUser loginUser) { } public Question setDeleted(boolean deleted) { - this.deleted = deleted; return this; } @@ -88,7 +105,7 @@ public boolean isDeleted() { return deleted; } - public List getAnswers() { + public Answers getAnswers() { return answers; } diff --git a/src/main/java/nextstep/qna/service/QnAService.java b/src/main/java/nextstep/qna/service/QnAService.java index 4bf1cb6e63..bde6a7e4f1 100644 --- a/src/main/java/nextstep/qna/service/QnAService.java +++ b/src/main/java/nextstep/qna/service/QnAService.java @@ -26,22 +26,6 @@ public class QnAService { @Transactional public void deleteQuestion(NsUser loginUser, long questionId) throws CannotDeleteException { Question question = questionRepository.findById(questionId).orElseThrow(NotFoundException::new); - question.validateOwnership(loginUser); - - 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())); - } - deleteHistoryService.saveAll(deleteHistories); + deleteHistoryService.saveAll(question.delete(loginUser)); } } From 6d53e299cc00e6bc82074f4db5f4cda635a26b0e Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Sun, 13 Apr 2025 21:18:12 +0900 Subject: [PATCH 11/37] =?UTF-8?q?fix=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/qna/domain/Answer.java | 7 ++++--- src/main/java/nextstep/qna/domain/Answers.java | 16 ++++++++-------- .../java/nextstep/qna/domain/DeleteHistory.java | 4 ++-- src/main/java/nextstep/qna/domain/Question.java | 10 ++-------- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index 624e4d5519..684245e349 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -46,13 +46,14 @@ public Answer(Long id, NsUser writer, Question question, String contents) { public void validateOwnership(NsUser loginUser) throws CannotDeleteException { if (!isOwner(loginUser)) { - throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + throw new CannotDeleteException("답변을 삭제할 권한이 없습니다."); } } - public DeleteHistory delete() { + public DeleteHistory delete(NsUser loginUser) throws CannotDeleteException { + validateOwnership(loginUser); deleted = true; - return DeleteHistory.ofAnswer(this); + return DeleteHistory.deleteOfAnswer(this); } public Long getId() { diff --git a/src/main/java/nextstep/qna/domain/Answers.java b/src/main/java/nextstep/qna/domain/Answers.java index 7a0c46fe43..f03e1a5d6c 100644 --- a/src/main/java/nextstep/qna/domain/Answers.java +++ b/src/main/java/nextstep/qna/domain/Answers.java @@ -18,15 +18,15 @@ public void add(Answer answer) { values.add(answer); } - public void validateOwnerships(NsUser loginUser) throws CannotDeleteException { - for (Answer answer : values) { - answer.validateOwnership(loginUser); - } - } - - public List deleteAnswers() { + public List deleteAnswers(NsUser loginUser) { return values.stream() - .map(Answer::delete) + .map(value -> { + try { + return value.delete(loginUser); + } catch (CannotDeleteException e) { + throw new RuntimeException(e); + } + }) .collect(Collectors.toList()); } } diff --git a/src/main/java/nextstep/qna/domain/DeleteHistory.java b/src/main/java/nextstep/qna/domain/DeleteHistory.java index c6169d327f..93751d76e0 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistory.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistory.java @@ -19,11 +19,11 @@ public class DeleteHistory { public DeleteHistory() { } - public static DeleteHistory ofQuestion(Question q) { + public static DeleteHistory deleteOfQuestion(Question q) { return new DeleteHistory(ContentType.QUESTION, q.getId(), q.getWriter(), LocalDateTime.now()); } - public static DeleteHistory ofAnswer(Answer a) { + public static DeleteHistory deleteOfAnswer(Answer a) { return new DeleteHistory(ContentType.ANSWER, a.getId(), a.getWriter(), LocalDateTime.now()); } diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index eca6f685f6..291eae6c81 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -41,13 +41,11 @@ public Question(Long id, NsUser writer, String title, String contents) { public List delete(NsUser loginUser) throws CannotDeleteException { validateOwnership(loginUser); - validateAnswersOwnership(loginUser); - this.deleted = true; List deleteHistories = new ArrayList<>(); - deleteHistories.add(DeleteHistory.ofQuestion(this)); - deleteHistories.addAll(answers.deleteAnswers()); + deleteHistories.add(DeleteHistory.deleteOfQuestion(this)); + deleteHistories.addAll(answers.deleteAnswers(loginUser)); return deleteHistories; } @@ -58,10 +56,6 @@ private void validateOwnership(NsUser loginUser) throws CannotDeleteException { } } - private void validateAnswersOwnership(NsUser loginUser) throws CannotDeleteException { - answers.validateOwnerships(loginUser); - } - public Long getId() { return id; } From aaeb5021a76b3fb0b5b8e6203af7da74c355a3e9 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Mon, 14 Apr 2025 20:51:23 +0900 Subject: [PATCH 12/37] =?UTF-8?q?fix=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/qna/domain/Answer.java | 2 +- src/main/java/nextstep/qna/domain/DeleteHistory.java | 8 ++++---- src/main/java/nextstep/qna/domain/Question.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index 684245e349..13f1605f06 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -53,7 +53,7 @@ public void validateOwnership(NsUser loginUser) throws CannotDeleteException { public DeleteHistory delete(NsUser loginUser) throws CannotDeleteException { validateOwnership(loginUser); deleted = true; - return DeleteHistory.deleteOfAnswer(this); + return DeleteHistory.ofAnswer(this); } public Long getId() { diff --git a/src/main/java/nextstep/qna/domain/DeleteHistory.java b/src/main/java/nextstep/qna/domain/DeleteHistory.java index 93751d76e0..4f2abf4737 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistory.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistory.java @@ -19,12 +19,12 @@ public class DeleteHistory { public DeleteHistory() { } - public static DeleteHistory deleteOfQuestion(Question q) { - return new DeleteHistory(ContentType.QUESTION, q.getId(), q.getWriter(), LocalDateTime.now()); + public static DeleteHistory ofQuestion(Question question) { + return new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()); } - public static DeleteHistory deleteOfAnswer(Answer a) { - return new DeleteHistory(ContentType.ANSWER, a.getId(), a.getWriter(), LocalDateTime.now()); + public static DeleteHistory ofAnswer(Answer answer) { + return new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now()); } public DeleteHistory(ContentType contentType, Long contentId, NsUser deletedBy, LocalDateTime createdDate) { diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index 291eae6c81..ecdd2544a2 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -44,7 +44,7 @@ public List delete(NsUser loginUser) throws CannotDeleteException this.deleted = true; List deleteHistories = new ArrayList<>(); - deleteHistories.add(DeleteHistory.deleteOfQuestion(this)); + deleteHistories.add(DeleteHistory.ofQuestion(this)); deleteHistories.addAll(answers.deleteAnswers(loginUser)); return deleteHistories; From 8a8a1952387a39d440d2165faca63c8e8d044d6f Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Mon, 14 Apr 2025 20:52:12 +0900 Subject: [PATCH 13/37] =?UTF-8?q?test=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=EA=B0=80=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EB=90=A8=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/nextstep/qna/domain/AnswerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/nextstep/qna/domain/AnswerTest.java b/src/test/java/nextstep/qna/domain/AnswerTest.java index 6558877335..d5d119fd73 100644 --- a/src/test/java/nextstep/qna/domain/AnswerTest.java +++ b/src/test/java/nextstep/qna/domain/AnswerTest.java @@ -18,6 +18,6 @@ public class AnswerTest { void 답변_작성자가_타인일_경우_예외가_발생한다() { Assertions.assertThatThrownBy(() -> A1.validateOwnership(NsUserTest.SANJIGI)) .isInstanceOf(CannotDeleteException.class) - .hasMessageContaining("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + .hasMessageContaining("답변을 삭제할 권한이 없습니다."); } } From c23a134b1bb14faa76fea254f28e249e1c25b782 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Mon, 14 Apr 2025 23:32:21 +0900 Subject: [PATCH 14/37] =?UTF-8?q?docs=20step2=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/step2.md diff --git a/docs/step2.md b/docs/step2.md new file mode 100644 index 0000000000..9bbe986de2 --- /dev/null +++ b/docs/step2.md @@ -0,0 +1,31 @@ +## 기능 요구사항 +- 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다. +- 강의는 시작일과 종료일을 가진다. +- 강의는 강의 커버 이미지 정보를 가진다. +- 이미지 크기는 1MB 이하여야 한다. +- 이미지 타입은 gif, jpg(jpeg 포함),, png, svg만 허용한다. +- 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다. +- 강의는 무료 강의와 유료 강의로 나뉜다. +- 무료 강의는 최대 수강 인원 제한이 없다. +- 유료 강의는 강의 최대 수강 인원을 초과할 수 없다. +- 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다. +- 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다. +- 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. +- 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. +- 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다. + +## 기능 목록 +- [ ] 강의의 종료일보다 시작일이 더 이후인지 확인한다. +- [ ] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. +- [ ] 이미지 타입이 gif, jpg, png, svg인지 확인한다. +- [ ] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. +- [ ] 이미지의 width와 height 비율이 3:2인지 확인한다. +- [ ] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. +- [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. +- [ ] 수강 신청을 한다. +- [ ] 강의 상태가 모집중인지 확인한다. +- [ ] 결제 정보를 조회한다. +- [ ] 강의를 생성한다. +- [ ] 강의 상태를 변경한다. +- [ ] 과정을 생성한다. +- [ ] 과정에 강의를 추가한다. \ No newline at end of file From e3f5f4d1d79e1db41038f1db8c46d09e39e46b8a Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 20:20:06 +0900 Subject: [PATCH 15/37] =?UTF-8?q?test=20SessionPeriod=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sessions/domain/SessionPeriodTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/test/java/nextstep/sessions/domain/SessionPeriodTest.java diff --git a/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java b/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java new file mode 100644 index 0000000000..e5c0c5b03f --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java @@ -0,0 +1,34 @@ +package nextstep.sessions.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +public class SessionPeriodTest { + private Date earlier; + private Date later; + + @BeforeEach + void setUp() { + this.earlier = new Date(2025, 1, 1); + this.later = new Date(2025, 3, 1); + } + + @Test + @DisplayName("시작일이 종료일보다 이전이면 기간을 정상 생성한다.") + void 기간_정상_생성() { + SessionPeriod sessionPeriod = new SessionPeriod(earlier, later); + } + + @Test + @DisplayName("시작일이 종료일보다 늦으면 IllegalArgumentException을 던진다.") + void 시작일이_종료일보다_늦으면_예외발생() { + Assertions.assertThatIllegalArgumentException().isThrownBy(() + -> new SessionPeriod(later, earlier)) + .withMessage("시작일은 종료일보다 이전이어야 합니다."); + + } +} From ac80d4f7318391d2ba947b5a0b43cc61f67e635c Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 20:24:17 +0900 Subject: [PATCH 16/37] =?UTF-8?q?feat=20=EA=B0=95=EC=9D=98=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B0=95=EC=9D=98=20=EC=8B=9C=EC=9E=91=EC=9D=BC,?= =?UTF-8?q?=20=EC=A2=85=EB=A3=8C=EC=9D=BC=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- .../sessions/domain/SessionPeriod.java | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/sessions/domain/SessionPeriod.java diff --git a/docs/step2.md b/docs/step2.md index 9bbe986de2..7a96c0a4a2 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -15,7 +15,7 @@ - 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반한된다. ## 기능 목록 -- [ ] 강의의 종료일보다 시작일이 더 이후인지 확인한다. +- [x] 강의의 종료일보다 시작일이 더 이후인지 확인한다. - [ ] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. - [ ] 이미지 타입이 gif, jpg, png, svg인지 확인한다. - [ ] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. diff --git a/src/main/java/nextstep/sessions/domain/SessionPeriod.java b/src/main/java/nextstep/sessions/domain/SessionPeriod.java new file mode 100644 index 0000000000..d9aae583ab --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionPeriod.java @@ -0,0 +1,20 @@ +package nextstep.sessions.domain; + +import java.util.Date; + +public class SessionPeriod { + private Date startDate; + private Date endDate; + + public SessionPeriod(Date startDate, Date endDate) { + validate(startDate, endDate); + this.startDate = startDate; + this.endDate = endDate; + } + + private void validate(Date startDate, Date endDate) { + if (startDate.after(endDate)) { + throw new IllegalArgumentException("시작일은 종료일보다 이전이어야 합니다."); + } + } +} From 8059451cc5f098c7087485155294f47e6f8bd8e4 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 20:45:10 +0900 Subject: [PATCH 17/37] =?UTF-8?q?test=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/ImageTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/test/java/nextstep/sessions/domain/ImageTest.java diff --git a/src/test/java/nextstep/sessions/domain/ImageTest.java b/src/test/java/nextstep/sessions/domain/ImageTest.java new file mode 100644 index 0000000000..bbd7180873 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/ImageTest.java @@ -0,0 +1,20 @@ +package nextstep.sessions.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ImageTest { + @Test + @DisplayName("이미지 최대 크기를 초과할 시 IllegalArgumentException을 던진다.") + void 이미지_크기_초과() { + Assertions.assertThatIllegalArgumentException().isThrownBy(() -> + new Image(2_000_000L, "이미지", ImageType.JPEG, 300F, 200F)); + } + + @Test + @DisplayName("이미지 최대 크기를 초과하지 않으면 이미지가 잘 생성된다.") + void 이미지_크기_적정() { + Image image = new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); + } +} From ea804075e3641287042de32ccf8672366844dc29 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 20:47:08 +0900 Subject: [PATCH 18/37] =?UTF-8?q?feat=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=81=AC=EA=B8=B0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- .../java/nextstep/sessions/domain/Image.java | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/sessions/domain/Image.java diff --git a/docs/step2.md b/docs/step2.md index 7a96c0a4a2..2d736e0364 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -16,7 +16,7 @@ ## 기능 목록 - [x] 강의의 종료일보다 시작일이 더 이후인지 확인한다. -- [ ] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. +- [x] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. - [ ] 이미지 타입이 gif, jpg, png, svg인지 확인한다. - [ ] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. - [ ] 이미지의 width와 height 비율이 3:2인지 확인한다. diff --git a/src/main/java/nextstep/sessions/domain/Image.java b/src/main/java/nextstep/sessions/domain/Image.java new file mode 100644 index 0000000000..e41df056bf --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Image.java @@ -0,0 +1,38 @@ +package nextstep.sessions.domain; + +public class Image { + private static final Long MAX_SIZE_BYTES = 1_048_576L; + private static final Float MIN_SIZE_WIDTH = 300F; + private static final Float MIN_SIZE_HEIGHT = 200F; + + private Long id; + private Long sizeInBytes; + private String title; + private ImageType type; + private Float width; + private Float height; + + public Image(Long sizeInBytes, String title, ImageType type, Float width, Float height) { + this(sizeInBytes, 0L, title, type, width, height); + } + + public Image(Long sizeInBytes, Long id, String title, ImageType type, Float width, Float height) { + validate(sizeInBytes, title, type, width, height); + this.sizeInBytes = sizeInBytes; + this.id = id; + this.title = title; + this.type = type; + this.width = width; + this.height = height; + } + + private void validate(Long sizeInBytes, String title, ImageType type, Float width, Float height) { + validateSize(sizeInBytes); + } + + private void validateSize(Long sizeInBytes) { + if (sizeInBytes > MAX_SIZE_BYTES) { + throw new IllegalArgumentException("이미지 크기는 최대 1MB 이하여야 합니다."); + } + } +} From 1e6b44f3d13efd6da87279a851ac739913b223b2 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 20:53:46 +0900 Subject: [PATCH 19/37] =?UTF-8?q?test=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EA=B2=80=EC=82=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sessions/domain/ImageTypeTest.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/test/java/nextstep/sessions/domain/ImageTypeTest.java diff --git a/src/test/java/nextstep/sessions/domain/ImageTypeTest.java b/src/test/java/nextstep/sessions/domain/ImageTypeTest.java new file mode 100644 index 0000000000..6152054633 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/ImageTypeTest.java @@ -0,0 +1,26 @@ +package nextstep.sessions.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +public class ImageTypeTest { + + @Test + @DisplayName("지원하는 이미지 타입은 enum으로 정상 변환된다.") + void 지원하는_이미지_타입_변환_성공() { + assertThat(ImageType.from("gif")).isEqualTo(ImageType.GIF); + assertThat(ImageType.from("jpg")).isEqualTo(ImageType.JPG); + assertThat(ImageType.from("jpeg")).isEqualTo(ImageType.JPEG); + assertThat(ImageType.from("png")).isEqualTo(ImageType.PNG); + assertThat(ImageType.from("svg")).isEqualTo(ImageType.SVG); + } + + @Test + @DisplayName("지원하지 않는 이미지 타입은 IllegalArgumentException을 던진다.") + void 지원하지_않는_이미지_타입_예외() { + assertThatIllegalArgumentException().isThrownBy(() -> ImageType.from("java")) + .withMessage("지원하지 않는 이미지 타입입니다."); + } +} From 1e733eb530097fa83c8241317caea5a423d58997 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 20:54:42 +0900 Subject: [PATCH 20/37] =?UTF-8?q?feat=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- .../java/nextstep/sessions/domain/ImageType.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/sessions/domain/ImageType.java diff --git a/docs/step2.md b/docs/step2.md index 2d736e0364..25c663449f 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -17,7 +17,7 @@ ## 기능 목록 - [x] 강의의 종료일보다 시작일이 더 이후인지 확인한다. - [x] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. -- [ ] 이미지 타입이 gif, jpg, png, svg인지 확인한다. +- [x] 이미지 타입이 gif, jpg, png, svg인지 확인한다. - [ ] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. - [ ] 이미지의 width와 height 비율이 3:2인지 확인한다. - [ ] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. diff --git a/src/main/java/nextstep/sessions/domain/ImageType.java b/src/main/java/nextstep/sessions/domain/ImageType.java new file mode 100644 index 0000000000..4f642050f5 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/ImageType.java @@ -0,0 +1,13 @@ +package nextstep.sessions.domain; + +public enum ImageType { + GIF, JPG, JPEG, PNG, SVG; + + public static ImageType from(String type) { + try { + return ImageType.valueOf(type.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("지원하지 않는 이미지 타입입니다."); + } + } +} From dd156997b679d2a53b54a80283a12f3e2b69cbfc Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:06:10 +0900 Subject: [PATCH 21/37] =?UTF-8?q?test=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B0=80=EB=A1=9C,=20=EC=84=B8=EB=A1=9C=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/ImageTest.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/test/java/nextstep/sessions/domain/ImageTest.java b/src/test/java/nextstep/sessions/domain/ImageTest.java index bbd7180873..da1fd31a14 100644 --- a/src/test/java/nextstep/sessions/domain/ImageTest.java +++ b/src/test/java/nextstep/sessions/domain/ImageTest.java @@ -1,20 +1,42 @@ package nextstep.sessions.domain; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; + public class ImageTest { @Test @DisplayName("이미지 최대 크기를 초과할 시 IllegalArgumentException을 던진다.") void 이미지_크기_초과() { - Assertions.assertThatIllegalArgumentException().isThrownBy(() -> - new Image(2_000_000L, "이미지", ImageType.JPEG, 300F, 200F)); + assertThatIllegalArgumentException().isThrownBy(() -> + new Image(2_000_000L, "이미지", ImageType.JPEG, 300F, 200F)) + .withMessage("이미지 크기는 최대 1MB 이하여야 합니다."); } @Test @DisplayName("이미지 최대 크기를 초과하지 않으면 이미지가 잘 생성된다.") void 이미지_크기_적정() { - Image image = new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); + new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); + } + + @Test + @DisplayName("이미지 가로 길이가 300보다 작을 시 IllegalArgumentException을 던진다.") + void 이미지_최소_가로_크기_부적절() { + assertThatIllegalArgumentException().isThrownBy(() -> + new Image(1_000_000L, "이미지", ImageType.JPEG, 200F, 200F)) + .withMessage("이미지 가로 길이는 최소 300픽셀 이상이어야 합니다."); + } + + @Test + @DisplayName("이미지 가로 길이가 300이상이면 정상적으로 생성된다.") + void 이미지_최소_가로_크기_적절() { + new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); + } + + @Test + @DisplayName("이미지 세로 길이가 200이상이면 정상적으로 생성된다.") + void 이미지_최소_세로_크기_적절() { + new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); } } From 3a3850750d278e5e704466f84e37c748e4961f5d Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:06:53 +0900 Subject: [PATCH 22/37] =?UTF-8?q?feat=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B0=80=EB=A1=9C,=20=EC=84=B8=EB=A1=9C=20=EA=B8=B8=EC=9D=B4?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- src/main/java/nextstep/sessions/domain/Image.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/step2.md b/docs/step2.md index 25c663449f..9e074731ad 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -18,7 +18,7 @@ - [x] 강의의 종료일보다 시작일이 더 이후인지 확인한다. - [x] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. - [x] 이미지 타입이 gif, jpg, png, svg인지 확인한다. -- [ ] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. +- [x] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. - [ ] 이미지의 width와 height 비율이 3:2인지 확인한다. - [ ] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. - [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. diff --git a/src/main/java/nextstep/sessions/domain/Image.java b/src/main/java/nextstep/sessions/domain/Image.java index e41df056bf..cf48a0fcaf 100644 --- a/src/main/java/nextstep/sessions/domain/Image.java +++ b/src/main/java/nextstep/sessions/domain/Image.java @@ -28,6 +28,8 @@ public Image(Long sizeInBytes, Long id, String title, ImageType type, Float widt private void validate(Long sizeInBytes, String title, ImageType type, Float width, Float height) { validateSize(sizeInBytes); + validateWidth(width); + validateHeight(height); } private void validateSize(Long sizeInBytes) { @@ -35,4 +37,16 @@ private void validateSize(Long sizeInBytes) { throw new IllegalArgumentException("이미지 크기는 최대 1MB 이하여야 합니다."); } } + + private void validateWidth(Float width) { + if (width < MIN_SIZE_WIDTH) { + throw new IllegalArgumentException("이미지 가로 길이는 최소 300픽셀 이상이어야 합니다."); + } + } + + private void validateHeight(Float height) { + if (height < MIN_SIZE_HEIGHT) { + throw new IllegalArgumentException("이미지 세로 길이는 최소 200픽셀 이상이어야 합니다."); + } + } } From a14350fdb251abb6518c858fbdda1fa5d05bc8d9 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:12:59 +0900 Subject: [PATCH 23/37] =?UTF-8?q?test=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B0=80=EB=A1=9C=20=EC=84=B8=EB=A1=9C=20=EB=B9=84=EC=9C=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nextstep/sessions/domain/ImageTest.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/test/java/nextstep/sessions/domain/ImageTest.java b/src/test/java/nextstep/sessions/domain/ImageTest.java index da1fd31a14..7f16df112e 100644 --- a/src/test/java/nextstep/sessions/domain/ImageTest.java +++ b/src/test/java/nextstep/sessions/domain/ImageTest.java @@ -10,7 +10,7 @@ public class ImageTest { @DisplayName("이미지 최대 크기를 초과할 시 IllegalArgumentException을 던진다.") void 이미지_크기_초과() { assertThatIllegalArgumentException().isThrownBy(() -> - new Image(2_000_000L, "이미지", ImageType.JPEG, 300F, 200F)) + new Image(2_000_000L, "이미지", ImageType.JPEG, 300F, 200F)) .withMessage("이미지 크기는 최대 1MB 이하여야 합니다."); } @@ -24,7 +24,7 @@ public class ImageTest { @DisplayName("이미지 가로 길이가 300보다 작을 시 IllegalArgumentException을 던진다.") void 이미지_최소_가로_크기_부적절() { assertThatIllegalArgumentException().isThrownBy(() -> - new Image(1_000_000L, "이미지", ImageType.JPEG, 200F, 200F)) + new Image(1_000_000L, "이미지", ImageType.JPEG, 200F, 200F)) .withMessage("이미지 가로 길이는 최소 300픽셀 이상이어야 합니다."); } @@ -39,4 +39,24 @@ public class ImageTest { void 이미지_최소_세로_크기_적절() { new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); } + + @Test + @DisplayName("이미지 가로 세로 비율이 3:2가 아니면 IllegalArgumentException을 던진다.") + void 이미지_가로_세로_비율_부적절() { + assertThatIllegalArgumentException().isThrownBy(() -> + new Image(1_000_000L, "이미지", ImageType.JPEG, 400F, 200F)) + .withMessage("이미지 비율은 3:2이어야 합니다."); + } + + @Test + @DisplayName("이미지 가로 세로 비율이 3:2면 정상적으로 생성된다.") + void 이미지_가로_세로_비율_적절() { + new Image(1_000_000L, "이미지", ImageType.JPEG, 300F, 200F); + } + + @Test + @DisplayName("이미지 가로 세로 비율이 약 3:2면 정상적으로 생성된다.") + void 이미지_가로_세로_비율_오차_범위_이내() { + new Image(1_000_000L, "이미지", ImageType.JPEG, 301F, 200F); + } } From 5c177c358c049503c5e2dbc08721f21acbecee6c Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:13:26 +0900 Subject: [PATCH 24/37] =?UTF-8?q?feat=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EC=9D=98=20=EA=B0=80=EB=A1=9C=20=EC=84=B8=EB=A1=9C=20=EB=B9=84?= =?UTF-8?q?=EC=9C=A8=EC=9D=B4=203:2=EC=9D=B8=EC=A7=80=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- src/main/java/nextstep/sessions/domain/Image.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/step2.md b/docs/step2.md index 9e074731ad..9eb15a49de 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -19,7 +19,7 @@ - [x] 강의 커버 이미지의 크기가 1MB 이하인지 확인한다. - [x] 이미지 타입이 gif, jpg, png, svg인지 확인한다. - [x] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. -- [ ] 이미지의 width와 height 비율이 3:2인지 확인한다. +- [x] 이미지의 width와 height 비율이 3:2인지 확인한다. - [ ] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. - [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [ ] 수강 신청을 한다. diff --git a/src/main/java/nextstep/sessions/domain/Image.java b/src/main/java/nextstep/sessions/domain/Image.java index cf48a0fcaf..253a2ef401 100644 --- a/src/main/java/nextstep/sessions/domain/Image.java +++ b/src/main/java/nextstep/sessions/domain/Image.java @@ -30,6 +30,7 @@ private void validate(Long sizeInBytes, String title, ImageType type, Float widt validateSize(sizeInBytes); validateWidth(width); validateHeight(height); + validateAspectRatio(width, height); } private void validateSize(Long sizeInBytes) { @@ -49,4 +50,11 @@ private void validateHeight(Float height) { throw new IllegalArgumentException("이미지 세로 길이는 최소 200픽셀 이상이어야 합니다."); } } + + private void validateAspectRatio(float width, float height) { + float ratio = width / height; + if (Math.abs(ratio - 1.5) > 0.01) { + throw new IllegalArgumentException("이미지 비율은 3:2이어야 합니다."); + } + } } From ec7e4779a30630cb6b4c39f2de4ab621041a77ed Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:39:52 +0900 Subject: [PATCH 25/37] =?UTF-8?q?test=20=EB=93=B1=EB=A1=9D=20=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/EnrollmentCapacityTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/test/java/nextstep/sessions/domain/EnrollmentCapacityTest.java diff --git a/src/test/java/nextstep/sessions/domain/EnrollmentCapacityTest.java b/src/test/java/nextstep/sessions/domain/EnrollmentCapacityTest.java new file mode 100644 index 0000000000..c6bb6f8e16 --- /dev/null +++ b/src/test/java/nextstep/sessions/domain/EnrollmentCapacityTest.java @@ -0,0 +1,24 @@ +package nextstep.sessions.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class EnrollmentCapacityTest { + @Test + @DisplayName("정원이 가득 찬 경우 인원 증가 시 예외를 던진다.") + void 등록_인원_초과() { + EnrollmentCapacity enrollmentCapacity = new EnrollmentCapacity(1, 1); + assertThatThrownBy(enrollmentCapacity::increaseEnrollment) + .isInstanceOf(IllegalStateException.class) + .hasMessage("정원이 초과되었습니다."); + } + + @Test + @DisplayName("정원이 가득 차지 않은 경우 등록 인원을 정상적으로 증가시킨다..") + void 등록_인원_증가() { + EnrollmentCapacity enrollmentCapacity = new EnrollmentCapacity(1, 0); + enrollmentCapacity.increaseEnrollment(); + } +} \ No newline at end of file From aab7ce1f1a1277f30b750480675adb0de150ed33 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:40:55 +0900 Subject: [PATCH 26/37] =?UTF-8?q?feat=20=EC=A0=95=EC=9B=90=EC=9D=B4=20?= =?UTF-8?q?=EC=B4=88=EA=B3=BC=ED=96=88=EB=8A=94=EC=A7=80=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- .../sessions/domain/EnrollmentCapacity.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java diff --git a/docs/step2.md b/docs/step2.md index 9eb15a49de..25a702ee32 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -20,7 +20,7 @@ - [x] 이미지 타입이 gif, jpg, png, svg인지 확인한다. - [x] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. - [x] 이미지의 width와 height 비율이 3:2인지 확인한다. -- [ ] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. +- [x] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. - [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [ ] 수강 신청을 한다. - [ ] 강의 상태가 모집중인지 확인한다. diff --git a/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java b/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java new file mode 100644 index 0000000000..e35b890db1 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java @@ -0,0 +1,25 @@ +package nextstep.sessions.domain; + +public class EnrollmentCapacity { + private int maxEnrollment; + private int currentEnrollment; + + public EnrollmentCapacity(int maxEnrollment) { + this(maxEnrollment, 0); + } + + public EnrollmentCapacity(int maxEnrollment, int currentEnrollment) { + this.maxEnrollment = maxEnrollment; + this.currentEnrollment = currentEnrollment; + } + + public boolean isFull() { + return currentEnrollment >= maxEnrollment; + } + + private void validate() { + if (isFull()) { + throw new IllegalStateException("정원이 초과되었습니다."); + } + } +} From 02cb05aac919300f714eec3d591d928524ab36b0 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:41:24 +0900 Subject: [PATCH 27/37] =?UTF-8?q?docs=20=EC=88=98=EA=B0=95=20=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=20=EC=A6=9D=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/step2.md b/docs/step2.md index 25a702ee32..40d829a042 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -21,6 +21,7 @@ - [x] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. - [x] 이미지의 width와 height 비율이 3:2인지 확인한다. - [x] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. +- [ ] 현재 수강 인원을 증가시킨다. - [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [ ] 수강 신청을 한다. - [ ] 강의 상태가 모집중인지 확인한다. From 9265ddb7ed8a54a21674eb91a51b0e97549e764c Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Tue, 15 Apr 2025 21:41:59 +0900 Subject: [PATCH 28/37] =?UTF-8?q?feat=20=ED=98=84=EC=9E=AC=20=EC=88=98?= =?UTF-8?q?=EA=B0=95=20=EC=9D=B8=EC=9B=90=EC=9D=84=20=EC=A6=9D=EA=B0=80?= =?UTF-8?q?=EC=8B=9C=ED=82=A4=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- .../java/nextstep/sessions/domain/EnrollmentCapacity.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/step2.md b/docs/step2.md index 40d829a042..6eb4d954f2 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -21,7 +21,7 @@ - [x] 이미지의 width와 height가 각각 300, 200 픽셀 이상인지 확인한다. - [x] 이미지의 width와 height 비율이 3:2인지 확인한다. - [x] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. -- [ ] 현재 수강 인원을 증가시킨다. +- [x] 현재 수강 인원을 증가시킨다. - [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [ ] 수강 신청을 한다. - [ ] 강의 상태가 모집중인지 확인한다. diff --git a/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java b/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java index e35b890db1..0efc6444a4 100644 --- a/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java +++ b/src/main/java/nextstep/sessions/domain/EnrollmentCapacity.java @@ -17,6 +17,11 @@ public boolean isFull() { return currentEnrollment >= maxEnrollment; } + public void increaseEnrollment() { + validate(); + currentEnrollment++; + } + private void validate() { if (isFull()) { throw new IllegalStateException("정원이 초과되었습니다."); From 2ecb6bdfc2a48314cc7942f6a42433beeee09936 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:07:52 +0900 Subject: [PATCH 29/37] =?UTF-8?q?test=20=EA=B2=B0=EC=A0=9C=20=EA=B8=88?= =?UTF-8?q?=EC=95=A1=EA=B3=BC=20=EA=B0=95=EC=9D=98=20=EA=B8=88=EC=95=A1=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=20=EC=97=AC=EB=B6=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enrollment/domain/EnrollmentTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/test/java/nextstep/enrollment/domain/EnrollmentTest.java diff --git a/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java b/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java new file mode 100644 index 0000000000..4dee15966e --- /dev/null +++ b/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java @@ -0,0 +1,22 @@ +package nextstep.enrollment.domain; + +import nextstep.payments.domain.Payment; +import nextstep.sessions.domain.Session; +import nextstep.users.domain.NsUser; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +class EnrollmentTest { + @Test + void 결제_금액_강의_금액_일치() { + new Enrollment(new NsUser(), new Session(10_000L), new Payment("user_id", 0L, 0L, 10_000L)); + } + + @Test + void 결제_금액_강의_금액_불일치() { + assertThatIllegalArgumentException().isThrownBy(() -> + new Enrollment(new NsUser(), new Session(10_000L), new Payment("user_id", 0L, 0L, 20_000L))) + .withMessage("결제 금액이 수강료와 일치하지 않습니다."); + } +} \ No newline at end of file From f547d74502a31b6760f350d141814e1bba2b6140 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:08:27 +0900 Subject: [PATCH 30/37] =?UTF-8?q?feat=20=EC=88=98=EA=B0=95=EB=A3=8C?= =?UTF-8?q?=EC=99=80=20=EA=B2=B0=EC=A0=9C=20=EA=B8=88=EC=95=A1=EC=9D=B4=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=ED=95=98=EB=8A=94=EC=A7=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 2 +- .../enrollment/domain/Enrollment.java | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/main/java/nextstep/enrollment/domain/Enrollment.java diff --git a/docs/step2.md b/docs/step2.md index 6eb4d954f2..89fbd43084 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -22,7 +22,7 @@ - [x] 이미지의 width와 height 비율이 3:2인지 확인한다. - [x] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. - [x] 현재 수강 인원을 증가시킨다. -- [ ] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. +- [x] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [ ] 수강 신청을 한다. - [ ] 강의 상태가 모집중인지 확인한다. - [ ] 결제 정보를 조회한다. diff --git a/src/main/java/nextstep/enrollment/domain/Enrollment.java b/src/main/java/nextstep/enrollment/domain/Enrollment.java new file mode 100644 index 0000000000..f08cc006b0 --- /dev/null +++ b/src/main/java/nextstep/enrollment/domain/Enrollment.java @@ -0,0 +1,37 @@ +package nextstep.enrollment.domain; + +import nextstep.payments.domain.Payment; +import nextstep.sessions.domain.Session; +import nextstep.users.domain.NsUser; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class Enrollment { + private final NsUser student; + private final Session session; + private final LocalDateTime enrolledAt; + private final Payment payment; + + public Enrollment(NsUser student, Session session, Payment payment) { + validate(session, payment); + this.student = student; + this.session = session; + this.enrolledAt = LocalDateTime.now(); + this.payment = payment; + } + + private void validate(Session session, Payment payment) { + validatePaidCorrectly(session, payment); + } + + private void validatePaidCorrectly(Session session, Payment payment) { + if (!hasPaidCorrectly(session, payment)) { + throw new IllegalArgumentException("결제 금액이 수강료와 일치하지 않습니다."); + } + } + + private boolean hasPaidCorrectly(Session session, Payment payment) { + return Objects.equals(payment.amount(), session.price()); + } +} From 835cf538a42c35768cb47f96db75a7541dffbeda Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:27:24 +0900 Subject: [PATCH 31/37] =?UTF-8?q?test=20=EA=B0=95=EC=9D=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=88=98=EA=B0=95?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enrollment/domain/EnrollmentTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java b/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java index 4dee15966e..2c4253ec62 100644 --- a/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java +++ b/src/test/java/nextstep/enrollment/domain/EnrollmentTest.java @@ -2,21 +2,39 @@ import nextstep.payments.domain.Payment; import nextstep.sessions.domain.Session; +import nextstep.sessions.domain.SessionStatus; import nextstep.users.domain.NsUser; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; class EnrollmentTest { @Test + @DisplayName("결제 금액이 강의 수강료와 일치하면 수강 신청에 성공한다.") void 결제_금액_강의_금액_일치() { new Enrollment(new NsUser(), new Session(10_000L), new Payment("user_id", 0L, 0L, 10_000L)); } @Test + @DisplayName("결제 금액이 강의 수강료와 다르면 예외를 던진다.") void 결제_금액_강의_금액_불일치() { assertThatIllegalArgumentException().isThrownBy(() -> new Enrollment(new NsUser(), new Session(10_000L), new Payment("user_id", 0L, 0L, 20_000L))) .withMessage("결제 금액이 수강료와 일치하지 않습니다."); } + + @Test + @DisplayName("강의 상태가 모집중이면 수강 신청이 가능하다.") + void 모집중_강의_수강_신청() { + new Enrollment(new NsUser(), new Session(SessionStatus.RECRUITING, 10_000L), new Payment("user_id", 0L, 0L, 10_000L)); + } + + @Test + @DisplayName("강의 상태가 모집중이 아니면 수강 신청 시 예외를 던진다.") + void 모집중_아닌_강의_수강_신청() { + assertThatIllegalArgumentException().isThrownBy(() -> + new Enrollment(new NsUser(), new Session(SessionStatus.CLOSED, 10_000L), new Payment("user_id", 0L, 0L, 10_000L))) + .withMessage("강의 상태가 모집중일 때만 수강 신청이 가능합니다."); + } } \ No newline at end of file From 31f9f94da5cee2770c68ddad63bf0b8a904db20a Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:28:49 +0900 Subject: [PATCH 32/37] =?UTF-8?q?feat=20Session=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=88=98=EA=B0=95?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=8B=9C=20=EA=B0=95=EC=9D=98=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=80=20=EB=AA=A8=EC=A7=91=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 4 +- .../enrollment/domain/Enrollment.java | 7 ++++ .../nextstep/sessions/domain/Session.java | 40 +++++++++++++++++++ .../sessions/domain/SessionStatus.java | 5 +++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/main/java/nextstep/sessions/domain/Session.java create mode 100644 src/main/java/nextstep/sessions/domain/SessionStatus.java diff --git a/docs/step2.md b/docs/step2.md index 89fbd43084..d722166f68 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -23,8 +23,8 @@ - [x] 유료 강의의 강의 최대 수강 인원을 초과하는지 확인한다. - [x] 현재 수강 인원을 증가시킨다. - [x] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. -- [ ] 수강 신청을 한다. -- [ ] 강의 상태가 모집중인지 확인한다. +- [x] 수강 신청을 한다. +- [x] 강의 상태가 모집중인지 확인한다. - [ ] 결제 정보를 조회한다. - [ ] 강의를 생성한다. - [ ] 강의 상태를 변경한다. diff --git a/src/main/java/nextstep/enrollment/domain/Enrollment.java b/src/main/java/nextstep/enrollment/domain/Enrollment.java index f08cc006b0..145ae78000 100644 --- a/src/main/java/nextstep/enrollment/domain/Enrollment.java +++ b/src/main/java/nextstep/enrollment/domain/Enrollment.java @@ -22,9 +22,16 @@ public Enrollment(NsUser student, Session session, Payment payment) { } private void validate(Session session, Payment payment) { + validateSessionStatus(session); validatePaidCorrectly(session, payment); } + private void validateSessionStatus(Session session) { + if (!session.isRecruiting()) { + throw new IllegalArgumentException("강의 상태가 모집중일 때만 수강 신청이 가능합니다."); + } + } + private void validatePaidCorrectly(Session session, Payment payment) { if (!hasPaidCorrectly(session, payment)) { throw new IllegalArgumentException("결제 금액이 수강료와 일치하지 않습니다."); diff --git a/src/main/java/nextstep/sessions/domain/Session.java b/src/main/java/nextstep/sessions/domain/Session.java new file mode 100644 index 0000000000..ee81f021a7 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/Session.java @@ -0,0 +1,40 @@ +package nextstep.sessions.domain; + +public class Session { + private Long id; + private String title; + private SessionType type; + private SessionStatus status; + private SessionPeriod period; + private Image coverImage; + private EnrollmentCapacity enrollmentCapacity; + private Long price; + + public Session(Long price) { + this.price = price; + } + + public Session(SessionStatus status, Long price) { + this.status = status; + this.price = price; + } + + public Session(Long id, String title, SessionType type, SessionStatus status, SessionPeriod period, Image coverImage, EnrollmentCapacity enrollmentCapacity, Long price) { + this.id = id; + this.title = title; + this.type = type; + this.status = status; + this.period = period; + this.coverImage = coverImage; + this.enrollmentCapacity = enrollmentCapacity; + this.price = price; + } + + public Long price() { + return this.price; + } + + public boolean isRecruiting() { + return status.equals(SessionStatus.RECRUITING); + } +} diff --git a/src/main/java/nextstep/sessions/domain/SessionStatus.java b/src/main/java/nextstep/sessions/domain/SessionStatus.java new file mode 100644 index 0000000000..9e00695a04 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionStatus.java @@ -0,0 +1,5 @@ +package nextstep.sessions.domain; + +public enum SessionStatus { + READY, RECRUITING, CLOSED +} \ No newline at end of file From 7ecb687073e049918a4ad7b2227c09e4fd5c7f3c Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:29:23 +0900 Subject: [PATCH 33/37] =?UTF-8?q?docs=20=ED=98=84=EC=9E=AC=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=EC=97=86=EC=96=B4=EB=B3=B4=EC=9D=B4=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/step2.md b/docs/step2.md index d722166f68..0ad18ffccd 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -24,9 +24,4 @@ - [x] 현재 수강 인원을 증가시킨다. - [x] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [x] 수강 신청을 한다. -- [x] 강의 상태가 모집중인지 확인한다. -- [ ] 결제 정보를 조회한다. -- [ ] 강의를 생성한다. -- [ ] 강의 상태를 변경한다. -- [ ] 과정을 생성한다. -- [ ] 과정에 강의를 추가한다. \ No newline at end of file +- [x] 강의 상태가 모집중인지 확인한다. \ No newline at end of file From db7209e9fa041e1d2073b3dda3221900ca242cb6 Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:29:48 +0900 Subject: [PATCH 34/37] =?UTF-8?q?feat=20=EA=B2=B0=EC=A0=9C=20=EA=B8=88?= =?UTF-8?q?=EC=95=A1=EC=9D=84=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/step2.md | 3 ++- src/main/java/nextstep/payments/domain/Payment.java | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/step2.md b/docs/step2.md index 0ad18ffccd..e65356cc1d 100644 --- a/docs/step2.md +++ b/docs/step2.md @@ -24,4 +24,5 @@ - [x] 현재 수강 인원을 증가시킨다. - [x] 유료 강의의 수강료와 수강생이 결제한 금액이 일치하는지 확인한다. - [x] 수강 신청을 한다. -- [x] 강의 상태가 모집중인지 확인한다. \ No newline at end of file +- [x] 강의 상태가 모집중인지 확인한다. +- [x] 결제 금액을 반환한다. \ No newline at end of file diff --git a/src/main/java/nextstep/payments/domain/Payment.java b/src/main/java/nextstep/payments/domain/Payment.java index 57d833f851..5af1257cbe 100644 --- a/src/main/java/nextstep/payments/domain/Payment.java +++ b/src/main/java/nextstep/payments/domain/Payment.java @@ -26,4 +26,8 @@ public Payment(String id, Long sessionId, Long nsUserId, Long amount) { this.amount = amount; this.createdAt = LocalDateTime.now(); } + + public Long amount() { + return this.amount; + } } From cf74b39536ee1feb3d2cb773a1b6653e2f6aae6e Mon Sep 17 00:00:00 2001 From: JiEung2 <181757@jnu.ac.kr> Date: Wed, 16 Apr 2025 00:30:32 +0900 Subject: [PATCH 35/37] =?UTF-8?q?feat=20SessionType=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/sessions/domain/SessionType.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/java/nextstep/sessions/domain/SessionType.java diff --git a/src/main/java/nextstep/sessions/domain/SessionType.java b/src/main/java/nextstep/sessions/domain/SessionType.java new file mode 100644 index 0000000000..fb938b5332 --- /dev/null +++ b/src/main/java/nextstep/sessions/domain/SessionType.java @@ -0,0 +1,5 @@ +package nextstep.sessions.domain; + +public enum SessionType { + FREE, PAID +} \ No newline at end of file From 6fe969cc428bb4b56889ba71850f8ad4c1e01561 Mon Sep 17 00:00:00 2001 From: jieung Date: Thu, 17 Apr 2025 18:21:23 +0900 Subject: [PATCH 36/37] =?UTF-8?q?fix:=20MAX=5FSIZE=20=ED=91=9C=ED=98=84=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=A7=A4?= =?UTF-8?q?=EC=A7=81=EB=84=98=EB=B2=84=20=EC=83=81=EC=88=98=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/nextstep/sessions/domain/Image.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/Image.java b/src/main/java/nextstep/sessions/domain/Image.java index 253a2ef401..d643304d79 100644 --- a/src/main/java/nextstep/sessions/domain/Image.java +++ b/src/main/java/nextstep/sessions/domain/Image.java @@ -1,9 +1,10 @@ package nextstep.sessions.domain; public class Image { - private static final Long MAX_SIZE_BYTES = 1_048_576L; + private static final Long MAX_SIZE_BYTES = 1024 * 1024L; private static final Float MIN_SIZE_WIDTH = 300F; private static final Float MIN_SIZE_HEIGHT = 200F; + private static final Float ASPECT_RATIO_TOLERANCE = 0.01f; private Long id; private Long sizeInBytes; @@ -53,7 +54,7 @@ private void validateHeight(Float height) { private void validateAspectRatio(float width, float height) { float ratio = width / height; - if (Math.abs(ratio - 1.5) > 0.01) { + if (Math.abs(ratio - (MIN_SIZE_WIDTH / MIN_SIZE_HEIGHT)) > ASPECT_RATIO_TOLERANCE) { throw new IllegalArgumentException("이미지 비율은 3:2이어야 합니다."); } } From fb64a352993ee99d11301a4b44d89129d05032e3 Mon Sep 17 00:00:00 2001 From: jieung Date: Thu, 17 Apr 2025 18:21:55 +0900 Subject: [PATCH 37/37] =?UTF-8?q?fix:=20Date=20LocalDate=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=B3=80=EC=88=98=20final=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/nextstep/sessions/domain/SessionPeriod.java | 12 ++++++------ .../nextstep/sessions/domain/SessionPeriodTest.java | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/nextstep/sessions/domain/SessionPeriod.java b/src/main/java/nextstep/sessions/domain/SessionPeriod.java index d9aae583ab..b2031a4b1f 100644 --- a/src/main/java/nextstep/sessions/domain/SessionPeriod.java +++ b/src/main/java/nextstep/sessions/domain/SessionPeriod.java @@ -1,19 +1,19 @@ package nextstep.sessions.domain; -import java.util.Date; +import java.time.LocalDate; public class SessionPeriod { - private Date startDate; - private Date endDate; + private final LocalDate startDate; + private final LocalDate endDate; - public SessionPeriod(Date startDate, Date endDate) { + public SessionPeriod(LocalDate startDate, LocalDate endDate) { validate(startDate, endDate); this.startDate = startDate; this.endDate = endDate; } - private void validate(Date startDate, Date endDate) { - if (startDate.after(endDate)) { + private void validate(LocalDate startDate, LocalDate endDate) { + if (startDate.isAfter(endDate)) { throw new IllegalArgumentException("시작일은 종료일보다 이전이어야 합니다."); } } diff --git a/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java b/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java index e5c0c5b03f..187c3a5ce9 100644 --- a/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java +++ b/src/test/java/nextstep/sessions/domain/SessionPeriodTest.java @@ -5,16 +5,16 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.Date; +import java.time.LocalDate; public class SessionPeriodTest { - private Date earlier; - private Date later; + private LocalDate earlier; + private LocalDate later; @BeforeEach void setUp() { - this.earlier = new Date(2025, 1, 1); - this.later = new Date(2025, 3, 1); + this.earlier = LocalDate.of(2025, 1, 1); + this.later = LocalDate.of(2025, 3, 1); } @Test