diff --git a/README.md b/README.md index 498506166..4371c7e59 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,18 @@ * 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) \ No newline at end of file +* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) + +--- + +## 기능 목록 + +### 질문 삭제하기 + +- 질문자 = 로그인 사용자 아니면 삭제 불가 +- 답변 중 다른 사람이 쓴 답변이 있으면 삭제 불가 +- 답변이 없으면 삭제 가능 +- 질문자와 답변자가 모두 동일하면 삭제 가능 +- 삭제되면 질문 상태 변경 +- 삭제되면 모든 답변 상태 변경 +- 삭제 이력 생성됨 \ No newline at end of file diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index cf681811e..e72f8f552 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -1,67 +1,39 @@ package nextstep.qna.domain; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import nextstep.qna.CannotDeleteException; import nextstep.qna.NotFoundException; import nextstep.qna.UnAuthorizedException; import nextstep.users.domain.NsUser; -import java.time.LocalDateTime; - -public class Answer { - private Long id; - - private NsUser writer; +public class Answer extends SoftDeletableBaseEntity { private Question question; private String contents; - private boolean deleted = false; - - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; - - public Answer() { - } - public Answer(NsUser writer, Question question, String contents) { this(null, writer, question, contents); } public Answer(Long id, NsUser writer, Question question, String contents) { - this.id = id; - if(writer == null) { - throw new UnAuthorizedException(); - } - - if(question == null) { - throw new NotFoundException(); - } - - this.writer = writer; + super(id, writer); this.question = question; this.contents = contents; } - public Long getId() { - return id; - } - - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } - - public boolean isDeleted() { - return deleted; - } + public static Answer create(NsUser writer, Question question, String contents) { + if (writer == null) { + throw new UnAuthorizedException(); + } - public boolean isOwner(NsUser writer) { - return this.writer.equals(writer); - } + if (question == null) { + throw new NotFoundException(); + } - public NsUser getWriter() { - return writer; + return new Answer(null, writer, question, contents); } public String getContents() { @@ -72,8 +44,22 @@ public void toQuestion(Question question) { this.question = question; } + public void delete(NsUser user) throws CannotDeleteException { + if (!isOwner(user)) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + markAsDeleted(); + } + + public List toDeleteHistories() { + List deleteHistories = new ArrayList<>(); + deleteHistories.add( + new DeleteHistory(ContentType.ANSWER, getId(), getWriter(), LocalDateTime.now())); + return deleteHistories; + } + @Override public String toString() { - return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]"; + return "Answer [id=" + getId() + ", writer=" + getWriter() + ", 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 000000000..02db7eee4 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/Answers.java @@ -0,0 +1,44 @@ +package nextstep.qna.domain; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import nextstep.qna.CannotDeleteException; +import nextstep.users.domain.NsUser; + +public class Answers { + + private final List answers; + + public Answers(Answer... answers) { + this(new ArrayList<>(Arrays.asList(answers))); + } + + public Answers(List answers) { + this.answers = answers; + } + + public List answers() { + return Collections.unmodifiableList(new ArrayList<>(answers)); + } + + public void add(Answer answer) { + answers.add(answer); + } + + public void delete(NsUser user) throws CannotDeleteException { + for (Answer answer : answers) { + answer.delete(user); + } + } + + public List toDeleteHistories() throws CannotDeleteException { + List deleteHistories = new ArrayList<>(); + for (Answer answer : answers) { + deleteHistories.addAll(answer.toDeleteHistories()); + } + return deleteHistories; + } + +} diff --git a/src/main/java/nextstep/qna/domain/BaseTimeEntity.java b/src/main/java/nextstep/qna/domain/BaseTimeEntity.java new file mode 100644 index 000000000..69b4f1e94 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/BaseTimeEntity.java @@ -0,0 +1,11 @@ +package nextstep.qna.domain; + +import java.time.LocalDateTime; + +public abstract class BaseTimeEntity { + + private LocalDateTime createdDate = LocalDateTime.now(); + + private LocalDateTime updatedDate; + +} diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index b623c52c7..1ae59c08d 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -1,66 +1,31 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUser; - import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import nextstep.qna.CannotDeleteException; +import nextstep.users.domain.NsUser; -public class Question { - private Long id; - - private String title; - - private String contents; - - private NsUser writer; - - private List answers = new ArrayList<>(); - - private boolean deleted = false; - - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; +public class Question extends SoftDeletableBaseEntity { + private QuestionContent content; - public Question() { - } + private Answers answers = new Answers(); - public Question(NsUser writer, String title, String contents) { - this(0L, writer, title, contents); + public Question(NsUser writer, QuestionContent content) { + this(0L, writer, content); } - 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 Question(Long id, NsUser writer, QuestionContent content) { + super(id, writer); + this.content = content; } public String getTitle() { - return title; - } - - public Question setTitle(String title) { - this.title = title; - return this; + return content.title(); } public String getContents() { - return contents; - } - - public Question setContents(String contents) { - this.contents = contents; - return this; - } - - public NsUser getWriter() { - return writer; + return content.contents(); } public void addAnswer(Answer answer) { @@ -68,25 +33,28 @@ public void addAnswer(Answer answer) { answers.add(answer); } - public boolean isOwner(NsUser loginUser) { - return writer.equals(loginUser); - } - - public Question setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + public Answers getAnswers() { + return answers; } - public boolean isDeleted() { - return deleted; + public void delete(NsUser user) throws CannotDeleteException { + if (!isOwner(user)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } + markAsDeleted(); + answers.delete(user); } - public List getAnswers() { - return answers; + public List toDeleteHistories() throws CannotDeleteException { + List deleteHistories = new ArrayList<>(); + deleteHistories.add( + new DeleteHistory(ContentType.QUESTION, getId(), getWriter(), LocalDateTime.now())); + deleteHistories.addAll(answers.toDeleteHistories()); + return deleteHistories; } @Override public String toString() { - return "Question [id=" + getId() + ", title=" + title + ", contents=" + contents + ", writer=" + writer + "]"; + return "Question [id=" + getId() + ", content=" + content + ", writer=" + getWriter() + "]"; } } 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 000000000..6a5d4c77e --- /dev/null +++ b/src/main/java/nextstep/qna/domain/QuestionContent.java @@ -0,0 +1,28 @@ +package nextstep.qna.domain; + +public class QuestionContent { + + private final String title; + + private final String contents; + + public QuestionContent(String title, String contents) { + this.title = title; + this.contents = contents; + } + + public String title() { + return title; + } + + public String contents() { + return contents; + } + + @Override + public String toString() { + return "QuestionContent{" + + ", title=" + title + ", contents=" + contents + + '}'; + } +} diff --git a/src/main/java/nextstep/qna/domain/SoftDeletableBaseEntity.java b/src/main/java/nextstep/qna/domain/SoftDeletableBaseEntity.java new file mode 100644 index 000000000..9c0dd9a0c --- /dev/null +++ b/src/main/java/nextstep/qna/domain/SoftDeletableBaseEntity.java @@ -0,0 +1,38 @@ +package nextstep.qna.domain; + +import nextstep.users.domain.NsUser; + +public abstract class SoftDeletableBaseEntity extends BaseTimeEntity { + + private Long id; + + private NsUser writer; + + private boolean deleted = false; + + protected SoftDeletableBaseEntity(Long id, NsUser writer) { + this.id = id; + this.writer = writer; + } + + public Long getId() { + return id; + } + + public NsUser getWriter() { + return writer; + } + + protected void markAsDeleted() { + this.deleted = true; + } + + public boolean isOwner(NsUser loginUser) { + return writer.equals(loginUser); + } + + public boolean isDeleted() { + return deleted; + } + +} diff --git a/src/main/java/nextstep/qna/service/QnAService.java b/src/main/java/nextstep/qna/service/QnAService.java index 5741c84d6..c591494ae 100644 --- a/src/main/java/nextstep/qna/service/QnAService.java +++ b/src/main/java/nextstep/qna/service/QnAService.java @@ -1,17 +1,15 @@ package nextstep.qna.service; +import javax.annotation.Resource; import nextstep.qna.CannotDeleteException; import nextstep.qna.NotFoundException; -import nextstep.qna.domain.*; +import nextstep.qna.domain.AnswerRepository; +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") public class QnAService { @Resource(name = "questionRepository") @@ -26,24 +24,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())); - } - deleteHistoryService.saveAll(deleteHistories); + question.delete(loginUser); + deleteHistoryService.saveAll(question.toDeleteHistories()); } } diff --git a/src/test/java/nextstep/qna/domain/AnswerTest.java b/src/test/java/nextstep/qna/domain/AnswerTest.java index 8e80ffb42..7e48e33b1 100644 --- a/src/test/java/nextstep/qna/domain/AnswerTest.java +++ b/src/test/java/nextstep/qna/domain/AnswerTest.java @@ -1,8 +1,47 @@ package nextstep.qna.domain; +import static nextstep.users.domain.NsUserTest.JAVAJIGI; +import static nextstep.users.domain.NsUserTest.SANJIGI; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.qna.CannotDeleteException; import nextstep.users.domain.NsUserTest; +import org.junit.jupiter.api.DisplayName; +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"); + public static final Answer A3 = new Answer(NsUserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents3"); + + @DisplayName("답변 작성자가 로그인 사용자일 경우 삭제 가능하다") + @Test + void shouldNotThrow_whenUserIsOwner() { + assertThatCode(() -> A1.delete(JAVAJIGI)) + .doesNotThrowAnyException(); + } + + @DisplayName("답변 중 다른 사람이 쓴 답변이 있는 경우 삭제 불가하다") + @Test + void shouldThrow_whenUserAndWriterDifferent() { + assertThatThrownBy(() -> A1.delete(SANJIGI)) + .isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } + + @DisplayName("답변 작성자가 삭제할 경우 상태가 변경된다") + @Test + void shouldChangeDeletedStatus_whenDeleteByOwner() throws CannotDeleteException { + A1.delete(JAVAJIGI); + assertThat(A1.isDeleted()).isTrue(); + } + + @DisplayName("삭제 이력을 반환한다") + @Test + void returnDeleteHistory() throws CannotDeleteException { + assertThat(A1.toDeleteHistories()).hasSize(1); + } + } 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 000000000..4aee60595 --- /dev/null +++ b/src/test/java/nextstep/qna/domain/AnswersTest.java @@ -0,0 +1,28 @@ +package nextstep.qna.domain; + +import static nextstep.qna.domain.AnswerTest.A1; +import static nextstep.qna.domain.AnswerTest.A3; +import static org.assertj.core.api.Assertions.assertThat; + +import nextstep.qna.CannotDeleteException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AnswersTest { + + @DisplayName("삭제 이력을 반환한다") + @Test + void returnDeleteHistory() throws CannotDeleteException { + Answers answers = new Answers(A1, A3); + assertThat(answers.toDeleteHistories()).hasSize(2); + } + + @DisplayName("Answers에 Answer를 추가할 수 있다") + @Test + void addAnswerToAnswers() { + Answers answers = new Answers(A1); + answers.add(A3); + assertThat(answers.answers()).hasSize(2); + } + +} \ No newline at end of file diff --git a/src/test/java/nextstep/qna/domain/QuestionContentTest.java b/src/test/java/nextstep/qna/domain/QuestionContentTest.java new file mode 100644 index 000000000..46b89b02b --- /dev/null +++ b/src/test/java/nextstep/qna/domain/QuestionContentTest.java @@ -0,0 +1,19 @@ +package nextstep.qna.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class QuestionContentTest { + public static final QuestionContent QC1 = new QuestionContent("title1", "contents1"); + public static final QuestionContent QC2 = new QuestionContent("title2", "contents2"); + + @DisplayName("QuestionContent 생성") + @Test + void createQuestionContent() { + assertThat(QC1.title()).isEqualTo("title1"); + assertThat(QC1.contents()).isEqualTo("contents1"); + } + +} \ No newline at end of file diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 3b8782396..5353a3457 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -1,8 +1,46 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUserTest; +import static nextstep.qna.domain.QuestionContentTest.QC1; +import static nextstep.qna.domain.QuestionContentTest.QC2; +import static nextstep.users.domain.NsUserTest.JAVAJIGI; +import static nextstep.users.domain.NsUserTest.SANJIGI; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import nextstep.qna.CannotDeleteException; +import org.junit.jupiter.api.DisplayName; +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"); + public static final Question Q1 = new Question(JAVAJIGI, QC1); + public static final Question Q2 = new Question(SANJIGI, QC2); + + @DisplayName("질문 작성자는 자신의 질문을 삭제할 수 있다") + @Test + void shouldNotThrow_whenUserIsOwnerAndNoOtherAnswers() { + assertThatCode(() -> Q2.delete(SANJIGI)) + .doesNotThrowAnyException(); + } + + @DisplayName("질문자와 로그인 사용자가 다른 경우 삭제 불가하다") + @Test + void shouldThrow_whenUserAndWriterDifferent() { + assertThatThrownBy(() -> Q1.delete(SANJIGI)) + .isInstanceOf(CannotDeleteException.class) + .hasMessageContaining("질문을 삭제할 권한이 없습니다"); + } + + @DisplayName("삭제 가능한 경우 상태가 변경된다") + @Test + void shouldChangeDeletedStatus_whenDeleteByOwner() throws CannotDeleteException { + Q2.delete(SANJIGI); + assertThat(Q2.isDeleted()).isTrue(); + } + + @DisplayName("삭제 시 삭제 이력을 반환한다") + @Test + void shouldReturnDeleteHistory_whenDeletePossible() throws CannotDeleteException { + assertThat(Q1.toDeleteHistories()).hasSize(1); + } } diff --git a/src/test/java/nextstep/qna/service/QnaServiceTest.java b/src/test/java/nextstep/qna/service/QnaServiceTest.java index e1e943c23..cb1da9f39 100644 --- a/src/test/java/nextstep/qna/service/QnaServiceTest.java +++ b/src/test/java/nextstep/qna/service/QnaServiceTest.java @@ -1,7 +1,22 @@ package nextstep.qna.service; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; import nextstep.qna.CannotDeleteException; -import nextstep.qna.domain.*; +import nextstep.qna.domain.Answer; +import nextstep.qna.domain.ContentType; +import nextstep.qna.domain.DeleteHistory; +import nextstep.qna.domain.Question; +import nextstep.qna.domain.QuestionContentTest; +import nextstep.qna.domain.QuestionRepository; +import nextstep.qna.domain.QuestionTest; import nextstep.users.domain.NsUserTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,16 +25,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) public class QnaServiceTest { @Mock @@ -36,7 +41,7 @@ public class QnaServiceTest { @BeforeEach public void setUp() throws Exception { - question = new Question(1L, NsUserTest.JAVAJIGI, "title1", "contents1"); + question = new Question(1L, NsUserTest.JAVAJIGI, QuestionContentTest.QC1); answer = new Answer(11L, NsUserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); question.addAnswer(answer); }