From c583deba8727baead218ac1eedc0db821fd6e0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=8F=99=ED=98=B8?= <66300965+kdkdhoho@users.noreply.github.com> Date: Sat, 12 Oct 2024 20:24:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EA=B0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=B5=9C=EC=8B=A0=20=EB=8C=93=EA=B8=80=201=EA=B0=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=91=EB=8B=B5=EC=97=90=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84=20(#300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 리스트 상세 조회 시 가장 최신 댓글 1개를 응답에 포함하도록 구현 (#295) * feat: 리스트 상세 조회 시에 가장 최신 댓글에 달린 답글의 총 개수도 포함 (#295) --- .../application/domain/list/ListEntity.java | 4 +- .../dto/response/ListDetailResponse.java | 38 +++++++- .../list/application/service/ListService.java | 8 +- .../list/repository/CommentRepository.java | 6 ++ .../repository/reply/ReplyRepository.java | 2 + .../acceptance/list/ListAcceptanceTest.java | 90 +++++++++++++++++-- .../domain/list/ListEntityTest.java | 4 +- 7 files changed, 138 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/listywave/list/application/domain/list/ListEntity.java b/src/main/java/com/listywave/list/application/domain/list/ListEntity.java index d07fd3c0..76fed271 100644 --- a/src/main/java/com/listywave/list/application/domain/list/ListEntity.java +++ b/src/main/java/com/listywave/list/application/domain/list/ListEntity.java @@ -244,7 +244,7 @@ public boolean isDeletedUser() { return user.isDelete(); } - public void validateOwnerIsNotDelete() { + public void validateOwnerIsNotDeleted() { if (this.user.isDelete()) { throw new CustomException(DELETED_USER_EXCEPTION, "탈퇴한 회원의 리스트입니다."); } @@ -263,7 +263,7 @@ public void validateUpdateAuthority(User loginUser, Collaborators beforeCollabor throw new CustomException(INVALID_ACCESS); } - public void increaseUpdateCount(){ + public void increaseUpdateCount() { this.updateCount++; } } diff --git a/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java b/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java index 439710b3..dd5197bd 100644 --- a/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java +++ b/src/main/java/com/listywave/list/application/dto/response/ListDetailResponse.java @@ -1,10 +1,12 @@ package com.listywave.list.application.dto.response; import com.listywave.collaborator.application.domain.Collaborator; +import com.listywave.list.application.domain.comment.Comment; import com.listywave.list.application.domain.item.Item; import com.listywave.list.application.domain.label.Label; import com.listywave.list.application.domain.list.ListEntity; import com.listywave.user.application.domain.User; +import jakarta.annotation.Nullable; import java.time.LocalDateTime; import java.util.List; import lombok.Builder; @@ -28,14 +30,19 @@ public record ListDetailResponse( String backgroundPalette, String backgroundColor, int collectCount, - int viewCount + int viewCount, + long totalCommentCount, + @Nullable NewestComment newestComment ) { public static ListDetailResponse of( ListEntity list, User owner, boolean isCollected, - List collaborators + List collaborators, + long totalCommentCount, + Comment newestComment, + Long totalReplyCount ) { return ListDetailResponse.builder() .categoryEngName(list.getCategory().name().toLowerCase()) @@ -56,6 +63,8 @@ public static ListDetailResponse of( .backgroundPalette(list.getBackgroundPalette().name()) .collectCount(list.getCollectCount()) .viewCount(list.getViewCount()) + .totalCommentCount(totalCommentCount) + .newestComment(NewestComment.of(newestComment, totalReplyCount)) .build(); } @@ -123,4 +132,29 @@ public static ItemResponse of(Item item) { .build(); } } + + @Builder + public record NewestComment( + Long userId, + String userNickname, + String userProfileImageUrl, + LocalDateTime createdDate, + String content, + Long totalReplyCount + ) { + + public static NewestComment of(@Nullable Comment comment, Long totalReplyCount) { + if (comment == null) { + return null; + } + return NewestComment.builder() + .userId(comment.getUserId()) + .userNickname(comment.getUserNickname()) + .userProfileImageUrl(comment.getUserProfileImageUrl()) + .createdDate(comment.getCreatedDate()) + .content(comment.getCommentContent()) + .totalReplyCount(totalReplyCount) + .build(); + } + } } diff --git a/src/main/java/com/listywave/list/application/service/ListService.java b/src/main/java/com/listywave/list/application/service/ListService.java index 622bda8d..850b2056 100644 --- a/src/main/java/com/listywave/list/application/service/ListService.java +++ b/src/main/java/com/listywave/list/application/service/ListService.java @@ -137,7 +137,7 @@ private Items createItems(List itemCreateRequests) { public ListDetailResponse getListDetail(Long listId, Long loginUserId) { ListEntity list = listRepository.getById(listId); - list.validateOwnerIsNotDelete(); + list.validateOwnerIsNotDeleted(); List collaborators = collaboratorService.findAllByList(list).collaborators(); boolean isCollected = false; @@ -145,7 +145,11 @@ public ListDetailResponse getListDetail(Long listId, Long loginUserId) { User user = userRepository.getById(loginUserId); isCollected = collectionRepository.existsByListAndUserId(list, user.getId()); } - return ListDetailResponse.of(list, list.getUser(), isCollected, collaborators); + + long totalCommentCount = commentRepository.countCommentsByList(list); + Comment newestComment = commentRepository.findFirstByListOrderByCreatedDateDesc(list); + Long totalReplyCount = replyRepository.countByComment(newestComment); + return ListDetailResponse.of(list, list.getUser(), isCollected, collaborators, totalCommentCount, newestComment, totalReplyCount); } @Transactional(readOnly = true) diff --git a/src/main/java/com/listywave/list/repository/CommentRepository.java b/src/main/java/com/listywave/list/repository/CommentRepository.java index f9093a21..2b909d3d 100644 --- a/src/main/java/com/listywave/list/repository/CommentRepository.java +++ b/src/main/java/com/listywave/list/repository/CommentRepository.java @@ -5,6 +5,7 @@ import com.listywave.common.exception.CustomException; import com.listywave.list.application.domain.comment.Comment; import com.listywave.list.application.domain.list.ListEntity; +import jakarta.annotation.Nullable; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -20,4 +21,9 @@ default Comment getById(Long id) { @Query("select c from Comment c where c.list in :lists") List findAllByListIn(@Param("lists") List lists); + + long countCommentsByList(ListEntity list); + + @Nullable + Comment findFirstByListOrderByCreatedDateDesc(ListEntity list); } diff --git a/src/main/java/com/listywave/list/repository/reply/ReplyRepository.java b/src/main/java/com/listywave/list/repository/reply/ReplyRepository.java index 6f325887..461270a3 100644 --- a/src/main/java/com/listywave/list/repository/reply/ReplyRepository.java +++ b/src/main/java/com/listywave/list/repository/reply/ReplyRepository.java @@ -24,4 +24,6 @@ default Reply getById(Long id) { @Modifying @Query("delete from Reply r where r.comment in :comments") void deleteAllByCommentIn(@Param("comments") List comments); + + Long countByComment(Comment comment); } diff --git a/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java b/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java index 5832739d..bb5da75d 100644 --- a/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java +++ b/src/test/java/com/listywave/acceptance/list/ListAcceptanceTest.java @@ -5,7 +5,33 @@ import static com.listywave.acceptance.comment.CommentAcceptanceTestHelper.댓글_저장_API_호출; import static com.listywave.acceptance.common.CommonAcceptanceHelper.HTTP_상태_코드를_검증한다; import static com.listywave.acceptance.follow.FollowAcceptanceTestHelper.팔로우_요청_API; -import static com.listywave.acceptance.list.ListAcceptanceTestHelper.*; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.가장_좋아하는_견종_TOP3_생성_요청_데이터; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.검색_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_공개_여부_변경_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_삭제_요청_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_상세_조회를_검증한다; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_수정_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트_저장_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.리스트의_아이템_순위와_히스토리의_아이템_순위를_검증한다; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원_리스트_상세_조회_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원_피드_리스트_조회_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원_히스토리_조회_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원이_피드_리스트_조회_카테고리_콜라보레이터_필터링_요청; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원이_피드_리스트_조회_카테고리_필터링_요청; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.비회원이_피드_리스트_조회_콜라보레이터_필터링_요청; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.아이템_순위와_라벨을_바꾼_좋아하는_견종_TOP3_요청_데이터; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.정렬기준을_포함한_검색_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.좋아하는_라면_TOP3_생성_요청_데이터; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.최신_리스트_10개_조회_카테고리_필터링_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.카테고리로_검색_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.카테고리와_키워드로_검색_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.콜렉트_요청_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.키워드로_검색_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.키워드와_정렬기준을_포함한_검색_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.트랜딩_리스트_조회_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.팔로우한_사용자의_최신_리스트_10개_조회_API_호출; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.회원_피드_리스트_조회; +import static com.listywave.acceptance.list.ListAcceptanceTestHelper.회원용_리스트_상세_조회_API_호출; import static com.listywave.acceptance.reply.ReplyAcceptanceTestHelper.답글_등록_API_호출; import static com.listywave.list.fixture.ListFixture.가장_좋아하는_견종_TOP3; import static com.listywave.list.fixture.ListFixture.가장_좋아하는_견종_TOP3_순위_변경; @@ -20,8 +46,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.springframework.http.HttpStatus.*; -import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.CREATED; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.NO_CONTENT; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; import com.listywave.acceptance.common.AcceptanceTest; import com.listywave.auth.infra.kakao.response.KakaoLogoutResponse; @@ -30,6 +60,7 @@ import com.listywave.list.application.domain.list.BackgroundColor; import com.listywave.list.application.domain.list.BackgroundPalette; import com.listywave.list.application.domain.list.ListEntity; +import com.listywave.list.application.dto.response.CommentCreateResponse; import com.listywave.list.application.dto.response.ListCreateResponse; import com.listywave.list.application.dto.response.ListDetailResponse; import com.listywave.list.application.dto.response.ListRecentResponse; @@ -38,6 +69,7 @@ import com.listywave.list.presentation.dto.request.ItemCreateRequest; import com.listywave.list.presentation.dto.request.ListUpdateRequest; import com.listywave.list.presentation.dto.request.ReplyCreateRequest; +import com.listywave.list.presentation.dto.request.comment.CommentCreateRequest; import com.listywave.user.application.dto.FindFeedListResponse; import com.listywave.user.application.dto.FindFeedListResponse.FeedListInfo; import com.listywave.user.application.dto.FindFeedListResponse.ListItemsResponse; @@ -210,7 +242,7 @@ class 리스트_상세_조회 { var 결과 = 비회원_리스트_상세_조회_API_호출(동호_리스트_ID).as(ListDetailResponse.class); // then - var 기대값 = ListDetailResponse.of(가장_좋아하는_견종_TOP3_순위_변경(동호, List.of()), 동호, false, List.of()); + var 기대값 = ListDetailResponse.of(가장_좋아하는_견종_TOP3_순위_변경(동호, List.of()), 동호, false, List.of(), 0, null, 0L); 리스트_상세_조회를_검증한다(결과, 기대값); } @@ -230,6 +262,52 @@ class 리스트_상세_조회 { // then HTTP_상태_코드를_검증한다(응답, BAD_REQUEST); } + + @Test + void 댓글이_하나도_없는_리스트라면_가장_최신_댓글_정보에_값이_담기지_않는다() { + // given + var 동호 = 회원을_저장한다(동호()); + var 리스트_생성_요청_데이터 = 가장_좋아하는_견종_TOP3_생성_요청_데이터(List.of()); + 리스트_저장_API_호출(리스트_생성_요청_데이터, 액세스_토큰을_발급한다(동호)); + + // when + var 동호_리스트 = 비회원_리스트_상세_조회_API_호출(1L).as(ListDetailResponse.class); + + // then + assertThat(동호_리스트.totalCommentCount()).isZero(); + assertThat(동호_리스트.newestComment()).isNull(); + } + + @Test + void 댓글이_있다면_가장_최신_댓글과_해당_댓글에_달린_답글_개수도_응답한다() { + // given + var 동호 = 회원을_저장한다(동호()); + var 리스트_생성_요청_데이터 = 가장_좋아하는_견종_TOP3_생성_요청_데이터(List.of()); + var 리스트_ID = 리스트_저장_API_호출(리스트_생성_요청_데이터, 액세스_토큰을_발급한다(동호)) + .as(ListCreateResponse.class) + .listId(); + + var 정수 = 회원을_저장한다(정수()); + 댓글_저장_API_호출(액세스_토큰을_발급한다(정수), 리스트_ID, new CommentCreateRequest("댓글 1빠!")); + var 댓글_ID = 댓글_저장_API_호출(액세스_토큰을_발급한다(정수), 리스트_ID, new CommentCreateRequest("댓글 2빠!")) + .as(CommentCreateResponse.class) + .id(); + + var 유진 = 회원을_저장한다(유진()); + 답글_등록_API_호출(액세스_토큰을_발급한다(유진), new ReplyCreateRequest("답글 1빠!"), 리스트_ID, 댓글_ID); + 답글_등록_API_호출(액세스_토큰을_발급한다(유진), new ReplyCreateRequest("답글 2빠!"), 리스트_ID, 댓글_ID); + 답글_등록_API_호출(액세스_토큰을_발급한다(유진), new ReplyCreateRequest("답글 3빠!"), 리스트_ID, 댓글_ID); + + // when + var 동호_리스트 = 비회원_리스트_상세_조회_API_호출(1L).as(ListDetailResponse.class); + + // then + assertAll( + () -> assertThat(동호_리스트.newestComment()).isNotNull(), + () -> assertThat(동호_리스트.newestComment().content()).isEqualTo("댓글 2빠!"), + () -> assertThat(동호_리스트.newestComment().totalReplyCount()).isEqualTo(3) + ); + } } @Nested @@ -253,7 +331,7 @@ class 리스트_수정 { // then var 리스트_상세_조회_결과 = 회원용_리스트_상세_조회_API_호출(동호_액세스_토큰, 동호_리스트_ID); ListEntity 수정된_리스트 = 가장_좋아하는_견종_TOP3_순위_변경(동호, List.of()); - ListDetailResponse 기대값 = ListDetailResponse.of(수정된_리스트, 동호, false, List.of(Collaborator.init(유진, 수정된_리스트))); + ListDetailResponse 기대값 = ListDetailResponse.of(수정된_리스트, 동호, false, List.of(Collaborator.init(유진, 수정된_리스트)), 0, null, 0L); 리스트_상세_조회를_검증한다(리스트_상세_조회_결과, 기대값); } @@ -624,7 +702,7 @@ class 리스트_팀섹 { .as(ListRecentResponse.class); // then - var 동호_리스트 = 비회원_피드_리스트_조회_API_호출(동호 ).as(FindFeedListResponse.class).feedLists(); + var 동호_리스트 = 비회원_피드_리스트_조회_API_호출(동호).as(FindFeedListResponse.class).feedLists(); var 정수_리스트 = 비회원_피드_리스트_조회_API_호출(정수).as(FindFeedListResponse.class).feedLists(); var 모든_리스트 = new ArrayList<>(동호_리스트); 모든_리스트.addAll(정수_리스트); diff --git a/src/test/java/com/listywave/list/application/domain/list/ListEntityTest.java b/src/test/java/com/listywave/list/application/domain/list/ListEntityTest.java index 267ccf5a..b362bf01 100644 --- a/src/test/java/com/listywave/list/application/domain/list/ListEntityTest.java +++ b/src/test/java/com/listywave/list/application/domain/list/ListEntityTest.java @@ -217,10 +217,10 @@ class ListEntityTest { @Test void 작성자가_삭제_처리됐는지_검증한다() { - assertThatNoException().isThrownBy(list::validateOwnerIsNotDelete); + assertThatNoException().isThrownBy(list::validateOwnerIsNotDeleted); user.softDelete(); - CustomException exception = assertThrows(CustomException.class, list::validateOwnerIsNotDelete); + CustomException exception = assertThrows(CustomException.class, list::validateOwnerIsNotDeleted); assertThat(exception.getErrorCode()).isEqualTo(DELETED_USER_EXCEPTION); }