diff --git a/src/main/java/com/somemore/community/domain/CommunityComment.java b/src/main/java/com/somemore/community/domain/CommunityComment.java index fd6a85d82..650981767 100644 --- a/src/main/java/com/somemore/community/domain/CommunityComment.java +++ b/src/main/java/com/somemore/community/domain/CommunityComment.java @@ -51,4 +51,14 @@ public boolean isWriter(UUID writerId) { public void updateWith(CommunityCommentUpdateRequestDto dto) { this.content = dto.content(); } + + @Override + public void markAsDeleted() { + super.markAsDeleted(); + this.content = "삭제된 댓글입니다"; + } + + public boolean isDeleted() { + return this.getDeleted(); + } } diff --git a/src/main/java/com/somemore/community/dto/response/CommunityBoardGetResponseDto.java b/src/main/java/com/somemore/community/dto/response/CommunityBoardGetResponseDto.java index 30f5400c8..ba15f67ee 100644 --- a/src/main/java/com/somemore/community/dto/response/CommunityBoardGetResponseDto.java +++ b/src/main/java/com/somemore/community/dto/response/CommunityBoardGetResponseDto.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; import com.fasterxml.jackson.databind.annotation.JsonNaming; -import com.somemore.community.domain.CommunityBoardView; +import com.somemore.community.repository.mapper.CommunityBoardView; import java.time.LocalDateTime; diff --git a/src/main/java/com/somemore/community/dto/response/CommunityCommentResponseDto.java b/src/main/java/com/somemore/community/dto/response/CommunityCommentResponseDto.java new file mode 100644 index 000000000..78731c7be --- /dev/null +++ b/src/main/java/com/somemore/community/dto/response/CommunityCommentResponseDto.java @@ -0,0 +1,36 @@ +package com.somemore.community.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.community.repository.mapper.CommunityCommentView; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record CommunityCommentResponseDto( + Long id, + String writerNickname, + String content, + LocalDateTime updatedAt, + List replies +) { + public CommunityCommentResponseDto { + replies = replies == null ? new ArrayList<>() : replies; + } + + public static CommunityCommentResponseDto fromView(CommunityCommentView comment) { + return new CommunityCommentResponseDto( + comment.communityComment().getId(), + comment.writerNickname(), + comment.communityComment().getContent(), + comment.communityComment().getUpdatedAt(), + new ArrayList<>() + ); + } + + public void addReply(CommunityCommentResponseDto reply) { + this.replies.add(reply); + } +} diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java index 44b34f517..64fb8ea0e 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepository.java @@ -1,7 +1,7 @@ package com.somemore.community.repository.board; import com.somemore.community.domain.CommunityBoard; -import com.somemore.community.domain.CommunityBoardView; +import com.somemore.community.repository.mapper.CommunityBoardView; import java.util.List; import java.util.Optional; diff --git a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java index 72bed7542..62dc924ba 100644 --- a/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java +++ b/src/main/java/com/somemore/community/repository/board/CommunityBoardRepositoryImpl.java @@ -4,7 +4,7 @@ import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import com.somemore.community.domain.CommunityBoard; -import com.somemore.community.domain.CommunityBoardView; +import com.somemore.community.repository.mapper.CommunityBoardView; import com.somemore.community.domain.QCommunityBoard; import com.somemore.volunteer.domain.QVolunteer; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java b/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java index c5db8228b..f71e5f630 100644 --- a/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java +++ b/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepository.java @@ -1,12 +1,15 @@ package com.somemore.community.repository.comment; import com.somemore.community.domain.CommunityComment; +import com.somemore.community.repository.mapper.CommunityCommentView; +import java.util.List; import java.util.Optional; public interface CommunityCommentRepository { CommunityComment save(CommunityComment communityComment); Optional findById(Long id); + List findCommentsByBoardId(Long boardId); boolean existsById(Long id); default boolean doesNotExistById(Long id) { return !existsById(id); diff --git a/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepositoryImpl.java b/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepositoryImpl.java index 8a6199f65..c3eba1fc8 100644 --- a/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepositoryImpl.java +++ b/src/main/java/com/somemore/community/repository/comment/CommunityCommentRepositoryImpl.java @@ -1,11 +1,15 @@ package com.somemore.community.repository.comment; +import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import com.somemore.community.domain.CommunityComment; import com.somemore.community.domain.QCommunityComment; +import com.somemore.community.repository.mapper.CommunityCommentView; +import com.somemore.volunteer.domain.QVolunteer; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @RequiredArgsConstructor @@ -31,6 +35,21 @@ public Optional findById(Long id) { .fetchOne()); } + public List findCommentsByBoardId(Long boardId) { + QCommunityComment communityComment = QCommunityComment.communityComment; + QVolunteer volunteer = QVolunteer.volunteer; + + return queryFactory + .select(Projections.constructor(CommunityCommentView.class, + communityComment, + volunteer.nickname)) + .from(communityComment) + .join(volunteer).on(communityComment.writerId.eq(volunteer.id)) + .where(communityComment.communityBoardId.eq(boardId)) + .orderBy(communityComment.parentCommentId.asc().nullsFirst(), communityComment.createdAt.asc()) + .fetch(); + } + @Override public boolean existsById(Long id) { QCommunityComment communityComment = QCommunityComment.communityComment; diff --git a/src/main/java/com/somemore/community/domain/CommunityBoardView.java b/src/main/java/com/somemore/community/repository/mapper/CommunityBoardView.java similarity index 51% rename from src/main/java/com/somemore/community/domain/CommunityBoardView.java rename to src/main/java/com/somemore/community/repository/mapper/CommunityBoardView.java index 01cbd31a9..a03f271aa 100644 --- a/src/main/java/com/somemore/community/domain/CommunityBoardView.java +++ b/src/main/java/com/somemore/community/repository/mapper/CommunityBoardView.java @@ -1,4 +1,6 @@ -package com.somemore.community.domain; +package com.somemore.community.repository.mapper; + +import com.somemore.community.domain.CommunityBoard; public record CommunityBoardView( CommunityBoard communityBoard, diff --git a/src/main/java/com/somemore/community/repository/mapper/CommunityCommentView.java b/src/main/java/com/somemore/community/repository/mapper/CommunityCommentView.java new file mode 100644 index 000000000..48664c0a9 --- /dev/null +++ b/src/main/java/com/somemore/community/repository/mapper/CommunityCommentView.java @@ -0,0 +1,16 @@ +package com.somemore.community.repository.mapper; + +import com.somemore.community.domain.CommunityComment; +import lombok.Builder; + +@Builder +public record CommunityCommentView( + CommunityComment communityComment, + String writerNickname +) { + public CommunityCommentView replaceWriterNickname(CommunityCommentView communityCommentView) { + return CommunityCommentView.builder() + .communityComment(communityCommentView.communityComment) + .writerNickname("").build(); + } +} diff --git a/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java b/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java index 98d45143f..d9be49dda 100644 --- a/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java +++ b/src/main/java/com/somemore/community/service/board/CommunityBoardQueryService.java @@ -1,7 +1,7 @@ package com.somemore.community.service.board; import com.somemore.community.domain.CommunityBoard; -import com.somemore.community.domain.CommunityBoardView; +import com.somemore.community.repository.mapper.CommunityBoardView; import com.somemore.community.dto.response.CommunityBoardGetDetailResponseDto; import com.somemore.community.dto.response.CommunityBoardGetResponseDto; import com.somemore.community.repository.board.CommunityBoardRepository; diff --git a/src/main/java/com/somemore/community/service/comment/CommunityCommentQueryService.java b/src/main/java/com/somemore/community/service/comment/CommunityCommentQueryService.java new file mode 100644 index 000000000..5be670611 --- /dev/null +++ b/src/main/java/com/somemore/community/service/comment/CommunityCommentQueryService.java @@ -0,0 +1,78 @@ +package com.somemore.community.service.comment; + +import com.somemore.community.domain.CommunityComment; +import com.somemore.community.dto.response.CommunityCommentResponseDto; +import com.somemore.community.repository.comment.CommunityCommentRepository; +import com.somemore.community.repository.mapper.CommunityCommentView; +import com.somemore.community.usecase.comment.CommunityCommentQueryUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class CommunityCommentQueryService implements CommunityCommentQueryUseCase { + + private final CommunityCommentRepository communityCommentRepository; + + @Override + public List getCommunityCommentsByBoardId(Long boardId) { + List allComments = communityCommentRepository.findCommentsByBoardId(boardId); + List filteredComments = filterValidComments(allComments); + return createCommentHierarchy(filteredComments); + } + + private List filterValidComments(List comments) { + List parentCommentIds = findParentCommentIds(comments); + + return comments.stream() + .flatMap(comment -> processDeletedComment(parentCommentIds, comment).stream()) + .toList(); + } + + private List findParentCommentIds(List comments) { + return comments.stream() + .filter(comment -> !comment.communityComment().getDeleted()) + .map(comment -> comment.communityComment().getParentCommentId()) + .filter(Objects::nonNull) + .toList(); + } + + private Optional processDeletedComment(List parentCommentIds, CommunityCommentView commentView) { + CommunityComment comment = commentView.communityComment(); + + if (comment.isDeleted()) { + if (parentCommentIds.contains(comment.getId())) { + return Optional.of(commentView.replaceWriterNickname(commentView)); + } + return Optional.empty(); + } + + return Optional.of(commentView); + } + + private List createCommentHierarchy(List comments) { + + Map commentMap = new HashMap<>(); + List rootComments = new ArrayList<>(); + + for (CommunityCommentView comment : comments) { + CommunityCommentResponseDto dto = CommunityCommentResponseDto.fromView(comment); + commentMap.put(dto.id(), dto); + + Long parentCommentId = comment.communityComment().getParentCommentId(); + + if (parentCommentId == null) { + rootComments.add(dto); + } else { + commentMap.get(parentCommentId).addReply(dto); + } + } + + return rootComments; + } +} + diff --git a/src/main/java/com/somemore/community/usecase/comment/CommunityCommentQueryUseCase.java b/src/main/java/com/somemore/community/usecase/comment/CommunityCommentQueryUseCase.java new file mode 100644 index 000000000..33ab69c94 --- /dev/null +++ b/src/main/java/com/somemore/community/usecase/comment/CommunityCommentQueryUseCase.java @@ -0,0 +1,9 @@ +package com.somemore.community.usecase.comment; + +import com.somemore.community.dto.response.CommunityCommentResponseDto; + +import java.util.List; + +public interface CommunityCommentQueryUseCase { + List getCommunityCommentsByBoardId(Long boardId); +} diff --git a/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java b/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java index acb29075f..bd524f811 100644 --- a/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java +++ b/src/test/java/com/somemore/community/repository/CommunityBoardRepositoryTest.java @@ -3,7 +3,7 @@ import com.somemore.IntegrationTestSupport; import com.somemore.auth.oauth.OAuthProvider; import com.somemore.community.domain.CommunityBoard; -import com.somemore.community.domain.CommunityBoardView; +import com.somemore.community.repository.mapper.CommunityBoardView; import com.somemore.community.repository.board.CommunityBoardRepository; import com.somemore.volunteer.domain.Volunteer; import com.somemore.volunteer.repository.VolunteerRepository; diff --git a/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java b/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java index 8903033ce..0a84492a6 100644 --- a/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java +++ b/src/test/java/com/somemore/community/repository/CommunityCommentRepositoryTest.java @@ -1,16 +1,21 @@ package com.somemore.community.repository; import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; import com.somemore.community.domain.CommunityBoard; import com.somemore.community.domain.CommunityComment; import com.somemore.community.repository.board.CommunityBoardRepository; import com.somemore.community.repository.comment.CommunityCommentRepository; +import com.somemore.community.repository.mapper.CommunityCommentView; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.repository.VolunteerRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; import java.util.UUID; @@ -23,6 +28,8 @@ class CommunityCommentRepositoryTest extends IntegrationTestSupport { CommunityCommentRepository communityCommentRepository; @Autowired CommunityBoardRepository communityBoardRepository; + @Autowired + VolunteerRepository volunteerRepository; private Long boardId; private UUID writerId; @@ -30,19 +37,23 @@ class CommunityCommentRepositoryTest extends IntegrationTestSupport { @BeforeEach void setUp() { + String oAuthId = "example-oauth-id"; + Volunteer volunteer = Volunteer.createDefault(OAuthProvider.NAVER, oAuthId); + volunteerRepository.save(volunteer); + + writerId = volunteer.getId(); + CommunityBoard communityBoard = CommunityBoard.builder() .title("테스트 커뮤니티 게시글 제목") .content("테스트 커뮤니티 게시글 내용") .imgUrl("http://community.example.com/123") - .writerId(UUID.randomUUID()) + .writerId(writerId) .build(); communityBoardRepository.save(communityBoard); boardId = communityBoard.getId(); - writerId = UUID.randomUUID(); - CommunityComment communityComment = CommunityComment.builder() .communityBoardId(boardId) .writerId(writerId) @@ -74,7 +85,7 @@ void createCommunityCommentReply() { .communityBoardId(boardId) .writerId(writerId) .content("커뮤니티 댓글 테스트 내용") - .parentCommentId(1L) + .parentCommentId(savedComment.getId()) .build(); //when @@ -83,7 +94,7 @@ void createCommunityCommentReply() { //then assertThat(savedCommentReply.getWriterId()).isEqualTo(writerId); assertThat(savedCommentReply.getContent()).isEqualTo("커뮤니티 댓글 테스트 내용"); - assertThat(savedCommentReply.getParentCommentId()).isEqualTo(1L); + assertThat(savedCommentReply.getParentCommentId()).isEqualTo(savedComment.getId()); } @DisplayName("댓글을 id로 조회할 수 있다. (Repository)") @@ -112,4 +123,27 @@ void existsById() { //then assertThat(isExist).isTrue(); } + + @DisplayName("게시글 id로 게시글에 달린 댓글을 조회할 수 있다. (Repository)") + @Test + void findCommunityCommentByBoardId() { + + //given + CommunityComment communityCommentReply = CommunityComment.builder() + .communityBoardId(boardId) + .writerId(writerId) + .content("커뮤니티 댓글 테스트 내용") + .parentCommentId(savedComment.getId()) + .build(); + + CommunityComment savedCommentReply = communityCommentRepository.save(communityCommentReply); + + //when + List comments = communityCommentRepository.findCommentsByBoardId(boardId); + + //then + assertThat(comments).hasSize(2); + assertThat(comments.getFirst().communityComment().getId()).isEqualTo(savedComment.getId()); + assertThat(comments.getLast().communityComment().getId()).isEqualTo(savedCommentReply.getId()); + } } diff --git a/src/test/java/com/somemore/community/service/comment/CommunityCommentQueryServiceTest.java b/src/test/java/com/somemore/community/service/comment/CommunityCommentQueryServiceTest.java new file mode 100644 index 000000000..a41b33119 --- /dev/null +++ b/src/test/java/com/somemore/community/service/comment/CommunityCommentQueryServiceTest.java @@ -0,0 +1,138 @@ +package com.somemore.community.service.comment; + +import com.somemore.IntegrationTestSupport; +import com.somemore.auth.oauth.OAuthProvider; +import com.somemore.community.domain.CommunityBoard; +import com.somemore.community.domain.CommunityComment; +import com.somemore.community.dto.request.CommunityBoardCreateRequestDto; +import com.somemore.community.dto.request.CommunityCommentCreateRequestDto; +import com.somemore.community.dto.response.CommunityCommentResponseDto; +import com.somemore.community.repository.board.CommunityBoardRepository; +import com.somemore.community.repository.comment.CommunityCommentRepository; +import com.somemore.community.usecase.comment.DeleteCommunityCommentUseCase; +import com.somemore.volunteer.domain.Volunteer; +import com.somemore.volunteer.repository.VolunteerRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + + +class CommunityCommentQueryServiceTest extends IntegrationTestSupport { + @Autowired + private CommunityCommentQueryService communityCommentQueryService; + @Autowired + private CommunityCommentRepository communityCommentRepository; + @Autowired + private CommunityBoardRepository communityBoardRepository; + @Autowired + private VolunteerRepository volunteerRepository; + @Autowired + private DeleteCommunityCommentUseCase deleteCommunityCommentUseCase; + + private Long boardId, commentId, replyId; + UUID writerId1, writerId2; + + @BeforeEach + void setUp() { + String oAuthId1 = "example-oauth-id"; + Volunteer volunteer1 = Volunteer.createDefault(OAuthProvider.NAVER, oAuthId1); + volunteerRepository.save(volunteer1); + writerId1 = volunteer1.getId(); + + String oAuthId2 = "example-oauth-id"; + Volunteer volunteer2 = Volunteer.createDefault(OAuthProvider.NAVER, oAuthId2); + volunteerRepository.save(volunteer2); + writerId2 = volunteer2.getId(); + + CommunityBoardCreateRequestDto boardDto = CommunityBoardCreateRequestDto.builder() + .title("커뮤니티 테스트 제목") + .content("커뮤니티 테스트 내용") + .build(); + CommunityBoard communityBoard = communityBoardRepository.save(boardDto.toEntity(writerId1, "https://test.image/123")); + boardId = communityBoard.getId(); + + CommunityCommentCreateRequestDto dto1 = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) + .content("커뮤니티 댓글 테스트 내용") + .parentCommentId(null) + .build(); + CommunityComment communityComment = communityCommentRepository.save(dto1.toEntity(writerId2)); + commentId = communityComment.getId(); + + CommunityCommentCreateRequestDto dto2 = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) + .content("커뮤니티 대댓글 테스트 내용") + .parentCommentId(commentId) + .build(); + CommunityComment communityReply = communityCommentRepository.save(dto2.toEntity(writerId1)); + replyId = communityReply.getId(); + } + + @AfterEach + void tearDown() { + communityCommentRepository.deleteAllInBatch(); + } + + @DisplayName("커뮤니티 게시글에 달린 댓글을 조회할 수 있다.") + @Test + void getCommentsByCommunityBoardId() { + + //given + //when + List comments = communityCommentQueryService.getCommunityCommentsByBoardId(boardId); + + //then + assertThat(comments).hasSize(1); + assertThat(comments.getFirst().id()).isEqualTo(commentId); + assertThat(comments.getFirst().replies()).hasSize(1); + assertThat(comments.getFirst().replies().getFirst().id()).isEqualTo(replyId); + } + + @DisplayName("삭제된 댓글의 경우 조회할 수 없다.") + @Test + void doesNotFind() { + + //given + CommunityCommentCreateRequestDto dto = CommunityCommentCreateRequestDto.builder() + .communityBoardId(boardId) + .content("커뮤니티 댓글 테스트 내용") + .parentCommentId(null) + .build(); + CommunityComment communityComment = communityCommentRepository.save(dto.toEntity(writerId2)); + + deleteCommunityCommentUseCase.deleteCommunityComment(writerId2, communityComment.getId()); + deleteCommunityCommentUseCase.deleteCommunityComment(writerId1, replyId); + + //when + List comments = communityCommentQueryService.getCommunityCommentsByBoardId(boardId); + + //then + assertThat(comments).hasSize(1); + assertThat(comments.getFirst().id()).isEqualTo(commentId); + assertThat(comments.getFirst().replies()).isEmpty(); + } + + @DisplayName("대댓글이 있는 댓글의 경우 삭제된 댓글로 조회할 수 있다.") + @Test + void getCommentsByCommunityBoardIdWithDeletedComment() { + + //given + deleteCommunityCommentUseCase.deleteCommunityComment(writerId2, commentId); + //when + List comments = communityCommentQueryService.getCommunityCommentsByBoardId(boardId); + + //then + assertThat(comments).hasSize(1); + assertThat(comments.getFirst().content()).isEqualTo("삭제된 댓글입니다"); + assertThat(comments.getFirst().writerNickname()).isEmpty(); + assertThat(comments.getFirst().replies()).hasSize(1); + assertThat(comments.getFirst().replies().getFirst().id()).isEqualTo(replyId); + } +}