diff --git "a/docs/step1\354\232\224\352\265\254\354\202\254\355\225\255.md" "b/docs/step1\354\232\224\352\265\254\354\202\254\355\225\255.md" new file mode 100644 index 0000000000..8e307667da --- /dev/null +++ "b/docs/step1\354\232\224\352\265\254\354\202\254\355\225\255.md" @@ -0,0 +1,25 @@ +# 문자열 사칙 연산 계산기 구현 +## 기능 요구사항 + +## 삭제 방식 변경 +- [x] 질문 데이터를 완전히 삭제하지 않는다(soft delete) +- [x] deleted 상태(boolean type)를 true로 변경한다 + +## 삭제 권한 검증 +- [x] 로그인 사용자와 질문 작성자가 같은 경우에만 삭제 가능하다 +- [x] 답변이 없는 경우 삭제가 가능하다 +- [x] 질문자와 모든 답변 작성자가 같은 경우 삭제가 가능하다 +- [x] 질문자와 답변자가 다른 경우 질문을 삭제할 수 없다 + +## 연관 데이터 처리 +- [x] 질문을 삭제할 때 관련된 모든 답변도 함께 삭제한다 +- [x] 답변의 삭제도 soft deled로 deleted 상태를 true로 변경한다 + +## 삭제 이력 관리 +- [x] 질문 삭제 시 DeleteHistory를 생성하여 이력을 남긴다 +- [x] 답변 삭제 시에도 DeleteHistory를 생성하여 이력을 남긴다 + + +## 리팩터링 요구사항 +- [ ] deleteQuestion() 메서드의 비즈니스 로직을 도메인으로 이동한다. + diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index cf681811e7..861b2ccc0b 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -47,11 +47,6 @@ public Long getId() { return id; } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } - public boolean isDeleted() { return deleted; } @@ -64,14 +59,15 @@ public NsUser getWriter() { return writer; } - public String getContents() { - return contents; - } - public void toQuestion(Question question) { this.question = question; } + public DeleteHistory delete(){ + this.deleted = true; + return new DeleteHistory(ContentType.ANSWER, id, writer, LocalDateTime.now()); + } + @Override public String toString() { return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]"; 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..6d229036b4 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/Answers.java @@ -0,0 +1,38 @@ +package nextstep.qna.domain; + +import nextstep.qna.CannotDeleteException; +import nextstep.users.domain.NsUser; + +import java.util.ArrayList; +import java.util.List; + +public class Answers { + private final List answers; + + public Answers() { + this(new ArrayList<>()); + } + + public Answers(List answers) { + this.answers = new ArrayList<>(answers); + } + + public void add(Answer answer) { + answers.add(answer); + } + + public List delete(NsUser loginUser) throws CannotDeleteException { + checkDeletable(loginUser); + List histories = new ArrayList<>(); + for (Answer answer : answers) { + histories.add(answer.delete()); + } + return histories; + } + + private void checkDeletable(NsUser loginUser) throws CannotDeleteException { + if (answers.stream().anyMatch(answer -> !answer.isOwner(loginUser))) { + throw new CannotDeleteException("다른 사람의 답변이 존재하여 삭제할 수 없습니다."); + } + } +} diff --git a/src/main/java/nextstep/qna/domain/BaseEntity.java b/src/main/java/nextstep/qna/domain/BaseEntity.java new file mode 100644 index 0000000000..4611df2b8b --- /dev/null +++ b/src/main/java/nextstep/qna/domain/BaseEntity.java @@ -0,0 +1,19 @@ +package nextstep.qna.domain; + +import java.time.LocalDateTime; + +public abstract class BaseEntity { + private Long id; + private LocalDateTime createdDate = LocalDateTime.now(); + private LocalDateTime updatedDate; + + public BaseEntity() {} + + public BaseEntity(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } +} diff --git a/src/main/java/nextstep/qna/domain/DeleteHistory.java b/src/main/java/nextstep/qna/domain/DeleteHistory.java index 43c37e5e5c..f450df8ac4 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistory.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistory.java @@ -26,6 +26,18 @@ public DeleteHistory(ContentType contentType, Long contentId, NsUser deletedBy, this.createdDate = createdDate; } + public ContentType getContentType() { + return contentType; + } + + public Long getContentId() { + return contentId; + } + + public NsUser getDeletedBy() { + return deletedBy; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index b623c52c76..798de6302a 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -1,29 +1,23 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUser; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -public class Question { - private Long id; - - private String title; - - private String contents; +public class Question extends BaseEntity { + private QuestionContent content; private NsUser writer; - private List answers = new ArrayList<>(); + private Answers answers = new Answers(); private boolean deleted = false; - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; - public Question() { + super(); } public Question(NsUser writer, String title, String contents) { @@ -31,32 +25,13 @@ public Question(NsUser writer, String title, String contents) { } public Question(Long id, NsUser writer, String title, String contents) { - this.id = id; - this.writer = writer; - this.title = title; - this.contents = contents; - } - - public Long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public Question setTitle(String title) { - this.title = title; - return this; - } - - public String getContents() { - return contents; + this(id, writer, new QuestionContent(title, contents)); } - public Question setContents(String contents) { - this.contents = contents; - return this; + public Question(Long id, NsUser writer, QuestionContent content) { + super(id); + this.writer = writer; + this.content = content; } public NsUser getWriter() { @@ -72,21 +47,29 @@ public boolean isOwner(NsUser loginUser) { return writer.equals(loginUser); } - public Question setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } - public boolean isDeleted() { return deleted; } - public List getAnswers() { - return answers; + public List delete(NsUser loginUser) throws CannotDeleteException { + if (!isOwner(loginUser)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } + + List deleteHistories = new ArrayList<>(); + deleteHistories.add(deleteQuestion()); + deleteHistories.addAll(answers.delete(loginUser)); + + return deleteHistories; + } + + private DeleteHistory deleteQuestion() { + this.deleted = true; + return new DeleteHistory(ContentType.QUESTION, getId(), writer, LocalDateTime.now()); } @Override public String toString() { - return "Question [id=" + getId() + ", title=" + title + ", contents=" + contents + ", writer=" + writer + "]"; + return "Question [id=" + getId() + ", title=" + content.getTitle() + ", contents=" + content.getContents() + ", writer=" + writer + "]"; } } diff --git a/src/main/java/nextstep/qna/domain/QuestionContent.java b/src/main/java/nextstep/qna/domain/QuestionContent.java new file mode 100644 index 0000000000..556ed13a71 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/QuestionContent.java @@ -0,0 +1,18 @@ +package nextstep.qna.domain; + +public class QuestionContent { + private String title; + private String contents; + + public QuestionContent(String title, String contents) { + this.title = title; + this.contents = contents; + } + public String getTitle() { + return title; + } + + public String getContents() { + return contents; + } +} diff --git a/src/main/java/nextstep/qna/service/QnAService.java b/src/main/java/nextstep/qna/service/QnAService.java index 5741c84d65..666a3d8cf5 100644 --- a/src/main/java/nextstep/qna/service/QnAService.java +++ b/src/main/java/nextstep/qna/service/QnAService.java @@ -2,14 +2,15 @@ import nextstep.qna.CannotDeleteException; import nextstep.qna.NotFoundException; -import nextstep.qna.domain.*; +import nextstep.qna.domain.AnswerRepository; +import nextstep.qna.domain.DeleteHistory; +import nextstep.qna.domain.Question; +import nextstep.qna.domain.QuestionRepository; import nextstep.users.domain.NsUser; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; -import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; @Service("qnaService") @@ -26,24 +27,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("질문을 삭제할 권한이 없습니다."); - } - - 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())); - } + List deleteHistories = question.delete(loginUser); deleteHistoryService.saveAll(deleteHistories); } } diff --git a/src/test/java/nextstep/qna/domain/AnswersTest.java b/src/test/java/nextstep/qna/domain/AnswersTest.java new file mode 100644 index 0000000000..6f33063d2e --- /dev/null +++ b/src/test/java/nextstep/qna/domain/AnswersTest.java @@ -0,0 +1,53 @@ +package nextstep.qna.domain; + +import nextstep.qna.CannotDeleteException; +import nextstep.users.domain.NsUserTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class AnswersTest { + + @Test + public void 모든_답변이_같은작성자면_삭제_가능() throws CannotDeleteException { + Question question = new Question(1L, NsUserTest.JAVAJIGI, "title", "contents"); + Answer answer1 = new Answer(11L, NsUserTest.JAVAJIGI, question, "answer1"); + Answer answer2 = new Answer(12L, NsUserTest.JAVAJIGI, question, "answer2"); + Answers answers = new Answers(Arrays.asList(answer1, answer2)); + + List delete = answers.delete(NsUserTest.JAVAJIGI); + } + + @Test + public void 다른_작성자의_답변이_있으면_예외() { + Question question = new Question(1L, NsUserTest.JAVAJIGI, "title", "contents"); + Answer answer1 = new Answer(11L, NsUserTest.JAVAJIGI, question, "answer1"); + Answer answer2 = new Answer(12L, NsUserTest.SANJIGI, question, "answer2"); + Answers answers = new Answers(Arrays.asList(answer1, answer2)); + + assertThatThrownBy(() -> { + answers.delete(NsUserTest.JAVAJIGI); + }).isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("다른 사람의 답변이 존재하여 삭제할 수 없습니다."); + } + + @Test + public void 모든_답변_삭제_및_DeleteHistory_반환() throws CannotDeleteException { + Question question = new Question(1L, NsUserTest.JAVAJIGI, "title", "contents"); + Answer answer1 = new Answer(11L, NsUserTest.JAVAJIGI, question, "answer1"); + Answer answer2 = new Answer(12L, NsUserTest.JAVAJIGI, question, "answer2"); + Answers answers = new Answers(Arrays.asList(answer1, answer2)); + + List deleteHistories = answers.delete(NsUserTest.JAVAJIGI); + + assertThat(answer1.isDeleted()).isTrue(); + assertThat(answer2.isDeleted()).isTrue(); + assertThat(deleteHistories).hasSize(2); + assertThat(deleteHistories.get(0).getContentType()).isEqualTo(ContentType.ANSWER); + assertThat(deleteHistories.get(1).getContentType()).isEqualTo(ContentType.ANSWER); + } +} diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 3b87823963..b4e205752f 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -1,8 +1,59 @@ package nextstep.qna.domain; +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUserTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; 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 + public void 답변없는질문_삭제() throws CannotDeleteException { + List deleteHistories = Q1.delete(NsUserTest.JAVAJIGI); + + assertThat(Q1.isDeleted()).isTrue(); + assertThat(deleteHistories).hasSize(1); + assertThat(deleteHistories.get(0).getContentType()).isEqualTo(ContentType.QUESTION); + } + + @Test + public void 질문자와_답변자가_같은경우_삭제() throws CannotDeleteException { + Answer answer1 = new Answer(11L, NsUserTest.JAVAJIGI, Q1, "answer1"); + Answer answer2 = new Answer(12L, NsUserTest.JAVAJIGI, Q1, "answer2"); + Q1.addAnswer(answer1); + Q1.addAnswer(answer2); + + List deleteHistories = Q1.delete(NsUserTest.JAVAJIGI); + + assertThat(Q1.isDeleted()).isTrue(); + assertThat(answer1.isDeleted()).isTrue(); + assertThat(answer2.isDeleted()).isTrue(); + assertThat(deleteHistories).hasSize(3); + } + + @Test + public void 다른사람의_답변이_있는경우_삭제() { + Question question = new Question(1L, NsUserTest.JAVAJIGI, "title", "contents"); + Answer answer = new Answer(11L, NsUserTest.SANJIGI, question, "answer"); + question.addAnswer(answer); + + assertThatThrownBy(() -> { + question.delete(NsUserTest.JAVAJIGI); + }).isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("다른 사람의 답변이 존재하여 삭제할 수 없습니다."); + } + + @Test + public void 질문자가_아닌경우_삭제불가() { + assertThatThrownBy(() -> { + Q1.delete(NsUserTest.SANJIGI); + }).isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("질문을 삭제할 권한이 없습니다."); + } }