diff --git a/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java index 90aa1c6ec..5d47bd591 100644 --- a/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java +++ b/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java @@ -40,6 +40,9 @@ public class Comment extends BaseEntity { @Column(length = 255) private String content; + @Column(name = "is_deleted", columnDefinition = "boolean default false", nullable = false) + private boolean isDeleted = false; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; @@ -102,6 +105,6 @@ public void updateContent(String content) { } public void deprecateComment() { - this.content = null; + this.isDeleted = true; } } diff --git a/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java index dcfcdcfa5..0d9f5f295 100644 --- a/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java @@ -20,11 +20,11 @@ public static PostFindCommentResponse from(Boolean isOwner, Comment comment, Sit return new PostFindCommentResponse( comment.getId(), getParentCommentId(comment), - comment.getContent(), + getDisplayContent(comment), isOwner, comment.getCreatedAt(), comment.getUpdatedAt(), - PostFindSiteUserResponse.from(siteUser) + getDisplaySiteUserResponse(comment, siteUser) ); } @@ -34,4 +34,13 @@ private static Long getParentCommentId(Comment comment) { } return null; } + + private static String getDisplayContent(Comment comment) + { + return comment.isDeleted() ? "" : comment.getContent(); + } + + private static PostFindSiteUserResponse getDisplaySiteUserResponse(Comment comment, SiteUser siteUser) { + return comment.isDeleted() ? null : PostFindSiteUserResponse.from(siteUser); + } } diff --git a/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java index cd2ae72ae..a17565bee 100644 --- a/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java @@ -16,14 +16,14 @@ public interface CommentRepository extends JpaRepository { WITH RECURSIVE CommentTree AS ( SELECT id, parent_id, post_id, site_user_id, content, - created_at, updated_at, + created_at, updated_at, is_deleted, 0 AS level, CAST(id AS CHAR(255)) AS path FROM comment WHERE post_id = :postId AND parent_id IS NULL UNION ALL SELECT c.id, c.parent_id, c.post_id, c.site_user_id, c.content, - c.created_at, c.updated_at, + c.created_at, c.updated_at, c.is_deleted, ct.level + 1, CONCAT(ct.path, '->', c.id) FROM comment c INNER JOIN CommentTree ct ON c.parent_id = ct.id diff --git a/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java index 08e458810..38a61b60d 100644 --- a/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java @@ -17,8 +17,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import static com.example.solidconnection.common.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; @@ -36,14 +39,47 @@ public class CommentService { @Transactional(readOnly = true) public List findCommentsByPostId(SiteUser siteUser, Long postId) { - SiteUser commentOwner = siteUserRepository.findById(siteUser.getId()) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - return commentRepository.findCommentTreeByPostId(postId) + List allComments = commentRepository.findCommentTreeByPostId(postId); + List filteredComments = filterCommentsByDeletionRules(allComments); + + Set userIds = filteredComments.stream() + .map(Comment::getSiteUserId) + .collect(Collectors.toSet()); + + Map userMap = siteUserRepository.findAllById(userIds) .stream() - .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser), comment, siteUser)) + .collect(Collectors.toMap(SiteUser::getId, user -> user)); + + return filteredComments.stream() + .map(comment -> PostFindCommentResponse.from( + isOwner(comment, siteUser), comment, userMap.get(comment.getSiteUserId()))) .collect(Collectors.toList()); } + private List filterCommentsByDeletionRules(List comments) { + Map> commentsByParent = comments.stream() + .filter(comment -> comment.getParentComment() != null) + .collect(Collectors.groupingBy(comment -> comment.getParentComment().getId())); + + List result = new ArrayList<>(); + + List parentComments = comments.stream() + .filter(comment -> comment.getParentComment() == null) + .toList(); + for (Comment parent : parentComments) { + List children = commentsByParent.getOrDefault(parent.getId(), List.of()); + boolean allDeleted = parent.isDeleted() && + children.stream().allMatch(Comment::isDeleted); + if (!allDeleted) { + result.add(parent); + result.addAll(children.stream() + .filter(child -> !child.isDeleted()) + .toList()); + } + } + return result; + } + private Boolean isOwner(Comment comment, SiteUser siteUser) { return Objects.equals(comment.getSiteUserId(), siteUser.getId()); } @@ -99,7 +135,7 @@ public CommentDeleteResponse deleteCommentById(SiteUser siteUser, Long commentId comment.resetPostAndParentComment(); commentRepository.deleteById(commentId); // 대댓글 삭제 이후, 부모댓글이 무의미하다면 이역시 삭제합니다. - if (parentComment.getCommentList().isEmpty() && parentComment.getContent() == null) { + if (parentComment.getCommentList().isEmpty() && parentComment.isDeleted()) { parentComment.resetPostAndParentComment(); commentRepository.deleteById(parentComment.getId()); } diff --git a/src/main/resources/db/migration/V22_add_is_deleted_to_comment.sql b/src/main/resources/db/migration/V22_add_is_deleted_to_comment.sql new file mode 100644 index 000000000..a479fbaa2 --- /dev/null +++ b/src/main/resources/db/migration/V22_add_is_deleted_to_comment.sql @@ -0,0 +1,2 @@ +ALTER TABLE comment + ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index 02501d7f1..136500502 100644 --- a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -15,6 +15,7 @@ import com.example.solidconnection.community.post.domain.PostCategory; import com.example.solidconnection.community.post.fixture.PostFixture; import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import jakarta.transaction.Transactional; @@ -108,6 +109,85 @@ class 댓글_조회_테스트 { )) ); } + + @Test + void 부모댓글과_대댓글이_모두_삭제되면_응답에서_제외한다() { + // given + Comment parentComment = commentFixture.부모_댓글("부모 댓글", post, user1); + Comment childComment1 = commentFixture.자식_댓글("자식 댓글1", post, user2, parentComment); + Comment childComment2 = commentFixture.자식_댓글("자식 댓글2", post, user2, parentComment); + + parentComment.deprecateComment(); + childComment1.deprecateComment(); + childComment2.deprecateComment(); + commentRepository.saveAll(List.of(parentComment, childComment1, childComment2)); + + // when + List responses = commentService.findCommentsByPostId(user1, post.getId()); + + // then + assertAll( + () -> assertThat(responses).isEmpty() + ); + } + + @Test + void 부모댓글이_삭제된_경우에도_자식댓글이_존재하면_자식댓글의_내용만_반환한다() { + // given + Comment parentComment = commentFixture.부모_댓글("부모 댓글", post, user1); + Comment childComment1 = commentFixture.자식_댓글("자식 댓글1", post, user2, parentComment); + Comment childComment2 = commentFixture.자식_댓글("자식 댓글2", post, user2, parentComment); + + parentComment.deprecateComment(); + commentRepository.saveAll(List.of(parentComment, childComment1, childComment2)); + + // when + List responses = commentService.findCommentsByPostId(user1, post.getId()); + + // then + assertAll( + () -> assertThat(responses).hasSize(3), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .containsExactlyInAnyOrder(parentComment.getId(), childComment1.getId(), childComment2.getId()), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(parentComment.getId())) + .extracting(PostFindCommentResponse::content) + .containsExactly(""), + () -> assertThat(responses) + .filteredOn(response -> !response.id().equals(parentComment.getId())) + .extracting(PostFindCommentResponse::content) + .containsExactlyInAnyOrder("자식 댓글1", "자식 댓글2") + ); + } + + @Test + void 부모댓글이_삭제된_경우_부모댓글의_사용자정보는_null이고_자식댓글의_사용자정보는_정상적으로_반환한다() { + // given + Comment parentComment = commentFixture.부모_댓글("부모 댓글", post, user1); + Comment childComment1 = commentFixture.자식_댓글("자식 댓글1", post, user2, parentComment); + Comment childComment2 = commentFixture.자식_댓글("자식 댓글2", post, user2, parentComment); + + parentComment.deprecateComment(); + commentRepository.saveAll(List.of(parentComment, childComment1, childComment2)); + + // when + List responses = commentService.findCommentsByPostId(user1, post.getId()); + + // then + assertAll( + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(parentComment.getId())) + .extracting(PostFindCommentResponse::postFindSiteUserResponse) + .containsExactly((PostFindSiteUserResponse) null), + () -> assertThat(responses) + .filteredOn(response -> !response.id().equals(parentComment.getId())) + .extracting(PostFindCommentResponse::postFindSiteUserResponse) + .isNotNull() + .extracting(PostFindSiteUserResponse::id) + .containsExactlyInAnyOrder(user2.getId(), user2.getId()) + ); + } } @Nested @@ -281,7 +361,8 @@ class 댓글_삭제_테스트 { // then Comment deletedComment = commentRepository.findById(response.id()).orElseThrow(); assertAll( - () -> assertThat(deletedComment.getContent()).isNull(), + () -> assertThat(deletedComment.getContent()).isEqualTo("부모 댓글"), + () -> assertThat(deletedComment.isDeleted()).isTrue(), () -> assertThat(deletedComment.getCommentList()) .extracting(Comment::getId) .containsExactlyInAnyOrder(childComment.getId()), @@ -316,27 +397,6 @@ class 댓글_삭제_테스트 { ); } - @Test - @Transactional - void 대댓글을_삭제하고_부모댓글이_삭제된_상태면_부모댓글도_삭제된다() { - // given - Comment parentComment = commentFixture.부모_댓글("부모 댓글", post, user1); - Comment childComment = commentFixture.자식_댓글("자식 댓글", post, user2, parentComment); - List comments = post.getCommentList(); - int expectedCommentsCount = comments.size() - 2; - parentComment.deprecateComment(); - - // when - CommentDeleteResponse response = commentService.deleteCommentById(user2, childComment.getId()); - - // then - assertAll( - () -> assertThat(commentRepository.findById(response.id())).isEmpty(), - () -> assertThat(commentRepository.findById(parentComment.getId())).isEmpty(), - () -> assertThat(post.getCommentList()).hasSize(expectedCommentsCount) - ); - } - @Test void 다른_사용자의_댓글을_삭제하면_예외_응답을_반환한다() { // given