From 9ec1ed0c035ae24c6815947e2a085782bf738cd1 Mon Sep 17 00:00:00 2001 From: JFe <33208246+Go-Jaecheol@users.noreply.github.com> Date: Fri, 7 Jun 2024 19:59:07 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EC=A1=B0=ED=9A=8C=20API=20=EC=88=98=EC=A0=95=20(#7?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 리뷰 랭킹 dto 필드 추가 및 수정 * refactor: 비로그인 시에도 LoginInfo 반환하도록 수정 * refactor: 리뷰 랭킹 서비스 로직 수정 * refactor: 로그인 여부에 따른 리뷰 랭킹 인수 테스트 분리 * refactor: 로그인 여부에 따른 리뷰 랭킹 서비스 테스트 추가 * refactor: 리뷰 랭킹 관련 dto 클래스 record로 타입 변경 * refactor: Boolean 타입 수정 --- .../persistence/ReviewFavoriteRepository.java | 2 + .../review/application/ReviewService.java | 19 +++- .../funeat/review/dto/RankingReviewDto.java | 74 +++++--------- .../review/dto/RankingReviewsResponse.java | 14 +-- .../presentation/ReviewApiController.java | 6 +- .../review/presentation/ReviewController.java | 2 +- .../review/ReviewAcceptanceTest.java | 38 +++++++- .../funeat/acceptance/review/ReviewSteps.java | 9 ++ .../com/funeat/fixture/ReviewFixture.java | 5 + .../review/application/ReviewServiceTest.java | 96 ++++++++++++++++--- 10 files changed, 176 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java b/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java index f1ae40e5..a79fc986 100644 --- a/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java +++ b/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java @@ -14,4 +14,6 @@ public interface ReviewFavoriteRepository extends JpaRepository findByReview(final Review review); + + boolean existsByMemberAndReviewAndFavoriteTrue(final Member member, final Review review); } diff --git a/src/main/java/com/funeat/review/application/ReviewService.java b/src/main/java/com/funeat/review/application/ReviewService.java index 586658ea..314e162a 100644 --- a/src/main/java/com/funeat/review/application/ReviewService.java +++ b/src/main/java/com/funeat/review/application/ReviewService.java @@ -63,6 +63,7 @@ public class ReviewService { private static final int RANKING_SIZE = 2; private static final long RANKING_MINIMUM_FAVORITE_COUNT = 1L; private static final int REVIEW_PAGE_SIZE = 10; + private static final long GUEST_ID = -1L; private final ReviewRepository reviewRepository; private final TagRepository tagRepository; @@ -212,17 +213,27 @@ private Boolean hasNextPage(final List sortingReviews) { return sortingReviews.size() > REVIEW_PAGE_SIZE; } - public RankingReviewsResponse getTopReviews() { + public RankingReviewsResponse getTopReviews(final Long memberId) { final List reviews = reviewRepository.findReviewsByFavoriteCountGreaterThanEqual(RANKING_MINIMUM_FAVORITE_COUNT); final List dtos = reviews.stream() .sorted(Comparator.comparing(Review::calculateRankingScore).reversed()) .limit(RANKING_SIZE) - .map(RankingReviewDto::toDto) - .collect(Collectors.toList()); - + .map(review -> createRankingReviewDto(memberId, review)) + .toList(); return RankingReviewsResponse.toResponse(dtos); } + private RankingReviewDto createRankingReviewDto(final Long memberId, final Review review) { + if (memberId == GUEST_ID) { + return RankingReviewDto.toDto(review, false); + } + + final Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); + final Boolean favorite = reviewFavoriteRepository.existsByMemberAndReviewAndFavoriteTrue(member, review); + return RankingReviewDto.toDto(review, favorite); + } + public MemberReviewsResponse findReviewByMember(final Long memberId, final Pageable pageable) { final Member findMember = memberRepository.findById(memberId) .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); diff --git a/src/main/java/com/funeat/review/dto/RankingReviewDto.java b/src/main/java/com/funeat/review/dto/RankingReviewDto.java index 42f0102e..a9fd6bcc 100644 --- a/src/main/java/com/funeat/review/dto/RankingReviewDto.java +++ b/src/main/java/com/funeat/review/dto/RankingReviewDto.java @@ -5,29 +5,22 @@ import com.funeat.tag.dto.TagDto; import java.util.List; -public class RankingReviewDto { - - private final Long reviewId; - private final Long productId; - private final String categoryType; - private final String productName; - private final String content; - private final String image; - private final List tags; - - private RankingReviewDto(final Long reviewId, final Long productId, final String categoryType, - final String productName, final String content, final String image, - final List tags) { - this.reviewId = reviewId; - this.productId = productId; - this.categoryType = categoryType; - this.productName = productName; - this.content = content; - this.image = image; - this.tags = tags; - } - - public static RankingReviewDto toDto(final Review review) { +public record RankingReviewDto( + Long id, + String userName, + String profileImage, + Long productId, + String productName, + String content, + String image, + Long rating, + Boolean rebuy, + Long favoriteCount, + Boolean favorite, + List tags +) { + + public static RankingReviewDto toDto(final Review review, final Boolean favorite) { final List tagDtos = review.getReviewTags().stream() .map(ReviewTag::getTag) .map(TagDto::toDto) @@ -35,40 +28,17 @@ public static RankingReviewDto toDto(final Review review) { return new RankingReviewDto( review.getId(), + review.getMember().getNickname(), + review.getMember().getProfileImage(), review.getProduct().getId(), - review.getProduct().getCategory().getType().getName(), review.getProduct().getName(), review.getContent(), review.getImage(), + review.getRating(), + review.getReBuy(), + review.getFavoriteCount(), + favorite, tagDtos ); } - - public Long getReviewId() { - return reviewId; - } - - public Long getProductId() { - return productId; - } - - public String getProductName() { - return productName; - } - - public String getContent() { - return content; - } - - public String getCategoryType() { - return categoryType; - } - - public String getImage() { - return image; - } - - public List getTags() { - return tags; - } } diff --git a/src/main/java/com/funeat/review/dto/RankingReviewsResponse.java b/src/main/java/com/funeat/review/dto/RankingReviewsResponse.java index fd2befb0..d3144142 100644 --- a/src/main/java/com/funeat/review/dto/RankingReviewsResponse.java +++ b/src/main/java/com/funeat/review/dto/RankingReviewsResponse.java @@ -2,19 +2,11 @@ import java.util.List; -public class RankingReviewsResponse { - - private final List reviews; - - public RankingReviewsResponse(final List reviews) { - this.reviews = reviews; - } +public record RankingReviewsResponse( + List reviews +) { public static RankingReviewsResponse toResponse(final List reviews) { return new RankingReviewsResponse(reviews); } - - public List getReviews() { - return reviews; - } } diff --git a/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/src/main/java/com/funeat/review/presentation/ReviewApiController.java index 886ed8ac..eb9ad90e 100644 --- a/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -11,9 +11,9 @@ import com.funeat.review.dto.ReviewFavoriteRequest; import com.funeat.review.dto.SortingReviewRequest; import com.funeat.review.dto.SortingReviewsResponse; +import jakarta.validation.Valid; import java.net.URI; import java.util.Optional; -import jakarta.validation.Valid; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -68,8 +68,8 @@ public ResponseEntity getSortingReviews(@AuthenticationP } @GetMapping("/api/ranks/reviews") - public ResponseEntity getRankingReviews() { - final RankingReviewsResponse response = reviewService.getTopReviews(); + public ResponseEntity getRankingReviews(@AuthenticationPrincipal final LoginInfo loginInfo) { + final RankingReviewsResponse response = reviewService.getTopReviews(loginInfo.getId()); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/funeat/review/presentation/ReviewController.java b/src/main/java/com/funeat/review/presentation/ReviewController.java index 13fb85e9..18ab1c26 100644 --- a/src/main/java/com/funeat/review/presentation/ReviewController.java +++ b/src/main/java/com/funeat/review/presentation/ReviewController.java @@ -64,7 +64,7 @@ ResponseEntity getSortingReviews(@AuthenticationPrincipa description = "리뷰 랭킹 Top3 조회 성공." ) @GetMapping - ResponseEntity getRankingReviews(); + ResponseEntity getRankingReviews(@AuthenticationPrincipal final LoginInfo loginInfo); @Operation(summary = "좋아요를 제일 많은 받은 리뷰 조회", description = "특정 상품에 대해 좋아요를 제일 많이 받은 리뷰를 조회한다.") @ApiResponse( diff --git a/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index c3a9ab61..7278553d 100644 --- a/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -10,7 +10,6 @@ import static com.funeat.acceptance.common.CommonSteps.정상_처리; import static com.funeat.acceptance.common.CommonSteps.정상_처리_NO_CONTENT; import static com.funeat.acceptance.common.CommonSteps.찾을수_없음; -import static com.funeat.acceptance.product.ProductSteps.상품_상세_조회_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_랭킹_조회_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_상세_조회_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_작성_요청; @@ -715,7 +714,7 @@ class getSortingReviews_비로그인_사용자_실패_테스트 { class getRankingReviews_성공_테스트 { @Test - void 리뷰_랭킹을_조회하다() { + void 로그인을_하지_않고_리뷰_랭킹을_조회한다() { // given final var 카테고리 = 카테고리_즉석조리_생성(); 단일_카테고리_저장(카테고리); @@ -737,7 +736,33 @@ class getRankingReviews_성공_테스트 { // then STATUS_CODE를_검증한다(응답, 정상_처리); - 리뷰_랭킹_조회_결과를_검증한다(응답, List.of(리뷰2, 리뷰3)); + 리뷰_랭킹_조회_결과를_검증한다(응답, List.of(리뷰2, 리뷰3), List.of(false, false)); + } + + @Test + void 로그인_후_리뷰_랭킹을_조회한다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품1 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 상품2 = 단일_상품_저장(상품_삼각김밥_가격2000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품1, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_3점, List.of(태그))); + 리뷰_작성_요청(로그인_쿠키_획득(멤버2), 상품1, 사진_명세_요청(이미지2), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + 리뷰_작성_요청(로그인_쿠키_획득(멤버3), 상품1, 사진_명세_요청(이미지3), 리뷰추가요청_재구매O_생성(점수_3점, List.of(태그))); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품2, 사진_명세_요청(이미지4), 리뷰추가요청_재구매O_생성(점수_5점, List.of(태그))); + 리뷰_작성_요청(로그인_쿠키_획득(멤버2), 상품2, 사진_명세_요청(이미지5), 리뷰추가요청_재구매O_생성(점수_1점, List.of(태그))); + 여러명이_리뷰_좋아요_요청(List.of(멤버1, 멤버2, 멤버3), 상품1, 리뷰2, 좋아요O); + 여러명이_리뷰_좋아요_요청(List.of(멤버1, 멤버2), 상품1, 리뷰3, 좋아요O); + 여러명이_리뷰_좋아요_요청(List.of(멤버1), 상품1, 리뷰4, 좋아요O); + + // when + final var 응답 = 리뷰_랭킹_조회_요청(로그인_쿠키_획득(멤버2)); + + // then + STATUS_CODE를_검증한다(응답, 정상_처리); + 리뷰_랭킹_조회_결과를_검증한다(응답, List.of(리뷰2, 리뷰3), List.of(true, true)); } } @@ -848,12 +873,15 @@ class getReviewDetail_실패_테스트 { .containsExactlyElementsOf(reviewIds); } - private void 리뷰_랭킹_조회_결과를_검증한다(final ExtractableResponse response, final List reviewIds) { + private void 리뷰_랭킹_조회_결과를_검증한다(final ExtractableResponse response, final List reviewIds, + final List favorites) { final var actual = response.jsonPath() .getList("reviews", RankingReviewDto.class); - assertThat(actual).extracting(RankingReviewDto::getReviewId) + assertThat(actual).extracting(RankingReviewDto::id) .containsExactlyElementsOf(reviewIds); + assertThat(actual).extracting(RankingReviewDto::favorite) + .isEqualTo(favorites); } private void 좋아요를_제일_많이_받은_리뷰_결과를_검증한다(final ExtractableResponse response, final Long reviewId) { diff --git a/src/test/java/com/funeat/acceptance/review/ReviewSteps.java b/src/test/java/com/funeat/acceptance/review/ReviewSteps.java index 670d6b22..72d185a7 100644 --- a/src/test/java/com/funeat/acceptance/review/ReviewSteps.java +++ b/src/test/java/com/funeat/acceptance/review/ReviewSteps.java @@ -88,6 +88,15 @@ public class ReviewSteps { .extract(); } + public static ExtractableResponse 리뷰_랭킹_조회_요청(final String loginCookie) { + return given() + .cookie("SESSION", loginCookie) + .when() + .get("/api/ranks/reviews") + .then() + .extract(); + } + public static ExtractableResponse 좋아요를_제일_많이_받은_리뷰_조회_요청(final Long productId) { return given() .when() diff --git a/src/test/java/com/funeat/fixture/ReviewFixture.java b/src/test/java/com/funeat/fixture/ReviewFixture.java index 362e7d2f..524cdec0 100644 --- a/src/test/java/com/funeat/fixture/ReviewFixture.java +++ b/src/test/java/com/funeat/fixture/ReviewFixture.java @@ -6,6 +6,7 @@ import static com.funeat.fixture.PageFixture.평점_오름차순; import com.funeat.member.domain.Member; +import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.product.domain.Product; import com.funeat.review.domain.Review; import com.funeat.review.domain.ReviewTag; @@ -105,6 +106,10 @@ public class ReviewFixture { return new ReviewFavoriteRequest(favorite); } + public static ReviewFavorite 리뷰_좋아요_생성(final Member member, final Review review, final Boolean favorite) { + return ReviewFavorite.create(member, review, favorite); + } + public static SortingReviewRequest 리뷰정렬요청_좋아요수_내림차순_생성(final Long lastReviewId) { return new SortingReviewRequest(좋아요수_내림차순, lastReviewId); } diff --git a/src/test/java/com/funeat/review/application/ReviewServiceTest.java b/src/test/java/com/funeat/review/application/ReviewServiceTest.java index b701501d..6dbd7f38 100644 --- a/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -23,6 +23,7 @@ import static com.funeat.fixture.ReviewFixture.리뷰_이미지test4_평점4점_재구매O_생성; import static com.funeat.fixture.ReviewFixture.리뷰_이미지test5_평점5점_재구매X_생성; import static com.funeat.fixture.ReviewFixture.리뷰_이미지없음_평점1점_재구매O_생성; +import static com.funeat.fixture.ReviewFixture.리뷰_좋아요_생성; import static com.funeat.fixture.ReviewFixture.리뷰정렬요청_좋아요수_내림차순_생성; import static com.funeat.fixture.ReviewFixture.리뷰정렬요청_최신순_생성; import static com.funeat.fixture.ReviewFixture.리뷰정렬요청_평점_내림차순_생성; @@ -1206,10 +1207,11 @@ class 리뷰_개수에_대한_테스트 { @Test void 전체_리뷰가_하나도_없어도_반환값은_있어야한다() { // given + final var loginId = -1L; final var expected = RankingReviewsResponse.toResponse(Collections.emptyList()); // when - final var actual = reviewService.getTopReviews(); + final var actual = reviewService.getTopReviews(loginId); // then assertThat(actual).usingRecursiveComparison() @@ -1219,6 +1221,7 @@ class 리뷰_개수에_대한_테스트 { @Test void 전체_리뷰가_1개라도_리뷰가_나와야한다() { // given + final var loginId = -1L; final var category = 카테고리_간편식사_생성(); 단일_카테고리_저장(category); @@ -1232,12 +1235,12 @@ class 리뷰_개수에_대한_테스트 { final var review = 리뷰_이미지test5_평점5점_재구매X_생성(member, product, 2L, now.minusDays(1L)); 단일_리뷰_저장(review); - final var rankingReviewDto = RankingReviewDto.toDto(review); + final var rankingReviewDto = RankingReviewDto.toDto(review, false); final var rankingReviewDtos = List.of(rankingReviewDto); final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); // when - final var actual = reviewService.getTopReviews(); + final var actual = reviewService.getTopReviews(loginId); // then assertThat(actual).usingRecursiveComparison() @@ -1247,6 +1250,7 @@ class 리뷰_개수에_대한_테스트 { @Test void 전체_리뷰_중_랭킹이_높은_상위_2개_리뷰를_구할_수_있다() { // given + final var loginId = -1L; final var category = 카테고리_간편식사_생성(); 단일_카테고리_저장(category); @@ -1262,14 +1266,14 @@ class 리뷰_개수에_대한_테스트 { final var review3 = 리뷰_이미지test5_평점5점_재구매X_생성(member, product, 4L, now); 복수_리뷰_저장(review1, review2, review3); - final var rankingReviewDto1 = RankingReviewDto.toDto(review1); - final var rankingReviewDto2 = RankingReviewDto.toDto(review2); - final var rankingReviewDto3 = RankingReviewDto.toDto(review3); + final var rankingReviewDto1 = RankingReviewDto.toDto(review1, false); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2, false); + final var rankingReviewDto3 = RankingReviewDto.toDto(review3, false); final var rankingReviewDtos = List.of(rankingReviewDto3, rankingReviewDto2); final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); // when - final var actual = reviewService.getTopReviews(); + final var actual = reviewService.getTopReviews(loginId); // then assertThat(actual).usingRecursiveComparison() @@ -1283,6 +1287,7 @@ class 리뷰_랭킹_점수에_대한_테스트 { @Test void 리뷰_좋아요_수가_같으면_최근_생성된_리뷰의_랭킹을_더_높게_반환한다() { // given + final var loginId = -1L; final var category = 카테고리_간편식사_생성(); 단일_카테고리_저장(category); @@ -1297,13 +1302,13 @@ class 리뷰_랭킹_점수에_대한_테스트 { final var review2 = 리뷰_이미지test5_평점5점_재구매X_생성(member, product, 10L, now.minusDays(4L)); 복수_리뷰_저장(review1, review2); - final var rankingReviewDto1 = RankingReviewDto.toDto(review1); - final var rankingReviewDto2 = RankingReviewDto.toDto(review2); + final var rankingReviewDto1 = RankingReviewDto.toDto(review1, false); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2, false); final var rankingReviewDtos = List.of(rankingReviewDto2, rankingReviewDto1); final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); // when - final var actual = reviewService.getTopReviews(); + final var actual = reviewService.getTopReviews(loginId); // then assertThat(actual).usingRecursiveComparison() @@ -1313,6 +1318,7 @@ class 리뷰_랭킹_점수에_대한_테스트 { @Test void 리뷰_생성_일자가_같으면_좋아요_수가_많은_리뷰의_랭킹을_더_높게_반환한다() { // given + final var loginId = -1L; final var category = 카테고리_간편식사_생성(); 단일_카테고리_저장(category); @@ -1327,13 +1333,77 @@ class 리뷰_랭킹_점수에_대한_테스트 { final var review2 = 리뷰_이미지test5_평점5점_재구매X_생성(member, product, 4L, now.minusDays(1L)); 복수_리뷰_저장(review1, review2); - final var rankingReviewDto1 = RankingReviewDto.toDto(review1); - final var rankingReviewDto2 = RankingReviewDto.toDto(review2); + final var rankingReviewDto1 = RankingReviewDto.toDto(review1, false); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2, false); final var rankingReviewDtos = List.of(rankingReviewDto2, rankingReviewDto1); final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); // when - final var actual = reviewService.getTopReviews(); + final var actual = reviewService.getTopReviews(loginId); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } + + @Nested + class 로그인_여부_응답_테스트 { + + @Test + void 로그인_안한_경우_리뷰_좋아요는_false로_반환한다() { + // given + final var loginId = -1L; + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점4점_생성(category); + 단일_상품_저장(product); + + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + + final var now = LocalDateTime.now(); + final var review = 리뷰_이미지test5_평점5점_재구매X_생성(member, product, 1L, now.minusDays(1L)); + 단일_리뷰_저장(review); + + final var rankingReviewDto = RankingReviewDto.toDto(review, false); + final var rankingReviewDtos = List.of(rankingReviewDto); + final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); + + // when + final var actual = reviewService.getTopReviews(loginId); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void 로그인_한_경우_리뷰_좋아요는_로그인한_사용자에_따라_반환한다() { + // given + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + final var loginId = member.getId(); + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점4점_생성(category); + 단일_상품_저장(product); + + final var now = LocalDateTime.now(); + final var review = 리뷰_이미지test5_평점5점_재구매X_생성(member, product, 1L, now.minusDays(1L)); + 단일_리뷰_저장(review); + + final var reviewFavorite = 리뷰_좋아요_생성(member, review, true); + 단일_리뷰_좋아요_저장(reviewFavorite); + + final var rankingReviewDto = RankingReviewDto.toDto(review, true); + final var rankingReviewDtos = List.of(rankingReviewDto); + final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); + + // when + final var actual = reviewService.getTopReviews(loginId); // then assertThat(actual).usingRecursiveComparison()