diff --git a/backend/src/main/java/com/funeat/product/application/ProductService.java b/backend/src/main/java/com/funeat/product/application/ProductService.java index b094d49e8..021ffe126 100644 --- a/backend/src/main/java/com/funeat/product/application/ProductService.java +++ b/backend/src/main/java/com/funeat/product/application/ProductService.java @@ -113,10 +113,9 @@ private boolean hasNextPage(final List findProducts) { public ProductResponse findProductDetail(final Long productId) { final Product product = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); - final Long reviewCount = reviewRepository.countByProduct(product); final List tags = reviewTagRepository.findTop3TagsByReviewIn(productId, PageRequest.of(TOP, THREE)); - return ProductResponse.toResponse(product, reviewCount, tags); + return ProductResponse.toResponse(product, tags); } public RankingProductsResponse getTop3Products() { diff --git a/backend/src/main/java/com/funeat/product/domain/Product.java b/backend/src/main/java/com/funeat/product/domain/Product.java index b3dfa60a1..d5c3607c1 100644 --- a/backend/src/main/java/com/funeat/product/domain/Product.java +++ b/backend/src/main/java/com/funeat/product/domain/Product.java @@ -71,16 +71,36 @@ public Product(final String name, final Long price, final String image, final St this.category = category; this.reviewCount = reviewCount; } - + + public Product(final String name, final Long price, final String image, final String content, + final Double averageRating, final Category category, final Long reviewCount) { + this.name = name; + this.price = price; + this.image = image; + this.content = content; + this.averageRating = averageRating; + this.category = category; + this.reviewCount = reviewCount; + } + public static Product create(final String name, final Long price, final String content, final Category category) { return new Product(name, price, null, content, category); } - public void updateAverageRating(final Long rating, final Long count) { + public void updateAverageRatingForInsert(final Long count, final Long rating) { final double calculatedRating = ((count - 1) * averageRating + rating) / count; this.averageRating = Math.round(calculatedRating * 10.0) / 10.0; } + public void updateAverageRatingForDelete(final Long deletedRating) { + if (reviewCount == 1) { + this.averageRating = 0.0; + return; + } + final double calculatedRating = (reviewCount * averageRating - deletedRating) / (reviewCount - 1); + this.averageRating = Math.round(calculatedRating * 10.0) / 10.0; + } + public Double calculateRankingScore(final Long reviewCount) { final double exponent = -Math.log10(reviewCount + 1); final double factor = Math.pow(2, exponent); @@ -133,4 +153,8 @@ public Long getReviewCount() { public void addReviewCount() { reviewCount++; } + + public void minusReviewCount() { + reviewCount--; + } } diff --git a/backend/src/main/java/com/funeat/product/dto/ProductResponse.java b/backend/src/main/java/com/funeat/product/dto/ProductResponse.java index 49c5bba53..d3c0ed264 100644 --- a/backend/src/main/java/com/funeat/product/dto/ProductResponse.java +++ b/backend/src/main/java/com/funeat/product/dto/ProductResponse.java @@ -29,13 +29,13 @@ public ProductResponse(final Long id, final String name, final Long price, final this.tags = tags; } - public static ProductResponse toResponse(final Product product, final Long reviewCount, final List tags) { + public static ProductResponse toResponse(final Product product, final List tags) { List tagDtos = new ArrayList<>(); for (Tag tag : tags) { tagDtos.add(TagDto.toDto(tag)); } return new ProductResponse(product.getId(), product.getName(), product.getPrice(), product.getImage(), - product.getContent(), product.getAverageRating(), reviewCount, tagDtos); + product.getContent(), product.getAverageRating(), product.getReviewCount(), tagDtos); } public Long getId() { diff --git a/backend/src/main/java/com/funeat/review/application/ReviewService.java b/backend/src/main/java/com/funeat/review/application/ReviewService.java index 6bc8c4f42..142a8e6b5 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -111,7 +111,7 @@ public void create(final Long productId, final Long memberId, final MultipartFil final Long countByProduct = reviewRepository.countByProduct(findProduct); - findProduct.updateAverageRating(savedReview.getRating(), countByProduct); + findProduct.updateAverageRatingForInsert(countByProduct, savedReview.getRating()); findProduct.addReviewCount(); reviewTagRepository.saveAll(reviewTags); } @@ -247,13 +247,19 @@ public void deleteReview(final Long reviewId, final Long memberId) { if (review.checkAuthor(member)) { eventPublisher.publishEvent(new ReviewDeleteEvent(image)); + updateProduct(product, review.getRating()); deleteThingsRelatedToReview(review); - updateProductImage(product.getId()); return; } throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); } + private void updateProduct(final Product product, final Long deletedRating) { + updateProductImage(product.getId()); + product.updateAverageRatingForDelete(deletedRating); + product.minusReviewCount(); + } + private void deleteThingsRelatedToReview(final Review review) { deleteReviewTags(review); deleteReviewFavorites(review); diff --git a/backend/src/test/java/com/funeat/product/domain/ProductTest.java b/backend/src/test/java/com/funeat/product/domain/ProductTest.java index ddfeaa2c7..7e012dd22 100644 --- a/backend/src/test/java/com/funeat/product/domain/ProductTest.java +++ b/backend/src/test/java/com/funeat/product/domain/ProductTest.java @@ -1,7 +1,6 @@ package com.funeat.product.domain; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import org.junit.jupiter.api.DisplayNameGeneration; @@ -14,17 +13,17 @@ class ProductTest { @Nested - class updateAverageRating_성공_테스트 { + class updateAverageRatingForInsert_성공_테스트 { @Test void 평균_평점을_업데이트_할_수_있다() { // given final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating = 4L; final var reviewCount = 1L; + final var reviewRating = 4L; // when - product.updateAverageRating(reviewRating, reviewCount); + product.updateAverageRatingForInsert(reviewCount, reviewRating); final var actual = product.getAverageRating(); // then @@ -35,16 +34,16 @@ class updateAverageRating_성공_테스트 { void 평균_평점을_여러번_업데이트_할_수_있다() { // given final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating1 = 4L; - final var reviewRating2 = 2L; final var reviewCount1 = 1L; final var reviewCount2 = 2L; + final var reviewRating1 = 4L; + final var reviewRating2 = 2L; // when - product.updateAverageRating(reviewRating1, reviewCount1); + product.updateAverageRatingForInsert(reviewCount1, reviewRating1); final var actual1 = product.getAverageRating(); - product.updateAverageRating(reviewRating2, reviewCount2); + product.updateAverageRatingForInsert(reviewCount2, reviewRating2); final var actual2 = product.getAverageRating(); // then @@ -58,39 +57,34 @@ class updateAverageRating_성공_테스트 { } @Nested - class updateAverageRating_실패_테스트 { + class updateAverageRatingForDelete_성공_테스트 { @Test - void 리뷰_평점에_null_값이_들어오면_예외가_발생한다() { + void 리뷰가_하나인_상품의_리뷰를_삭제하면_평균평점은_0점이_된다() { // given - final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewCount = 1L; + final var product = new Product("testName", 1000L, "testImage", "testContent", 4.0, null, 1L); + final var reviewRating = 4L; // when - assertThatThrownBy(() -> product.updateAverageRating(null, reviewCount)) - .isInstanceOf(NullPointerException.class); - } - - @Test - void 리뷰_평점이_0점이라면_예외가_발생해야하는데_관련_로직이_없어_통과하고_있다() { - // given - final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating = 0L; - final var reviewCount = 1L; + product.updateAverageRatingForDelete(reviewRating); + final var actual = product.getAverageRating(); - // when - product.updateAverageRating(reviewRating, reviewCount); + // then + assertThat(actual).isEqualTo(0.0); } @Test - void 리뷰_개수가_0개라면_예외가_발생해야하는데_calculatedRating값이_infinity가_나와_통과하고_있다() { + void 리뷰가_여러개인_상품의_리뷰를_삭제하면_평균평점이_갱신된다() { // given - final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating = 3L; - final var reviewCount = 0L; + final var product = new Product("testName", 1000L, "testImage", "testContent", 4.0, null, 4L); + final var reviewRating = 5L; // when - product.updateAverageRating(reviewRating, reviewCount); + product.updateAverageRatingForDelete(reviewRating); + final var actual = product.getAverageRating(); + + // then + assertThat(actual).isEqualTo(3.7); } } diff --git a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java index 7d35ab166..93fd0e046 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -800,28 +800,37 @@ class deleteReview_성공_테스트 { final var tagIds = 태그_아이디_변환(tag1, tag2); final var image = 이미지_생성(); - final var reviewCreateRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); - reviewService.create(productId, authorId, image, reviewCreateRequest); + final var reviewCreateRequest1 = 리뷰추가요청_재구매O_생성(2L, tagIds); + final var reviewCreateRequest2 = 리뷰추가요청_재구매O_생성(4L, tagIds); - final var review = reviewRepository.findAll().get(0); - final var reviewId = review.getId(); + reviewService.create(productId, authorId, image, reviewCreateRequest1); + reviewService.create(productId, authorId, image, reviewCreateRequest2); + + final var reviews = reviewRepository.findAll(); + final var rating2_review = reviews.stream() + .filter(it -> it.getRating() == 2L) + .findFirst() + .get(); final var favoriteRequest = 리뷰좋아요요청_생성(true); - reviewService.likeReview(reviewId, authorId, favoriteRequest); - reviewService.likeReview(reviewId, memberId, favoriteRequest); + reviewService.likeReview(rating2_review.getId(), authorId, favoriteRequest); + reviewService.likeReview(rating2_review.getId(), memberId, favoriteRequest); // when - reviewService.deleteReview(reviewId, authorId); + reviewService.deleteReview(rating2_review.getId(), authorId); // then - final var tags = reviewTagRepository.findAll(); - final var favorites = reviewFavoriteRepository.findAll(); - final var findReview = reviewRepository.findById(reviewId); + final var tags = reviewTagRepository.findByReview(rating2_review); + final var favorites = reviewFavoriteRepository.findByReview(rating2_review); + final var findReview = reviewRepository.findById(rating2_review.getId()); + final var findProduct = productRepository.findById(productId).get(); assertSoftly(soft -> { soft.assertThat(tags).isEmpty(); soft.assertThat(favorites).isEmpty(); soft.assertThat(findReview).isEmpty(); + soft.assertThat(findProduct.getAverageRating()).isEqualTo(4.0); + soft.assertThat(findProduct.getReviewCount()).isEqualTo(1); }); } }