Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b6037ab
refactor: community 도메인 제외 연관관계 삭제
lsy1307 Jul 1, 2025
df10149
fix: application, university 테스트 코드 오류 수정
lsy1307 Jul 1, 2025
32cb72f
refactor: 불필요한 siteUser 재조회 수정
lsy1307 Jul 1, 2025
3ca8f4e
fix: conflict해결
lsy1307 Jul 1, 2025
2938a79
refactor: 수정사항 반영
lsy1307 Jul 2, 2025
9c59985
fix: conflict 해결
lsy1307 Jul 2, 2025
541fed7
fix: conflict merge 과정에서 생긴 더미 파일 삭제
lsy1307 Jul 2, 2025
5cf0660
refactor: 불필요한 import문 삭제
lsy1307 Jul 2, 2025
b739ceb
refactor: Community 연관관계 매핑 수정/삭제
lsy1307 Jul 2, 2025
805ae3d
refactor: 사용하지 않는 import문 삭제
lsy1307 Jul 2, 2025
1a00a77
refactor: 댓글 정책 변경에 따른 수정
lsy1307 Jul 3, 2025
41afcad
fix: likedPostCount 테스트 코드 주석처리 및 todo 추가
lsy1307 Jul 4, 2025
f24495e
fix: 서브모듈 참조 오류 수정
lsy1307 Jul 8, 2025
2bb6e95
refactor: 코드리뷰 수정사항 반영
lsy1307 Jul 8, 2025
d3c9b0e
refactor: 코드리뷰 수정사항 반영
lsy1307 Jul 9, 2025
8257dfc
Merge branch 'refactor/358-refactor-delete-relationship' of https://g…
lsy1307 Jul 9, 2025
cfa18eb
refactor: Parameter 명칭 변경
lsy1307 Jul 9, 2025
13a4673
Merge branch 'refactor/358-refactor-delete-relationship' of https://g…
lsy1307 Jul 9, 2025
68f4bbd
refactor: 코드리뷰 수정사항 반영
lsy1307 Jul 9, 2025
af4ec96
refactor: 코드리뷰 수정사항 반영
lsy1307 Jul 9, 2025
6fcc903
fix: SiteUser 결함 수정
lsy1307 Jul 9, 2025
028190a
Merge branch 'refactor/368-refactor-delete-relationship-community' of…
lsy1307 Jul 9, 2025
5f6bcaf
fix: 직전 커밋 revert
lsy1307 Jul 10, 2025
786e0ae
fix: conflict 해결
lsy1307 Jul 10, 2025
afa0b37
Merge branch 'refactor/368-refactor-delete-relationship-community' of…
lsy1307 Jul 10, 2025
36621bf
refactor: 코드 리뷰 수정사항 반영
lsy1307 Jul 10, 2025
cd71d21
refactor: flyway script 추가
lsy1307 Jul 10, 2025
acde555
fix: CommentService 수정
lsy1307 Jul 10, 2025
fd85b32
refactor: 삭제된 댓글에 대한 사용자 정보를 반환하지 않도록 수정
lsy1307 Jul 10, 2025
e91c74b
Merge branch 'refactor/368-refactor-delete-relationship-community' of…
lsy1307 Jul 10, 2025
6526fd1
refactor: 테스트 코드 수정
lsy1307 Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -102,6 +105,6 @@ public void updateContent(String content) {
}

public void deprecateComment() {
this.content = null;
this.isDeleted = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ public interface CommentRepository extends JpaRepository<Comment, Long> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,14 +39,47 @@ public class CommentService {

@Transactional(readOnly = true)
public List<PostFindCommentResponse> findCommentsByPostId(SiteUser siteUser, Long postId) {
SiteUser commentOwner = siteUserRepository.findById(siteUser.getId())
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
return commentRepository.findCommentTreeByPostId(postId)
List<Comment> allComments = commentRepository.findCommentTreeByPostId(postId);
List<Comment> filteredComments = filterCommentsByDeletionRules(allComments);

Set<Long> userIds = filteredComments.stream()
.map(Comment::getSiteUserId)
.collect(Collectors.toSet());

Map<Long, SiteUser> 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<Comment> filterCommentsByDeletionRules(List<Comment> comments) {
Map<Long, List<Comment>> commentsByParent = comments.stream()
.filter(comment -> comment.getParentComment() != null)
.collect(Collectors.groupingBy(comment -> comment.getParentComment().getId()));

List<Comment> result = new ArrayList<>();

List<Comment> parentComments = comments.stream()
.filter(comment -> comment.getParentComment() == null)
.toList();
for (Comment parent : parentComments) {
List<Comment> 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;
}
Comment on lines +59 to +81
Copy link

@coderabbitai coderabbitai bot Jul 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

댓글 필터링 로직 검토 필요 ⚠️

현재 로직에 몇 가지 문제점이 있습니다:

  1. 부모 댓글이 삭제되었지만 자식 중 일부만 삭제된 경우:

    • 현재: 부모 댓글은 포함되지만 삭제된 자식은 제외됨 (라인 69-72)
    • 문제: 삭제된 부모 댓글의 내용이 노출될 수 있음
  2. 결과 리스트 순서:

    • 재귀 쿼리로 가져온 원본 순서가 보장되지 않을 수 있음
 private List<Comment> filterCommentsByDeletionRules(List<Comment> comments) {
     Map<Long, List<Comment>> commentsByParent = comments.stream()
             .filter(comment -> comment.getParentComment() != null)
             .collect(Collectors.groupingBy(comment -> comment.getParentComment().getId()));

     List<Comment> result = new ArrayList<>();

     List<Comment> parentComments = comments.stream()
             .filter(comment -> comment.getParentComment() == null)
             .toList();
     for (Comment parent : parentComments) {
         List<Comment> 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());
+            result.addAll(children);
         }
     }
     return result;
 }

서비스 레이어가 아닌 PostFindCommentResponse에서 삭제된 댓글의 내용을 숨기는 것이 더 적절할 것 같습니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/community/comment/service/CommentService.java
lines 53 to 75, the current filterCommentsByDeletionRules method incorrectly
includes deleted parent comments when some child comments are not deleted,
exposing deleted content, and does not preserve the original comment order. To
fix this, adjust the logic to exclude any deleted parent comment regardless of
child status, and ensure the output list maintains the original order from the
input list. Additionally, consider moving the deletion content hiding logic from
the service layer to the PostFindCommentResponse DTO to better separate
concerns.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 테스트한 바로는 두 문제 모두 발생하지 않는 것 같습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whqtker 테스트 결과를 알려주셔서 감사합니다! 실제 동작을 확인해보신 결과가 이론적 분석보다 훨씬 신뢰할 만하네요.

  1. 콘텐츠 노출 문제: PostFindCommentResponse.from에서 getDisplayContent 메서드를 통해 삭제된 댓글의 내용을 숨기는 처리가 이미 되어 있군요.

  2. 순서 보장: findCommentTreeByPostId가 계층적 순서로 댓글을 반환하고, 현재 필터링 로직이 이를 적절히 유지하고 있는 것 같습니다.

실제 테스트를 통해 검증해주셔서 도움이 되었습니다. 코드가 의도한 대로 잘 동작하고 있다니 다행이네요! 🎉


private Boolean isOwner(Comment comment, SiteUser siteUser) {
return Objects.equals(comment.getSiteUserId(), siteUser.getId());
}
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE comment
ADD COLUMN is_deleted BOOLEAN NOT NULL DEFAULT FALSE;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트코드 보충이 필요해보여요! 서비스 로직에서 수행하고 있는 것은

  • 부모, 자식 댓글들이 모두 삭제되었다면 보여주지 않는다.
  • 부모 댓글이 삭제된 경우에도, 자식 댓글이 있으면 "삭제되지 않은 자식 댓글만" 보여준다.

이렇게 두가지인데, 첫번째에 대한 테스트만 존재하네요.!
두번째 로직에 대한 테스트가 추가되어야 할 것 같습니다~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가하였습니다!

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<PostFindCommentResponse> 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<PostFindCommentResponse> 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<PostFindCommentResponse> responses = commentService.findCommentsByPostId(user1, post.getId());

// then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 코드검증이 저는 개인적으로 좀 읽기 힘들었는데

// then
            PostFindCommentResponse parentResponse = responses.stream()
                    .filter(response -> response.id().equals(parentComment.getId()))
                    .findFirst()
                    .orElseThrow();

            List<PostFindCommentResponse> childResponses = responses.stream()
                    .filter(response -> !response.id().equals(parentComment.getId()))
                    .toList();

            assertAll(
                    () -> assertThat(responses).hasSize(3),
                    () -> assertThat(responses)
                            .extracting(PostFindCommentResponse::id)
                            .containsExactlyInAnyOrder(parentComment.getId(), childComment1.getId(), childComment2.getId()),

                    // 부모 댓글 검증
                    () -> assertThat(parentResponse.content()).isEqualTo(""),
                    () -> assertThat(parentResponse.postFindSiteUserResponse()).isNull(),

                    // 자식 댓글 검증
                    () -> assertThat(childResponses)
                            .extracting(PostFindCommentResponse::content)
                            .containsExactlyInAnyOrder("자식 댓글1", "자식 댓글2"),
                    () -> assertThat(childResponses)
                            .extracting(PostFindCommentResponse::postFindSiteUserResponse)
                            .isNotNull()
                            .extracting(PostFindSiteUserResponse::id)
                            .containsExactlyInAnyOrder(user2.getId(), user2.getId())
            );

이런 느낌으로 좀 나눠가는 건 어떤가요? 반영 안하셔도 괜찮습니다 이건!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 좀 필요없는 것까지 있는 것 같아서 영서님께서 제안해주신 것이랑 같이 확인해서 간결하게 수정해보았습니다!

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
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -316,27 +397,6 @@ class 댓글_삭제_테스트 {
);
}

@Test
@Transactional
void 대댓글을_삭제하고_부모댓글이_삭제된_상태면_부모댓글도_삭제된다() {
// given
Comment parentComment = commentFixture.부모_댓글("부모 댓글", post, user1);
Comment childComment = commentFixture.자식_댓글("자식 댓글", post, user2, parentComment);
List<Comment> 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
Expand Down
Loading