diff --git a/src/main/java/nextstep/common/domain/BaseEntity.java b/src/main/java/nextstep/common/domain/BaseEntity.java new file mode 100644 index 000000000..ef5afe1e5 --- /dev/null +++ b/src/main/java/nextstep/common/domain/BaseEntity.java @@ -0,0 +1,49 @@ +package nextstep.common.domain; + +import java.time.LocalDateTime; +import java.util.Objects; + +public abstract class BaseEntity { + private Long id; + private LocalDateTime createdDate; + private LocalDateTime updatedDate; + + protected BaseEntity(Long id) { + this(id, LocalDateTime.now(), LocalDateTime.now()); + } + + public BaseEntity(Long id, LocalDateTime createdDate, LocalDateTime updatedDate) { + this.id = id; + this.createdDate = createdDate; + this.updatedDate = updatedDate; + } + + public Long getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + BaseEntity that = (BaseEntity) o; + return Objects.equals(id, that.id) + && Objects.equals(createdDate, that.createdDate) + && Objects.equals(updatedDate, that.updatedDate); + } + + @Override + public int hashCode() { + return Objects.hash(id, createdDate, updatedDate); + } + + @Override + public String toString() { + return "BaseEntity{" + + "id=" + id + + ", createdDate=" + createdDate + + ", updatedDate=" + updatedDate + + '}'; + } +} diff --git a/src/main/java/nextstep/common/domain/SoftDeletableBaseEntity.java b/src/main/java/nextstep/common/domain/SoftDeletableBaseEntity.java new file mode 100644 index 000000000..36beb14ee --- /dev/null +++ b/src/main/java/nextstep/common/domain/SoftDeletableBaseEntity.java @@ -0,0 +1,56 @@ +package nextstep.common.domain; + +import java.time.LocalDateTime; +import java.util.Objects; + +public abstract class SoftDeletableBaseEntity { + private Long id; + private boolean deleted; + private LocalDateTime createdDate; + private LocalDateTime updatedDate; + + protected SoftDeletableBaseEntity(Long id) { + this(id, false, LocalDateTime.now(), LocalDateTime.now()); + } + + protected SoftDeletableBaseEntity(Long id, LocalDateTime createdAt, LocalDateTime updatedAt) { + this(id, false, createdAt, updatedAt); + } + + protected SoftDeletableBaseEntity(Long id, boolean deleted, LocalDateTime createdDate, + LocalDateTime updatedDate) { + this.id = id; + this.deleted = deleted; + this.createdDate = createdDate; + this.updatedDate = updatedDate; + } + + protected boolean isDeleted() { + return this.deleted; + } + + public void updateDeleted() { + this.deleted = true; + } + + public Long getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + SoftDeletableBaseEntity that = (SoftDeletableBaseEntity) o; + return deleted == that.deleted + && Objects.equals(id, that.id) + && Objects.equals(createdDate, that.createdDate) + && Objects.equals(updatedDate, that.updatedDate); + } + + @Override + public int hashCode() { + return Objects.hash(id, deleted, createdDate, updatedDate); + } +} diff --git a/src/main/java/nextstep/qna/domain/Answer.java b/src/main/java/nextstep/qna/domain/Answer.java index cf681811e..bc2b82959 100644 --- a/src/main/java/nextstep/qna/domain/Answer.java +++ b/src/main/java/nextstep/qna/domain/Answer.java @@ -1,71 +1,58 @@ package nextstep.qna.domain; -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; - +import nextstep.common.domain.SoftDeletableBaseEntity; +import nextstep.qna.exception.unchecked.CannotDeleteException; +import nextstep.qna.exception.unchecked.NotFoundException; +import nextstep.qna.exception.unchecked.UnAuthorizedException; +import nextstep.qna.exception.unchecked.WrongRequestException; + +public class Answer extends SoftDeletableBaseEntity { + private Long writerId; private Question question; + private BoardContent boardContent; - private String contents; - - private boolean deleted = false; - - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; - - public Answer() { + public Answer(long writerId, Question question, String contents) { + this(null, writerId, question, contents); } - 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) { + public Answer(Long id, long writerId, Question question, String contents) { + super(id); + if (writerId <= 0L) { throw new UnAuthorizedException(); } - if(question == null) { + if (question == null) { throw new NotFoundException(); } - this.writer = writer; + this.writerId = writerId; this.question = question; - this.contents = contents; + this.boardContent = new BoardContent("" , contents); } - public Long getId() { - return id; + public boolean isDeleted() { + return super.isDeleted(); } - public Answer setDeleted(boolean deleted) { - this.deleted = deleted; - return this; - } + public void putOnDelete(long requesterId) { + if (!isOwner(requesterId)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } - public boolean isDeleted() { - return deleted; + super.updateDeleted(); } - public boolean isOwner(NsUser writer) { - return this.writer.equals(writer); + public boolean isOwner(long writerId) { + return this.writerId == writerId; } - public NsUser getWriter() { - return writer; - } + public DeleteHistory createAnswerDeleteHistory(LocalDateTime deletedDateTime) { + if (!isDeleted()) { + throw new WrongRequestException("삭제되지 않은 답변은 삭제이력을 생성할 수 없습니다."); + } - public String getContents() { - return contents; + return new DeleteHistory(ContentType.ANSWER, super.getId(), this.writerId, deletedDateTime); } public void toQuestion(Question question) { @@ -74,6 +61,9 @@ public void toQuestion(Question question) { @Override public String toString() { - return "Answer [id=" + getId() + ", writer=" + writer + ", contents=" + contents + "]"; + return "Answer [id=" + super.getId() + + ", writerId=" + writerId + + ", contents=" + boardContent + + "]"; } } diff --git a/src/main/java/nextstep/qna/domain/BoardContent.java b/src/main/java/nextstep/qna/domain/BoardContent.java new file mode 100644 index 000000000..9eb1cb180 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/BoardContent.java @@ -0,0 +1,19 @@ +package nextstep.qna.domain; + +public class BoardContent { + private String title; + private String contents; + + public BoardContent(String title, String contents) { + this.title = title; + this.contents = contents; + } + + @Override + public String toString() { + return "BoardContent{" + + "title='" + title + '\'' + + ", contents='" + contents + '\'' + + '}'; + } +} diff --git a/src/main/java/nextstep/qna/domain/DeleteHistory.java b/src/main/java/nextstep/qna/domain/DeleteHistory.java index 43c37e5e5..80ebe5335 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistory.java +++ b/src/main/java/nextstep/qna/domain/DeleteHistory.java @@ -1,50 +1,53 @@ package nextstep.qna.domain; +import static java.util.Objects.*; + +import nextstep.common.domain.BaseEntity; import nextstep.users.domain.NsUser; import java.time.LocalDateTime; import java.util.Objects; -public class DeleteHistory { - private Long id; - +public class DeleteHistory extends BaseEntity { private ContentType contentType; - private Long contentId; + private Long deletedId; - private NsUser deletedBy; - - private LocalDateTime createdDate = LocalDateTime.now(); - - public DeleteHistory() { + public DeleteHistory(ContentType contentType, Long contentId, Long deletedId, LocalDateTime createdDate) { + this(0L, contentType, contentId, deletedId, createdDate, null); } - public DeleteHistory(ContentType contentType, Long contentId, NsUser deletedBy, LocalDateTime createdDate) { + public DeleteHistory(Long id, ContentType contentType, Long contentId, Long deletedId, LocalDateTime createdDate, LocalDateTime updateDate) { + super(id, createdDate, updateDate); this.contentType = contentType; this.contentId = contentId; - this.deletedBy = deletedBy; - this.createdDate = createdDate; + this.deletedId = deletedId; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (o == null || getClass() != o.getClass()) { + return false; + } DeleteHistory that = (DeleteHistory) o; - return Objects.equals(id, that.id) && - contentType == that.contentType && - Objects.equals(contentId, that.contentId) && - Objects.equals(deletedBy, that.deletedBy); + return super.equals(o) + && contentType == that.contentType + && Objects.equals(contentId, that.contentId) + && Objects.equals(deletedId, that.deletedId); } @Override public int hashCode() { - return Objects.hash(id, contentType, contentId, deletedBy); + return hash(super.hashCode(), contentType, contentId, deletedId); } @Override public String toString() { - return "DeleteHistory [id=" + id + ", contentType=" + contentType + ", contentId=" + contentId + ", deletedBy=" - + deletedBy + ", createdDate=" + createdDate + "]"; + return "DeleteHistory{" + + "id=" + super.toString() + + ", contentType=" + contentType + + ", contentId=" + contentId + + ", deletedId=" + deletedId + + '}'; } } diff --git a/src/main/java/nextstep/qna/domain/QnADomainService.java b/src/main/java/nextstep/qna/domain/QnADomainService.java new file mode 100644 index 000000000..7a7b6c1c6 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/QnADomainService.java @@ -0,0 +1,19 @@ +package nextstep.qna.domain; + +import java.time.LocalDateTime; +import java.util.List; + +public class QnADomainService { + + // TODO : 피드백 주신 부분 잘보았습니다. + // 저는 개인적으로 도메인 로직은 순수하게 유지하는게 가장 좋다고 생각합니다만. + // 스프링을 활용한 의존성주입이 필요한 예외적인 상황(불가피한 외부API 호출, 이메일 등등..)에서는 허용하고 사용해주는것도 좋다고 생각합니다. + // 다만 단계를 거쳐볼것 같아요. 1. 순수 객체 -> 2. 클래스 메서드(static) 집합 객체(=외부 bean 필요없이 캐싱 작업이 필요힌 경우) -> 3. 외부 bean이 불가피하게 필요한경우 bean & di + public List deleteQuestion( + long requesterId, Question question, LocalDateTime fixedDeletedDateTime + ) { + question.putOnDelete(requesterId); + + return question.bringAllDeleteHistories(fixedDeletedDateTime); + } +} diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index b623c52c7..def6094f4 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -1,92 +1,100 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUser; +import static java.util.Objects.isNull; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import nextstep.common.domain.SoftDeletableBaseEntity; +import nextstep.qna.exception.unchecked.CannotDeleteException; +import nextstep.qna.exception.unchecked.WrongRequestException; -public class Question { - private Long id; - - private String title; - - private String contents; - - private NsUser writer; - +public class Question extends SoftDeletableBaseEntity { + private BoardContent boardContent; + private Long writerId; private List answers = new ArrayList<>(); - private boolean deleted = false; - - private LocalDateTime createdDate = LocalDateTime.now(); - - private LocalDateTime updatedDate; - - public Question() { + public Question(long writerId, String title, String contents) { + this(0L, writerId, title, contents); } - public Question(NsUser writer, String title, String contents) { - this(0L, writer, title, contents); + public Question(Long id, long writerId, String title, String contents) { + super(id); + this.writerId = writerId; + this.boardContent = new BoardContent(title, contents); } - public Question(Long id, NsUser writer, String title, String contents) { - this.id = id; - this.writer = writer; - this.title = title; - this.contents = contents; + public boolean isOwner(long requesterId) { + return writerId == requesterId; } - public Long getId() { - return id; + public void addAnswer(Answer answer) { + answer.toQuestion(this); + answers.add(answer); } - public String getTitle() { - return title; + public boolean isDeleted() { + return super.isDeleted(); } - public Question setTitle(String title) { - this.title = title; - return this; + public boolean hasAnswers() { + return !this.answers.isEmpty(); } - public String getContents() { - return contents; + public boolean isAllSameContentsWriter() { + return this.answers.stream() + .allMatch(answer -> answer.isOwner(this.writerId)); } - public Question setContents(String contents) { - this.contents = contents; - return this; - } + public void putOnDelete(long requesterId) { + if (requesterId <= 0L) { + throw new IllegalArgumentException("잘못된 요청자 정보 입니다."); + } - public NsUser getWriter() { - return writer; - } + if (!isOwner(requesterId)) { + throw new CannotDeleteException("질문을 삭제할 권한이 없습니다."); + } - public void addAnswer(Answer answer) { - answer.toQuestion(this); - answers.add(answer); - } + if (hasAnswers() && !isAllSameContentsWriter()) { + throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); + } - public boolean isOwner(NsUser loginUser) { - return writer.equals(loginUser); + super.updateDeleted(); + putOnAllAnswersDelete(requesterId); } - public Question setDeleted(boolean deleted) { - this.deleted = deleted; - return this; + public void putOnAllAnswersDelete(long requesterId) { + this.answers.forEach(answer -> answer.putOnDelete(requesterId)); } - public boolean isDeleted() { - return deleted; + public DeleteHistory createQuestionDeleteHistory(LocalDateTime deletedDateTime) { + if (isNull(deletedDateTime)) { + throw new WrongRequestException("삭제시점은 필수 입니다."); + } + + if (!super.isDeleted()) { + throw new WrongRequestException("삭제되지 않은 질문은 삭제이력을 생성할 수 없습니다."); + } + + return new DeleteHistory(ContentType.QUESTION, super.getId(), this.writerId, deletedDateTime); } - public List getAnswers() { - return answers; + public List bringAllDeleteHistories(LocalDateTime deletedDateTime) { + List deleteHistories = new ArrayList<>(); + deleteHistories.add(createQuestionDeleteHistory(deletedDateTime)); + + for (Answer answer : answers) { + deleteHistories.add(answer.createAnswerDeleteHistory(deletedDateTime)); + } + + return deleteHistories; } @Override public String toString() { - return "Question [id=" + getId() + ", title=" + title + ", contents=" + contents + ", writer=" + writer + "]"; + return "Question [id=" + super.getId() + + ", boardContents=" + boardContent + + ", writerId=" + writerId + + "]"; } } diff --git a/src/main/java/nextstep/qna/UnAuthenticationException.java b/src/main/java/nextstep/qna/exception/checked/UnAuthenticationException.java similarity index 94% rename from src/main/java/nextstep/qna/UnAuthenticationException.java rename to src/main/java/nextstep/qna/exception/checked/UnAuthenticationException.java index bff057a2a..4019ba86f 100644 --- a/src/main/java/nextstep/qna/UnAuthenticationException.java +++ b/src/main/java/nextstep/qna/exception/checked/UnAuthenticationException.java @@ -1,4 +1,4 @@ -package nextstep.qna; +package nextstep.qna.exception.checked; public class UnAuthenticationException extends Exception { private static final long serialVersionUID = 1L; diff --git a/src/main/java/nextstep/qna/CannotDeleteException.java b/src/main/java/nextstep/qna/exception/unchecked/CannotDeleteException.java similarity index 57% rename from src/main/java/nextstep/qna/CannotDeleteException.java rename to src/main/java/nextstep/qna/exception/unchecked/CannotDeleteException.java index a8d9d2832..13b83292b 100644 --- a/src/main/java/nextstep/qna/CannotDeleteException.java +++ b/src/main/java/nextstep/qna/exception/unchecked/CannotDeleteException.java @@ -1,6 +1,6 @@ -package nextstep.qna; +package nextstep.qna.exception.unchecked; -public class CannotDeleteException extends Exception { +public class CannotDeleteException extends RuntimeException { private static final long serialVersionUID = 1L; public CannotDeleteException(String message) { diff --git a/src/main/java/nextstep/qna/ForbiddenException.java b/src/main/java/nextstep/qna/exception/unchecked/ForbiddenException.java similarity index 81% rename from src/main/java/nextstep/qna/ForbiddenException.java rename to src/main/java/nextstep/qna/exception/unchecked/ForbiddenException.java index 4e858b50d..3218a83c3 100644 --- a/src/main/java/nextstep/qna/ForbiddenException.java +++ b/src/main/java/nextstep/qna/exception/unchecked/ForbiddenException.java @@ -1,4 +1,4 @@ -package nextstep.qna; +package nextstep.qna.exception.unchecked; public class ForbiddenException extends RuntimeException{ public ForbiddenException() { diff --git a/src/main/java/nextstep/qna/NotFoundException.java b/src/main/java/nextstep/qna/exception/unchecked/NotFoundException.java similarity index 59% rename from src/main/java/nextstep/qna/NotFoundException.java rename to src/main/java/nextstep/qna/exception/unchecked/NotFoundException.java index 5ac272b8b..e23899d9f 100644 --- a/src/main/java/nextstep/qna/NotFoundException.java +++ b/src/main/java/nextstep/qna/exception/unchecked/NotFoundException.java @@ -1,4 +1,4 @@ -package nextstep.qna; +package nextstep.qna.exception.unchecked; public class NotFoundException extends RuntimeException { } diff --git a/src/main/java/nextstep/qna/UnAuthorizedException.java b/src/main/java/nextstep/qna/exception/unchecked/UnAuthorizedException.java similarity index 94% rename from src/main/java/nextstep/qna/UnAuthorizedException.java rename to src/main/java/nextstep/qna/exception/unchecked/UnAuthorizedException.java index 1b8fb2c14..f2c92aabe 100644 --- a/src/main/java/nextstep/qna/UnAuthorizedException.java +++ b/src/main/java/nextstep/qna/exception/unchecked/UnAuthorizedException.java @@ -1,4 +1,4 @@ -package nextstep.qna; +package nextstep.qna.exception.unchecked; public class UnAuthorizedException extends RuntimeException { private static final long serialVersionUID = 1L; diff --git a/src/main/java/nextstep/qna/exception/unchecked/WrongRequestException.java b/src/main/java/nextstep/qna/exception/unchecked/WrongRequestException.java new file mode 100644 index 000000000..1708571a8 --- /dev/null +++ b/src/main/java/nextstep/qna/exception/unchecked/WrongRequestException.java @@ -0,0 +1,9 @@ +package nextstep.qna.exception.unchecked; + +public class WrongRequestException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public WrongRequestException(String message) { + super(message); + } +} diff --git a/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java b/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java index 7c27c6dd8..65a826e29 100644 --- a/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java +++ b/src/main/java/nextstep/qna/infrastructure/JdbcAnswerRepository.java @@ -1,7 +1,7 @@ package nextstep.qna.infrastructure; import nextstep.qna.domain.Answer; -import nextstep.qna.domain.AnswerRepository; +import nextstep.qna.service.repository.AnswerRepository; import org.springframework.stereotype.Repository; import java.util.List; diff --git a/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java b/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java index 7ff018887..fd0470765 100644 --- a/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java +++ b/src/main/java/nextstep/qna/infrastructure/JdbcDeleteHistoryRepository.java @@ -1,7 +1,7 @@ package nextstep.qna.infrastructure; import nextstep.qna.domain.DeleteHistory; -import nextstep.qna.domain.DeleteHistoryRepository; +import nextstep.qna.service.repository.DeleteHistoryRepository; import org.springframework.stereotype.Repository; import java.util.List; diff --git a/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java b/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java index a6e4062dc..ab4868cf5 100644 --- a/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java +++ b/src/main/java/nextstep/qna/infrastructure/JdbcQuestionRepository.java @@ -1,7 +1,7 @@ package nextstep.qna.infrastructure; import nextstep.qna.domain.Question; -import nextstep.qna.domain.QuestionRepository; +import nextstep.qna.service.repository.QuestionRepository; import org.springframework.stereotype.Repository; import java.util.Optional; diff --git a/src/main/java/nextstep/qna/service/DeleteHistoryService.java b/src/main/java/nextstep/qna/service/DeleteHistoryService.java index 7599dca96..7d11fe86a 100644 --- a/src/main/java/nextstep/qna/service/DeleteHistoryService.java +++ b/src/main/java/nextstep/qna/service/DeleteHistoryService.java @@ -1,7 +1,7 @@ package nextstep.qna.service; import nextstep.qna.domain.DeleteHistory; -import nextstep.qna.domain.DeleteHistoryRepository; +import nextstep.qna.service.repository.DeleteHistoryRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/nextstep/qna/service/QnAService.java b/src/main/java/nextstep/qna/service/QnAService.java index 5741c84d6..87a0d31fd 100644 --- a/src/main/java/nextstep/qna/service/QnAService.java +++ b/src/main/java/nextstep/qna/service/QnAService.java @@ -1,49 +1,33 @@ package nextstep.qna.service; -import nextstep.qna.CannotDeleteException; -import nextstep.qna.NotFoundException; -import nextstep.qna.domain.*; -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; +import javax.annotation.Resource; +import nextstep.qna.domain.DeleteHistory; +import nextstep.qna.domain.QnADomainService; +import nextstep.qna.domain.Question; +import nextstep.qna.exception.unchecked.NotFoundException; +import nextstep.qna.service.repository.QuestionRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service("qnaService") public class QnAService { + @Resource(name = "questionRepository") private QuestionRepository questionRepository; - @Resource(name = "answerRepository") - private AnswerRepository answerRepository; - @Resource(name = "deleteHistoryService") private DeleteHistoryService deleteHistoryService; @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("질문을 삭제할 권한이 없습니다."); - } + public void deleteQuestion(long requesterId, long questionId, LocalDateTime fixedDeletedDateTime) { + Question question = questionRepository.findById(questionId) + .orElseThrow(NotFoundException::new); - List answers = question.getAnswers(); - for (Answer answer : answers) { - if (!answer.isOwner(loginUser)) { - throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다."); - } - } + List deleteHistories = new QnADomainService() + .deleteQuestion(requesterId, question, fixedDeletedDateTime); - 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); } } diff --git a/src/main/java/nextstep/qna/domain/AnswerRepository.java b/src/main/java/nextstep/qna/service/repository/AnswerRepository.java similarity index 59% rename from src/main/java/nextstep/qna/domain/AnswerRepository.java rename to src/main/java/nextstep/qna/service/repository/AnswerRepository.java index bb894f84c..579c9cc3d 100644 --- a/src/main/java/nextstep/qna/domain/AnswerRepository.java +++ b/src/main/java/nextstep/qna/service/repository/AnswerRepository.java @@ -1,6 +1,7 @@ -package nextstep.qna.domain; +package nextstep.qna.service.repository; import java.util.List; +import nextstep.qna.domain.Answer; public interface AnswerRepository { List findByQuestion(Long questionId); diff --git a/src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java b/src/main/java/nextstep/qna/service/repository/DeleteHistoryRepository.java similarity index 60% rename from src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java rename to src/main/java/nextstep/qna/service/repository/DeleteHistoryRepository.java index e8671add7..968791209 100644 --- a/src/main/java/nextstep/qna/domain/DeleteHistoryRepository.java +++ b/src/main/java/nextstep/qna/service/repository/DeleteHistoryRepository.java @@ -1,6 +1,7 @@ -package nextstep.qna.domain; +package nextstep.qna.service.repository; import java.util.List; +import nextstep.qna.domain.DeleteHistory; public interface DeleteHistoryRepository { diff --git a/src/main/java/nextstep/qna/domain/QuestionRepository.java b/src/main/java/nextstep/qna/service/repository/QuestionRepository.java similarity index 58% rename from src/main/java/nextstep/qna/domain/QuestionRepository.java rename to src/main/java/nextstep/qna/service/repository/QuestionRepository.java index ec354bb3f..ba343af71 100644 --- a/src/main/java/nextstep/qna/domain/QuestionRepository.java +++ b/src/main/java/nextstep/qna/service/repository/QuestionRepository.java @@ -1,6 +1,7 @@ -package nextstep.qna.domain; +package nextstep.qna.service.repository; import java.util.Optional; +import nextstep.qna.domain.Question; public interface QuestionRepository { Optional findById(Long id); diff --git a/src/main/java/nextstep/users/domain/LogInPairKey.java b/src/main/java/nextstep/users/domain/LogInPairKey.java new file mode 100644 index 000000000..813bfdf7d --- /dev/null +++ b/src/main/java/nextstep/users/domain/LogInPairKey.java @@ -0,0 +1,48 @@ +package nextstep.users.domain; + +import java.util.Objects; + +public class LogInPairKey { + private String userId; + private String password; + + public LogInPairKey(String userId, String password) { + this.userId = userId; + this.password = password; + } + + public boolean matchUser(String targetUserId, String targetPassword) { + return matchUserId(targetUserId) && matchPassword(targetPassword); + } + + private boolean matchUserId(String targetUserId) { + return this.userId.equals(targetUserId); + } + + private boolean matchPassword(String targetPassword) { + return password.equals(targetPassword); + } + + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + LogInPairKey that = (LogInPairKey) o; + return Objects.equals(userId, that.userId) && Objects.equals(password, + that.password); + } + + @Override + public int hashCode() { + return Objects.hash(userId, password); + } + + @Override + public String toString() { + return "LogInPairKey{" + + "userId='" + userId + '\'' + + '}'; + } +} diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java index 62ec5138c..5f90332af 100755 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -1,90 +1,43 @@ package nextstep.users.domain; -import nextstep.qna.UnAuthorizedException; +import nextstep.common.domain.BaseEntity; +import nextstep.qna.exception.unchecked.UnAuthorizedException; import java.time.LocalDateTime; import java.util.Objects; -public class NsUser { - public static final GuestNsUser GUEST_USER = new GuestNsUser(); - - private Long id; - - private String userId; - - private String password; +public class NsUser extends BaseEntity { + private LogInPairKey logInPairKey; private String name; private String email; - private LocalDateTime createdAt; - - private LocalDateTime updatedAt; - - public NsUser() { - } - public NsUser(Long id, String userId, String password, String name, String email) { this(id, userId, password, name, email, LocalDateTime.now(), null); } public NsUser(Long id, String userId, String password, String name, String email, LocalDateTime createdAt, LocalDateTime updatedAt) { - this.id = id; - this.userId = userId; - this.password = password; + super(id, createdAt, updatedAt); + this.logInPairKey = new LogInPairKey(userId, password); this.name = name; this.email = email; - this.createdAt = createdAt; - this.updatedAt = updatedAt; } public Long getId() { - return id; - } - - public String getUserId() { - return userId; - } - - public NsUser setUserId(String userId) { - this.userId = userId; - return this; - } - - public String getPassword() { - return password; - } - - public NsUser setPassword(String password) { - this.password = password; - return this; + return super.getId(); } public String getName() { return name; } - public NsUser setName(String name) { - this.name = name; - return this; - } - - public String getEmail() { - return email; - } - - public NsUser setEmail(String email) { - this.email = email; - return this; - } - public void update(NsUser loginUser, NsUser target) { - if (!matchUserId(loginUser.getUserId())) { + if (!target.matchLoginPairKey(this.logInPairKey)) { throw new UnAuthorizedException(); } - if (!matchPassword(target.getPassword())) { + if (!loginUser.matchLoginPairKey(this.logInPairKey)) { throw new UnAuthorizedException(); } @@ -92,47 +45,17 @@ public void update(NsUser loginUser, NsUser target) { this.email = target.email; } - public boolean matchUser(NsUser target) { - return matchUserId(target.getUserId()); - } - - private boolean matchUserId(String userId) { - return this.userId.equals(userId); - } - - public boolean matchPassword(String targetPassword) { - return password.equals(targetPassword); - } - - public boolean equalsNameAndEmail(NsUser target) { - if (Objects.isNull(target)) { - return false; - } - - return name.equals(target.name) && - email.equals(target.email); - } - - public boolean isGuestUser() { - return false; - } - - private static class GuestNsUser extends NsUser { - @Override - public boolean isGuestUser() { - return true; - } + private boolean matchLoginPairKey(LogInPairKey targetKey) { + return this.logInPairKey.equals(targetKey); } @Override public String toString() { return "NsUser{" + - "id=" + id + - ", userId='" + userId + '\'' + + "id=" + getId() + + ", userId='" + logInPairKey + '\'' + ", name='" + name + '\'' + ", email='" + email + '\'' + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + '}'; } } diff --git a/src/test/java/nextstep/qna/domain/AnswerTest.java b/src/test/java/nextstep/qna/domain/AnswerTest.java index 8e80ffb42..523b9f0c9 100644 --- a/src/test/java/nextstep/qna/domain/AnswerTest.java +++ b/src/test/java/nextstep/qna/domain/AnswerTest.java @@ -1,8 +1,41 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUserTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDateTime; +import nextstep.qna.exception.unchecked.WrongRequestException; +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 요청자가_답변작성자와_동일한지_확인할_수_있다() { + long requesterId = 1L; + Question question = new Question(1L, "title1", "contents1"); + Answer answer = new Answer(requesterId, question, "Answers Contents1"); + + assertThat(answer.isOwner(requesterId)).isTrue(); + } + + @Test + void 삭제된_답변객체는_삭제이력을_만들수_있다() { + Question question = new Question(1L, "title1", "contents1"); + Answer answer = new Answer(1L, question, "Answers Contents1"); + answer.putOnDelete(1L); + + assertThat(answer.createAnswerDeleteHistory(LocalDateTime.now())).isNotNull(); + } + + @Test + void 삭제되지_않은_질문객체를_삭제이력_객체로_만들수_없다() { + Question question = new Question(1L, "title1", "contents1"); + Answer answer = new Answer(1L, question, "Answers Contents1"); + + assertThatThrownBy( + () -> answer.createAnswerDeleteHistory(LocalDateTime.now()) + ).isInstanceOf(WrongRequestException.class); + } + + } diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 3b8782396..d18883b42 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -1,8 +1,123 @@ package nextstep.qna.domain; -import nextstep.users.domain.NsUserTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.LocalDateTime; +import nextstep.qna.exception.unchecked.CannotDeleteException; +import nextstep.qna.exception.unchecked.WrongRequestException; +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 요청자가_질문자와_동일한지_확인할_수_있다() { + long requesterId = 1L; + Question question = new Question(1L, "title1", "contents1"); + + assertThat(question.isOwner(requesterId)).isTrue(); + } + + @Test + void 질문에_답변이_있는지_확인할_수_있다() { + Question question = new Question(1L, "title1", "contents1"); + question.addAnswer( + new Answer(1L, question, "Answers Contents1")); + + assertThat(question.hasAnswers()).isTrue(); + } + + @Test + void 질문에_답변이_없는지_확인할_수_있다() { + Question question = new Question(1L, "title1", "contents1"); + + assertThat(question.hasAnswers()).isFalse(); + } + + @Test + void 질문의_답변들이_질문자가_작성한_답변만_존재하는지_확인할_수_있다() { + Question question = new Question(1L, "title1", "contents1"); + question.addAnswer(new Answer(1L, question, "Answers Contents1")); + question.addAnswer(new Answer(1L, question, "Answers Contents2")); + + assertThat(question.isAllSameContentsWriter()).isTrue(); + } + + @Test + void 질문의_답변들이_여러사람이_작성한_답변이_존재하는지_확인할_수_있다() { + Question question = new Question(1L, "title1", "contents1"); + question.addAnswer(new Answer(1L, question, "Answers Contents1")); + question.addAnswer(new Answer(2L, question, "Answers Contents2")); + + assertThat(question.isAllSameContentsWriter()).isFalse(); + } + + @Test + void 질문을_삭제상태로_변경할_수_있다() { + Question question = new Question(1L, "title1", "contents1"); + + question.putOnDelete(1L); + + assertThat(question.isDeleted()).isTrue(); + } + + @Test + void 비정상적인_요청자번호로_질문을_삭제시도할_수_없다() { + Question question = new Question(1L, "title1", "contents1"); + + assertThatThrownBy( + () -> question.putOnDelete(-1L) + ).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 요청자와_질문자가_다르면_질문을_삭제시도할_수_없다() { + Question question = new Question(1L, "title1", "contents1"); + + assertThatThrownBy( + () -> question.putOnDelete(2L) + ).isInstanceOf(CannotDeleteException.class); + } + + @Test + void 답변중_질문자와_다른유저의_답변이_있다면_질문을_삭제시도할_수_없다() { + Question question = new Question(1L, "title1", "contents1"); + question.addAnswer(new Answer(1L, question, "Answers Contents1")); + question.addAnswer(new Answer(2L, question, "Answers Contents2")); + + assertThatThrownBy( + () -> question.putOnDelete(1L) + ).isInstanceOf(CannotDeleteException.class); + } + + @Test + void 질문의_모든답변을_삭제상태로_변경할_수_있다() { + Question question = new Question(1L, "title1", "contents1"); + Answer firstAnswers = new Answer(1L, question, "Answers Contents1"); + question.addAnswer(firstAnswers); + Answer secondAnswers = new Answer(1L, question, "Answers Contents2"); + question.addAnswer(secondAnswers); + + question.putOnAllAnswersDelete(1L); + + assertThat(firstAnswers.isDeleted()).isTrue(); + assertThat(secondAnswers.isDeleted()).isTrue(); + } + + @Test + void 삭제된_질문객체는_삭제이력을_만들수_있다() { + Question question = new Question(1L, "title1", "contents1"); + question.putOnDelete(1L); + + assertThat(question.createQuestionDeleteHistory(LocalDateTime.now())).isNotNull(); + } + + @Test + void 삭제되지_않은_질문객체를_삭제이력_객체로_만들수_없다() { + Question question = new Question(1L, "title1", "contents1"); + + assertThatThrownBy( + () -> question.createQuestionDeleteHistory(LocalDateTime.now()) + ).isInstanceOf(WrongRequestException.class); + } } diff --git a/src/test/java/nextstep/qna/service/QnaServiceTest.java b/src/test/java/nextstep/qna/service/QnaServiceTest.java index e1e943c23..a8c04b957 100644 --- a/src/test/java/nextstep/qna/service/QnaServiceTest.java +++ b/src/test/java/nextstep/qna/service/QnaServiceTest.java @@ -1,8 +1,20 @@ package nextstep.qna.service; -import nextstep.qna.CannotDeleteException; -import nextstep.qna.domain.*; -import nextstep.users.domain.NsUserTest; +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.domain.Answer; +import nextstep.qna.domain.ContentType; +import nextstep.qna.domain.DeleteHistory; +import nextstep.qna.domain.Question; +import nextstep.qna.exception.unchecked.CannotDeleteException; +import nextstep.qna.service.repository.QuestionRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,18 +22,9 @@ 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 private QuestionRepository questionRepository; @@ -35,56 +38,60 @@ public class QnaServiceTest { private Answer answer; @BeforeEach - public void setUp() throws Exception { - question = new Question(1L, NsUserTest.JAVAJIGI, "title1", "contents1"); - answer = new Answer(11L, NsUserTest.JAVAJIGI, QuestionTest.Q1, "Answers Contents1"); + public void setUp() { + question = new Question(1L, 1L, "title1", "contents1"); + answer = new Answer(11L, 1L, question, "Answers Contents1"); question.addAnswer(answer); } @Test - public void delete_성공() throws Exception { - when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); + public void delete_성공() { + when(questionRepository.findById(1L)).thenReturn(Optional.of(question)); + LocalDateTime fixedNow = LocalDateTime.now(); assertThat(question.isDeleted()).isFalse(); - qnAService.deleteQuestion(NsUserTest.JAVAJIGI, question.getId()); + qnAService.deleteQuestion(1L, 1L, fixedNow); assertThat(question.isDeleted()).isTrue(); - verifyDeleteHistories(); + verifyDeleteHistories(fixedNow); } @Test - public void delete_다른_사람이_쓴_글() throws Exception { - when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); + public void delete_다른_사람이_쓴_글() { + when(questionRepository.findById(1L)).thenReturn(Optional.of(question)); + LocalDateTime fixedNow = LocalDateTime.now(); - assertThatThrownBy(() -> { - qnAService.deleteQuestion(NsUserTest.SANJIGI, question.getId()); - }).isInstanceOf(CannotDeleteException.class); + assertThatThrownBy(() -> + qnAService.deleteQuestion(2L, 1L, fixedNow) + ).isInstanceOf(CannotDeleteException.class); } @Test - public void delete_성공_질문자_답변자_같음() throws Exception { - when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); + public void delete_성공_질문자_답변자_같음() { + when(questionRepository.findById(1L)).thenReturn(Optional.of(question)); + LocalDateTime fixedNow = LocalDateTime.now(); - qnAService.deleteQuestion(NsUserTest.JAVAJIGI, question.getId()); + qnAService.deleteQuestion(1L, 1L, fixedNow); assertThat(question.isDeleted()).isTrue(); assertThat(answer.isDeleted()).isTrue(); - verifyDeleteHistories(); + verifyDeleteHistories(fixedNow); } @Test - public void delete_답변_중_다른_사람이_쓴_글() throws Exception { - when(questionRepository.findById(question.getId())).thenReturn(Optional.of(question)); + public void delete_답변_중_다른_사람이_쓴_글() { + when(questionRepository.findById(1L)).thenReturn(Optional.of(question)); + LocalDateTime fixedNow = LocalDateTime.now(); - assertThatThrownBy(() -> { - qnAService.deleteQuestion(NsUserTest.SANJIGI, question.getId()); - }).isInstanceOf(CannotDeleteException.class); + assertThatThrownBy(() -> + qnAService.deleteQuestion(2L, 1L, fixedNow) + ).isInstanceOf(CannotDeleteException.class); } - private void verifyDeleteHistories() { + private void verifyDeleteHistories(LocalDateTime fixedNow) { List deleteHistories = Arrays.asList( - new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()), - new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now())); + new DeleteHistory(ContentType.QUESTION, 1L, 1L, fixedNow), + new DeleteHistory(ContentType.ANSWER, 11L, 1L, fixedNow)); verify(deleteHistoryService).saveAll(deleteHistories); } } diff --git a/src/test/java/nextstep/users/domain/LogInPairKeyTest.java b/src/test/java/nextstep/users/domain/LogInPairKeyTest.java new file mode 100644 index 000000000..f0b7932d1 --- /dev/null +++ b/src/test/java/nextstep/users/domain/LogInPairKeyTest.java @@ -0,0 +1,29 @@ +package nextstep.users.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class LogInPairKeyTest { + + @Test + void 아아디_비밀번호가_일치하는지_검증할_수_있다() { + assertThat( + new LogInPairKey("min", "6038").matchUser("min", "6038") + ).isTrue(); + } + + @Test + void 아아디_비밀번호중_아이디가_불일치하는지_검증할_수_있다() { + assertThat( + new LogInPairKey("min", "6038").matchUser("diff min", "6038") + ).isFalse(); + } + + @Test + void 아아디_비밀번호중_비밀번호가_불일치하는지_검증할_수_있다() { + assertThat( + new LogInPairKey("min", "6038").matchUser("min", "1234") + ).isFalse(); + } +} \ No newline at end of file