diff --git a/maeil-wiki/src/main/java/maeilwiki/comment/Comment.java b/maeil-wiki/src/main/java/maeilwiki/comment/Comment.java index 50df341..ddce60d 100644 --- a/maeil-wiki/src/main/java/maeilwiki/comment/Comment.java +++ b/maeil-wiki/src/main/java/maeilwiki/comment/Comment.java @@ -1,7 +1,6 @@ package maeilwiki.comment; import java.time.LocalDateTime; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -15,7 +14,6 @@ import lombok.NoArgsConstructor; import maeilsupport.BaseEntity; import maeilwiki.member.Member; -import maeilwiki.wiki.Wiki; @Entity @Getter @@ -38,16 +36,15 @@ public class Comment extends BaseEntity { @JoinColumn(nullable = false) private Member member; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(nullable = false) - private Wiki wiki; + @Column(nullable = false) + private Long wikiId; - public Comment(String answer, boolean isAnonymous, Member member, Wiki wiki) { + public Comment(String answer, boolean isAnonymous, Member member, Long wikiId) { validateAnswer(answer); this.answer = answer; this.isAnonymous = isAnonymous; this.member = member; - this.wiki = wiki; + this.wikiId = wikiId; } private void validateAnswer(String answer) { @@ -55,4 +52,12 @@ private void validateAnswer(String answer) { throw new IllegalArgumentException("답변은 필수 입력값입니다."); } } + + public void remove() { + if (deletedAt != null) { + throw new IllegalStateException("이미 삭제된 답변입니다."); + } + + deletedAt = LocalDateTime.now(); + } } diff --git a/maeil-wiki/src/main/java/maeilwiki/comment/CommentApi.java b/maeil-wiki/src/main/java/maeilwiki/comment/CommentApi.java deleted file mode 100644 index 08d7c08..0000000 --- a/maeil-wiki/src/main/java/maeilwiki/comment/CommentApi.java +++ /dev/null @@ -1,29 +0,0 @@ -package maeilwiki.comment; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -class CommentApi { - - private final CommentService commentService; - - @PostMapping("/wiki/{wikiId}/comment") - public ResponseEntity createComment(@RequestBody CommentRequest request, @PathVariable Long wikiId) { - commentService.comment(request, wikiId); - - return ResponseEntity.noContent().build(); - } - - @PostMapping("/wiki/{wikiId}/comment/{id}/like") - public ResponseEntity createCommentLike(@PathVariable Long wikiId, @PathVariable Long id) { - commentService.toggleLike(id); - - return ResponseEntity.noContent().build(); - } -} diff --git a/maeil-wiki/src/main/java/maeilwiki/comment/CommentRepository.java b/maeil-wiki/src/main/java/maeilwiki/comment/CommentRepository.java index 9ade6e0..83f3f32 100644 --- a/maeil-wiki/src/main/java/maeilwiki/comment/CommentRepository.java +++ b/maeil-wiki/src/main/java/maeilwiki/comment/CommentRepository.java @@ -2,5 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; -interface CommentRepository extends JpaRepository { +public interface CommentRepository extends JpaRepository { + + boolean existsByWikiIdAndDeletedAtIsNull(Long wikiId); } diff --git a/maeil-wiki/src/main/java/maeilwiki/comment/CommentRequest.java b/maeil-wiki/src/main/java/maeilwiki/comment/CommentRequest.java index a01f422..7cd6ca6 100644 --- a/maeil-wiki/src/main/java/maeilwiki/comment/CommentRequest.java +++ b/maeil-wiki/src/main/java/maeilwiki/comment/CommentRequest.java @@ -1,11 +1,10 @@ package maeilwiki.comment; import maeilwiki.member.Member; -import maeilwiki.wiki.Wiki; -record CommentRequest(String answer, boolean isAnonymous) { +public record CommentRequest(String answer, boolean isAnonymous) { - public Comment toComment(Member member, Wiki wiki) { - return new Comment(answer, isAnonymous, member, wiki); + public Comment toComment(Member member, Long wikiId) { + return new Comment(answer, isAnonymous, member, wikiId); } } diff --git a/maeil-wiki/src/main/java/maeilwiki/comment/CommentService.java b/maeil-wiki/src/main/java/maeilwiki/comment/CommentService.java index bdcfd80..1c0ee8e 100644 --- a/maeil-wiki/src/main/java/maeilwiki/comment/CommentService.java +++ b/maeil-wiki/src/main/java/maeilwiki/comment/CommentService.java @@ -7,19 +7,16 @@ import lombok.RequiredArgsConstructor; import maeilwiki.member.Member; import maeilwiki.member.MemberRepository; -import maeilwiki.wiki.Wiki; -import maeilwiki.wiki.WikiRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronizationManager; @Service @RequiredArgsConstructor -class CommentService { +public class CommentService { - private final WikiRepository wikiRepository; - private final CommentRepository commentRepository; private final MemberRepository memberRepository; + private final CommentRepository commentRepository; private final CommentLikeRepository commentLikeRepository; private final Map transactionTmpMemberMap = new ConcurrentHashMap<>(); @@ -29,13 +26,20 @@ public void comment(CommentRequest request, Long wikiId) { String uuid = UUID.randomUUID().toString(); Member temporalMember = new Member(uuid, uuid, "GITHUB"); memberRepository.save(temporalMember); - Wiki wiki = wikiRepository.findById(wikiId) - .orElseThrow(NoSuchElementException::new); - Comment comment = request.toComment(temporalMember, wiki); + Comment comment = request.toComment(temporalMember, wikiId); commentRepository.save(comment); } + @Transactional + public void remove(Long commentId) { + // TODO: member 소유인지 확인해야한다. + Comment comment = commentRepository.findById(commentId) + .orElseThrow(NoSuchElementException::new); + + comment.remove(); + } + @Transactional public void toggleLike(Long id) { Member member = memberSetting(); diff --git a/maeil-wiki/src/main/java/maeilwiki/wiki/Wiki.java b/maeil-wiki/src/main/java/maeilwiki/wiki/Wiki.java index cf5ef16..9c46a73 100644 --- a/maeil-wiki/src/main/java/maeilwiki/wiki/Wiki.java +++ b/maeil-wiki/src/main/java/maeilwiki/wiki/Wiki.java @@ -1,7 +1,6 @@ package maeilwiki.wiki; import java.time.LocalDateTime; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -70,4 +69,12 @@ private void validateQuestion(String question) { throw new IllegalArgumentException("질문은 %d자 이하여야 합니다.".formatted(MAX_QUESTION_LENGTH)); } } + + public void remove() { + if (deletedAt != null) { + throw new IllegalStateException("이미 삭제된 위키입니다."); + } + + deletedAt = LocalDateTime.now(); + } } diff --git a/maeil-wiki/src/main/java/maeilwiki/wiki/WikiApi.java b/maeil-wiki/src/main/java/maeilwiki/wiki/WikiApi.java index f4c5560..9047fa6 100644 --- a/maeil-wiki/src/main/java/maeilwiki/wiki/WikiApi.java +++ b/maeil-wiki/src/main/java/maeilwiki/wiki/WikiApi.java @@ -1,7 +1,11 @@ package maeilwiki.wiki; import lombok.RequiredArgsConstructor; +import maeilwiki.comment.CommentRequest; +import maeilwiki.comment.CommentService; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -11,6 +15,7 @@ class WikiApi { private final WikiService wikiService; + private final CommentService commentService; @PostMapping("/wiki") public ResponseEntity createWiki(@RequestBody WikiRequest request) { @@ -18,4 +23,37 @@ public ResponseEntity createWiki(@RequestBody WikiRequest request) { return ResponseEntity.noContent().build(); } + + @DeleteMapping("/wiki/{id}") + public ResponseEntity deleteWiki(@PathVariable Long id) { + wikiService.remove(id); + + return ResponseEntity.noContent().build(); + } + + @PostMapping("/wiki/{wikiId}/comment") + public ResponseEntity createComment(@RequestBody CommentRequest request, @PathVariable Long wikiId) { + wikiService.comment(request, wikiId); + + return ResponseEntity.noContent().build(); + } + + /** + * wikiId는 현재 사용 계획이 없지만, 입력 받는 이유는 다음과 같습니다. + * - 나중에 특정 위키가 아카이브 상태로 전환되는 기능을 구현할 때는 필요할 수 있습니다. + * - comment가 wiki에 속하는 개념이라는 것을 uri로 표현할 수 있습니다. + */ + @DeleteMapping("/wiki/{wikiId}/comment/{id}") + public ResponseEntity deleteComment(@PathVariable Long wikiId, @PathVariable Long id) { + commentService.remove(id); + + return ResponseEntity.noContent().build(); + } + + @PostMapping("/wiki/{wikiId}/comment/{id}/like") + public ResponseEntity toggleLike(@PathVariable Long wikiId, @PathVariable Long id) { + commentService.toggleLike(id); + + return ResponseEntity.noContent().build(); + } } diff --git a/maeil-wiki/src/main/java/maeilwiki/wiki/WikiRepository.java b/maeil-wiki/src/main/java/maeilwiki/wiki/WikiRepository.java index 49871ae..af4f0fd 100644 --- a/maeil-wiki/src/main/java/maeilwiki/wiki/WikiRepository.java +++ b/maeil-wiki/src/main/java/maeilwiki/wiki/WikiRepository.java @@ -1,6 +1,9 @@ package maeilwiki.wiki; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface WikiRepository extends JpaRepository { + + Optional findByIdAndDeletedAtIsNull(Long id); } diff --git a/maeil-wiki/src/main/java/maeilwiki/wiki/WikiService.java b/maeil-wiki/src/main/java/maeilwiki/wiki/WikiService.java index 97afdd1..73c12c8 100644 --- a/maeil-wiki/src/main/java/maeilwiki/wiki/WikiService.java +++ b/maeil-wiki/src/main/java/maeilwiki/wiki/WikiService.java @@ -1,7 +1,11 @@ package maeilwiki.wiki; +import java.util.NoSuchElementException; import java.util.UUID; import lombok.RequiredArgsConstructor; +import maeilwiki.comment.CommentRepository; +import maeilwiki.comment.CommentRequest; +import maeilwiki.comment.CommentService; import maeilwiki.member.Member; import maeilwiki.member.MemberRepository; import org.springframework.stereotype.Service; @@ -13,6 +17,8 @@ class WikiService { private final WikiRepository wikiRepository; private final MemberRepository memberRepository; + private final CommentRepository commentRepository; + private final CommentService commentService; @Transactional public void create(WikiRequest request) { @@ -23,4 +29,29 @@ public void create(WikiRequest request) { wikiRepository.save(wiki); } + + @Transactional + public void remove(Long wikiId) { + // TODO : member 소유인지 확인해야한다. + validateHasComment(wikiId); + Wiki wiki = wikiRepository.findById(wikiId) + .orElseThrow(NoSuchElementException::new); + + wiki.remove(); + } + + private void validateHasComment(Long wikiId) { + boolean hasComment = commentRepository.existsByWikiIdAndDeletedAtIsNull(wikiId); + if (hasComment) { + throw new IllegalStateException("답변이 존재하는 위키는 삭제할 수 없습니다."); + } + } + + @Transactional + public void comment(CommentRequest request, Long wikiId) { + Wiki wiki = wikiRepository.findByIdAndDeletedAtIsNull(wikiId) + .orElseThrow(NoSuchElementException::new); + + commentService.comment(request, wiki.getId()); + } } diff --git a/maeil-wiki/src/test/java/maeilwiki/comment/CommentRepositoryTest.java b/maeil-wiki/src/test/java/maeilwiki/comment/CommentRepositoryTest.java new file mode 100644 index 0000000..1a2816a --- /dev/null +++ b/maeil-wiki/src/test/java/maeilwiki/comment/CommentRepositoryTest.java @@ -0,0 +1,60 @@ +package maeilwiki.comment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.UUID; +import maeilwiki.member.Member; +import maeilwiki.member.MemberRepository; +import maeilwiki.support.IntegrationTestSupport; +import maeilwiki.wiki.Wiki; +import maeilwiki.wiki.WikiRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CommentRepositoryTest extends IntegrationTestSupport { + + @Autowired + private WikiRepository wikiRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommentRepository commentRepository; + + @Test + @DisplayName("주어진 위키에 속하는 답변이 존재하는지 조회한다.") + void existsComment() { + Member member = createMember(); + Wiki wiki = createWiki(member); + Wiki noCommentWiki = createWiki(member); + createComment(member, wiki); + Comment comment = createComment(member, noCommentWiki); + comment.remove(); + + assertAll( + () -> assertThat(commentRepository.existsByWikiIdAndDeletedAtIsNull(wiki.getId())).isTrue(), + () -> assertThat(commentRepository.existsByWikiIdAndDeletedAtIsNull(noCommentWiki.getId())).isFalse() + ); + } + + private Member createMember() { + Member member = new Member(UUID.randomUUID().toString(), UUID.randomUUID().toString(), "GITHUB"); + + return memberRepository.save(member); + } + + private Wiki createWiki(Member member) { + Wiki wiki = new Wiki("question", "backend", false, member); + + return wikiRepository.save(wiki); + } + + private Comment createComment(Member member, Wiki wiki) { + Comment comment = new Comment("answer", false, member, wiki.getId()); + + return commentRepository.save(comment); + } +} diff --git a/maeil-wiki/src/test/java/maeilwiki/comment/CommentServiceTest.java b/maeil-wiki/src/test/java/maeilwiki/comment/CommentServiceTest.java index 65b948a..4504c36 100644 --- a/maeil-wiki/src/test/java/maeilwiki/comment/CommentServiceTest.java +++ b/maeil-wiki/src/test/java/maeilwiki/comment/CommentServiceTest.java @@ -32,18 +32,17 @@ class CommentServiceTest extends IntegrationTestSupport { private WikiRepository wikiRepository; @Test - @DisplayName("존재하지 않는 위키에 답변을 작성할 수 없다.") - void notfound() { - CommentRequest request = new CommentRequest("답변을 작성합니다.", false); - Long unknownWikiId = -1L; + @DisplayName("존재하지 않는 답변을 삭제할 수 없다.") + void notFoundComment() { + Long unknownCommentId = -1L; - assertThatThrownBy(() -> commentService.comment(request, unknownWikiId)) + assertThatThrownBy(() -> commentService.remove(unknownCommentId)) .isInstanceOf(NoSuchElementException.class); } @Test @DisplayName("존재하지 않는 답변에 좋아요를 생성할 수 없다.") - void notFoundComment() { + void notFoundCommentForLike() { Long unknownCommentId = -1L; assertThatThrownBy(() -> commentService.toggleLike(unknownCommentId)) @@ -80,7 +79,7 @@ private Comment createComment() { Wiki wiki = new Wiki("question", "backend", false, member); wikiRepository.save(wiki); - Comment comment = new Comment("answer", false, member, wiki); + Comment comment = new Comment("answer", false, member, wiki.getId()); return commentRepository.save(comment); } } diff --git a/maeil-wiki/src/test/java/maeilwiki/comment/CommentTest.java b/maeil-wiki/src/test/java/maeilwiki/comment/CommentTest.java new file mode 100644 index 0000000..72a3a3b --- /dev/null +++ b/maeil-wiki/src/test/java/maeilwiki/comment/CommentTest.java @@ -0,0 +1,39 @@ +package maeilwiki.comment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import maeilwiki.member.Member; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CommentTest { + + @Test + @DisplayName("답변을 삭제할 수 있다.") + void remove() { + Comment comment = createComment(); + + comment.remove(); + + assertThat(comment.getDeletedAt()).isNotNull(); + } + + @Test + @DisplayName("이미 삭제된 답변은 다시 삭제될 수 없다.") + void alreadyRemoved() { + Comment comment = createComment(); + comment.remove(); + + assertThatThrownBy(comment::remove) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 삭제된 답변입니다."); + } + + private Comment createComment() { + Member member = mock(Member.class); + + return new Comment("answer", false, member, 1L); + } +} diff --git a/maeil-wiki/src/test/java/maeilwiki/wiki/WikiServiceTest.java b/maeil-wiki/src/test/java/maeilwiki/wiki/WikiServiceTest.java new file mode 100644 index 0000000..d43d79c --- /dev/null +++ b/maeil-wiki/src/test/java/maeilwiki/wiki/WikiServiceTest.java @@ -0,0 +1,79 @@ +package maeilwiki.wiki; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.NoSuchElementException; +import java.util.UUID; +import maeilwiki.comment.Comment; +import maeilwiki.comment.CommentRepository; +import maeilwiki.comment.CommentRequest; +import maeilwiki.member.Member; +import maeilwiki.member.MemberRepository; +import maeilwiki.support.IntegrationTestSupport; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class WikiServiceTest extends IntegrationTestSupport { + + @Autowired + private WikiRepository wikiRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private WikiService wikiService; + + @Test + @DisplayName("존재하지 않는 위키에 답변을 작성할 수 없다.") + void notfound() { + CommentRequest request = new CommentRequest("답변을 작성합니다.", false); + Long unknownWikiId = -1L; + + assertThatThrownBy(() -> wikiService.comment(request, unknownWikiId)) + .isInstanceOf(NoSuchElementException.class); + } + + @Test + @DisplayName("답변이 존재하는 위키는 삭제할 수 없다.") + void cantRemove() { + Member member = createMember(); + Wiki wiki = createWiki(member); + Comment comment = createComment(member, wiki); + + assertThatThrownBy(() -> wikiService.remove(wiki.getId())) + .isInstanceOf(IllegalStateException.class) + .hasMessage("답변이 존재하는 위키는 삭제할 수 없습니다."); + } + + @Test + @DisplayName("존재하지 않는 위키는 삭제할 수 없다.") + void cantRemoveUnknownWiki() { + Long unknownWikiId = -1L; + + assertThatThrownBy(() -> wikiService.remove(unknownWikiId)) + .isInstanceOf(NoSuchElementException.class); + } + + private Member createMember() { + Member member = new Member(UUID.randomUUID().toString(), UUID.randomUUID().toString(), "GITHUB"); + + return memberRepository.save(member); + } + + private Wiki createWiki(Member member) { + Wiki wiki = new Wiki("question", "backend", false, member); + + return wikiRepository.save(wiki); + } + + private Comment createComment(Member member, Wiki wiki) { + Comment comment = new Comment("answer", false, member, wiki.getId()); + + return commentRepository.save(comment); + } +} diff --git a/maeil-wiki/src/test/java/maeilwiki/wiki/WikiTest.java b/maeil-wiki/src/test/java/maeilwiki/wiki/WikiTest.java new file mode 100644 index 0000000..482f033 --- /dev/null +++ b/maeil-wiki/src/test/java/maeilwiki/wiki/WikiTest.java @@ -0,0 +1,39 @@ +package maeilwiki.wiki; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +import maeilwiki.member.Member; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class WikiTest { + + @Test + @DisplayName("위키를 삭제할 수 있다.") + void remove() { + Wiki wiki = createWiki(); + + wiki.remove(); + + assertThat(wiki.getDeletedAt()).isNotNull(); + } + + @Test + @DisplayName("이미 삭제된 위키는 다시 삭제될 수 없다.") + void alreadyRemoved() { + Wiki wiki = createWiki(); + wiki.remove(); + + assertThatThrownBy(wiki::remove) + .isInstanceOf(IllegalStateException.class) + .hasMessage("이미 삭제된 위키입니다."); + } + + private Wiki createWiki() { + Member member = mock(Member.class); + + return new Wiki("question", "detail", "backend", false, member); + } +} diff --git a/maeil-wiki/src/test/resources/application.yml b/maeil-wiki/src/test/resources/application.yml new file mode 100644 index 0000000..cb3ad03 --- /dev/null +++ b/maeil-wiki/src/test/resources/application.yml @@ -0,0 +1,14 @@ +spring: + main: + allow-bean-definition-overriding: true + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + auto_quote_keyword: true + format_sql: true + show-sql: true + sql: + init: + mode: never