From 99a55ef6bd8ce9b0554c960e8ae3c6d996d9211c Mon Sep 17 00:00:00 2001 From: hanueleee Date: Mon, 9 Oct 2023 19:54:52 +0900 Subject: [PATCH 01/29] =?UTF-8?q?feat:=20=ED=95=B4=EB=8B=B9=20review?= =?UTF-8?q?=EC=97=90=20=EB=8B=AC=EB=A6=B0=20tag=EB=A5=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReviewTagRepository.java | 3 ++ .../persistence/ReviewTagRepositoryTest.java | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java index 7129a711c..1d30bc852 100644 --- a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java @@ -1,5 +1,6 @@ package com.funeat.review.persistence; +import com.funeat.review.domain.Review; import com.funeat.review.domain.ReviewTag; import com.funeat.tag.domain.Tag; import java.util.List; @@ -16,4 +17,6 @@ public interface ReviewTagRepository extends JpaRepository { + "GROUP BY rt.tag " + "ORDER BY cnt DESC") List findTop3TagsByReviewIn(final Long productId, final Pageable pageable); + + void deleteByReview(final Review review); } diff --git a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java index baab6abdb..8060f5fe3 100644 --- a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java +++ b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java @@ -72,6 +72,52 @@ class findTop3TagsByReviewIn_성공_테스트 { } } + @Nested + class deleteByReview_성공_테스트 { + + @Test + void 해당_리뷰에_달린_태그를_삭제할_수_있다() { + // given + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격3000원_평점2점_생성(category); + 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_단짠단짠_TASTE_생성(); + final var tag3 = 태그_갓성비_PRICE_생성(); + final var tag4 = 태그_간식_ETC_생성(); + 복수_태그_저장(tag1, tag2, tag3, tag4); + + final var review1 = 리뷰_이미지test5_평점5점_재구매O_생성(member, product, 0L); + final var review2 = 리뷰_이미지test3_평점3점_재구매X_생성(member, product, 0L); + final var review3 = 리뷰_이미지test4_평점4점_재구매O_생성(member, product, 0L); + 복수_리뷰_저장(review1, review2, review3); + + final var reviewTag1_1 = 리뷰_태그_생성(review1, tag1); + final var reviewTag1_2 = 리뷰_태그_생성(review1, tag2); + final var reviewTag2_1 = 리뷰_태그_생성(review2, tag1); + final var reviewTag2_2 = 리뷰_태그_생성(review2, tag2); + final var reviewTag2_3 = 리뷰_태그_생성(review2, tag3); + final var reviewTag3_1 = 리뷰_태그_생성(review3, tag1); + 복수_리뷰_태그_저장(reviewTag1_1, reviewTag1_2, reviewTag2_1, reviewTag2_2, reviewTag2_3, reviewTag3_1); + + final var expected = List.of(reviewTag2_1, reviewTag2_2, reviewTag2_3, reviewTag3_1); + + // when + reviewTagRepository.deleteByReview(review1); + + // then + final var remainings = reviewTagRepository.findAll(); + assertThat(remainings).usingRecursiveComparison() + .isEqualTo(expected); + } + } + private ReviewTag 리뷰_태그_생성(final Review review, final Tag tag) { return ReviewTag.createReviewTag(review, tag); } From 714167b66e014a6bbb98787702098a8adcff276d Mon Sep 17 00:00:00 2001 From: hanueleee Date: Mon, 9 Oct 2023 19:55:47 +0900 Subject: [PATCH 02/29] =?UTF-8?q?feat:=20=ED=95=B4=EB=8B=B9=20review?= =?UTF-8?q?=EC=97=90=20=EB=8B=AC=EB=A6=B0=20favorite=EC=9D=84=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReviewFavoriteRepository.java | 2 + .../ReviewFavoriteRepositoryTest.java | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java index 2e96e623a..30a9f90df 100644 --- a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java +++ b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java @@ -9,4 +9,6 @@ public interface ReviewFavoriteRepository extends JpaRepository { Optional findByMemberAndReview(final Member member, final Review review); + + void deleteByReview(Review review); } diff --git a/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java b/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java index 73ed00553..533e38c8b 100644 --- a/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java +++ b/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java @@ -3,6 +3,7 @@ import static com.funeat.fixture.CategoryFixture.카테고리_즉석조리_생성; import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성; import static com.funeat.fixture.MemberFixture.멤버_멤버2_생성; +import static com.funeat.fixture.MemberFixture.멤버_멤버3_생성; import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격1000원_평점3점_생성; import static com.funeat.fixture.ReviewFixture.리뷰_이미지test4_평점4점_재구매O_생성; import static com.funeat.fixture.ReviewFixture.리뷰_이미지test5_평점5점_재구매O_생성; @@ -11,6 +12,7 @@ import com.funeat.common.RepositoryTest; import com.funeat.member.domain.favorite.ReviewFavorite; +import java.util.List; import java.util.NoSuchElementException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -106,4 +108,44 @@ class findByMemberAndReview_실패_테스트 { .isInstanceOf(NoSuchElementException.class); } } + + @Nested + class deleteByReview_성공_테스트 { + + @Test + void 해당_리뷰에_달린_좋아요를_삭제할_수_있다() { + // given + final var member1 = 멤버_멤버1_생성(); + final var member2 = 멤버_멤버2_생성(); + final var member3 = 멤버_멤버3_생성(); + 복수_멤버_저장(member1, member2, member3); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + 단일_상품_저장(product); + + final var review1 = 리뷰_이미지test4_평점4점_재구매O_생성(member1, product, 0L); + final var review2 = 리뷰_이미지test5_평점5점_재구매O_생성(member1, product, 0L); + 복수_리뷰_저장(review1, review2); + + final var reviewFavorite1_1 = ReviewFavorite.create(member1, review1, true); + final var reviewFavorite1_2 = ReviewFavorite.create(member2, review1, true); + final var reviewFavorite1_3 = ReviewFavorite.create(member3, review1, true); + final var reviewFavorite2_1 = ReviewFavorite.create(member1, review2, true); + final var reviewFavorite2_2 = ReviewFavorite.create(member2, review2, true); + 복수_리뷰_좋아요_저장(reviewFavorite1_1, reviewFavorite1_2, reviewFavorite1_3, reviewFavorite2_1, reviewFavorite2_2); + + final var expected = List.of(reviewFavorite2_1, reviewFavorite2_2); + + // when + reviewFavoriteRepository.deleteByReview(review1); + + // then + final var remainings = reviewFavoriteRepository.findAll(); + assertThat(remainings).usingRecursiveComparison() + .isEqualTo(expected); + } + } } From e6103d38220b82fa1f5a028155028193eaf53d92 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Mon, 9 Oct 2023 20:54:13 +0900 Subject: [PATCH 03/29] =?UTF-8?q?feat:=20NotAuthorOfReviewException=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/funeat/review/exception/ReviewErrorCode.java | 1 + .../java/com/funeat/review/exception/ReviewException.java | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java index d91c0c8c3..986ee6ef8 100644 --- a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java +++ b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java @@ -5,6 +5,7 @@ public enum ReviewErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 리뷰입니다. 리뷰 id를 확인하세요.", "3001"), + NOT_AUTHOR_OF_REVIEW(HttpStatus.BAD_REQUEST, "리뷰의 작성자가 아닙니다.", "3002") ; private final HttpStatus status; diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewException.java b/backend/src/main/java/com/funeat/review/exception/ReviewException.java index 4699f3af6..a961f3301 100644 --- a/backend/src/main/java/com/funeat/review/exception/ReviewException.java +++ b/backend/src/main/java/com/funeat/review/exception/ReviewException.java @@ -15,4 +15,10 @@ public ReviewNotFoundException(final ReviewErrorCode errorCode, final Long revie super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), reviewId)); } } + + public static class NotAuthorOfReviewException extends ReviewException { + public NotAuthorOfReviewException(final ReviewErrorCode errorCode, final Long memberId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), memberId)); + } + } } From fd8c80ded4a5b5c45c369a6d7caa7834c49306e5 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Mon, 9 Oct 2023 21:07:20 +0900 Subject: [PATCH 04/29] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 37 ++++++++ .../java/com/funeat/review/domain/Review.java | 4 + .../presentation/ReviewApiController.java | 10 +++ .../review/presentation/ReviewController.java | 10 +++ .../review/application/ReviewServiceTest.java | 85 +++++++++++++++++++ 5 files changed, 146 insertions(+) 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 ec7020cad..e2bf4dae8 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -3,6 +3,7 @@ import static com.funeat.member.exception.MemberErrorCode.MEMBER_DUPLICATE_FAVORITE; import static com.funeat.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; +import static com.funeat.review.exception.ReviewErrorCode.NOT_AUTHOR_OF_REVIEW; import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND; import com.funeat.common.ImageUploader; @@ -26,6 +27,7 @@ import com.funeat.review.dto.ReviewFavoriteRequest; import com.funeat.review.dto.SortingReviewDto; import com.funeat.review.dto.SortingReviewsResponse; +import com.funeat.review.exception.ReviewException.NotAuthorOfReviewException; import com.funeat.review.exception.ReviewException.ReviewNotFoundException; import com.funeat.review.persistence.ReviewRepository; import com.funeat.review.persistence.ReviewTagRepository; @@ -178,4 +180,39 @@ public MemberReviewsResponse findReviewByMember(final Long memberId, final Pagea return MemberReviewsResponse.toResponse(pageDto, dtos); } + + @Transactional + public void deleteReview(final Long reviewId, final Long memberId) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); + final Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); + final Product product = review.getProduct(); + + if (review.checkAuthor(member)) { + deleteThingsRelatedToReview(reviewId, review); + updateProductImage(product); + // TODO : s3에 있는 image삭제 + } else { + throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); // TODO : memberId만 info로 넘겨도 괜찮은가? + } + } + + private void deleteThingsRelatedToReview(final Long reviewId, final Review review) { + reviewTagRepository.deleteByReview(review); + reviewFavoriteRepository.deleteByReview(review); + reviewRepository.deleteById(reviewId); + } + + // TODO : 또다른 updateProductImage 메소드에 대해 질문 + private void updateProductImage(final Product product) { + final Long productId = product.getId(); + final PageRequest pageRequest = PageRequest.of(TOP, ONE); + + final List topFavoriteReview = reviewRepository.findPopularReviewWithImage(productId, pageRequest); + if (!topFavoriteReview.isEmpty()) { + final String topFavoriteReviewImage = topFavoriteReview.get(TOP).getImage(); + product.updateImage(topFavoriteReviewImage); + } + } } diff --git a/backend/src/main/java/com/funeat/review/domain/Review.java b/backend/src/main/java/com/funeat/review/domain/Review.java index 3545371e3..f01788dd8 100644 --- a/backend/src/main/java/com/funeat/review/domain/Review.java +++ b/backend/src/main/java/com/funeat/review/domain/Review.java @@ -88,6 +88,10 @@ public void minusFavoriteCount() { this.favoriteCount--; } + public boolean checkAuthor(Member member) { + return this.member == member; + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index 00c7683b6..e9010cbf0 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -14,6 +14,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -55,6 +56,15 @@ public ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, return ResponseEntity.noContent().build(); } + @Logging + @DeleteMapping("/api/products/{productId}/reviews/{reviewId}") + public ResponseEntity deleteReview(@PathVariable final Long reviewId, + @AuthenticationPrincipal final LoginInfo loginInfo) { + reviewService.deleteReview(reviewId, loginInfo.getId()); + + return ResponseEntity.ok().build(); + } + @GetMapping("/api/products/{productId}/reviews") public ResponseEntity getSortingReviews(@AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long productId, diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java index 574e50fb4..e46c3667a 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -44,6 +45,15 @@ ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestBody final ReviewFavoriteRequest request); + @Operation(summary = "리뷰 삭제", description = "자신이 작성한 리뷰를 삭제한다.") + @ApiResponse( + responseCode = "200", + description = "리뷰 삭제 성공." + ) + @DeleteMapping("/api/products/{productId}/reviews/{reviewId}") + ResponseEntity deleteReview(@PathVariable final Long reviewId, + @AuthenticationPrincipal final LoginInfo loginInfo); + @Operation(summary = "리뷰를 정렬후 조회", description = "리뷰를 정렬후 조회한다.") @ApiResponse( responseCode = "200", 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 aa2a0b33e..693c4b06c 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -39,6 +39,7 @@ import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.review.domain.Review; import com.funeat.review.dto.SortingReviewDto; +import com.funeat.review.exception.ReviewException.NotAuthorOfReviewException; import com.funeat.review.exception.ReviewException.ReviewNotFoundException; import com.funeat.tag.domain.Tag; import java.util.List; @@ -793,6 +794,90 @@ class updateProductImage_실패_테스트 { } } + @Nested + class deleteReview_성공_테스트 { + + @Test + void 자신이_작성한_리뷰를_삭제할_수_있다() { + // given + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + final var member = 멤버_멤버2_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + final var reviewCreateRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + final var favoriteRequest = 리뷰좋아요요청_생성(true); + reviewService.likeReview(reviewId, authorId, favoriteRequest); + reviewService.likeReview(reviewId, memberId, favoriteRequest); + + // when + reviewService.deleteReview(reviewId, authorId); + + // then + final var tags = reviewTagRepository.findAll(); + final var favorites = reviewFavoriteRepository.findAll(); + final var findReview = reviewRepository.findById(reviewId); + + assertSoftly(soft -> { + soft.assertThat(tags).isEmpty(); + soft.assertThat(favorites).isEmpty(); + soft.assertThat(findReview).isEmpty(); + }); + } + } + + @Nested + class deleteReview_실패_테스트 { + + @Test + void 자신이_작성하지_않은_리뷰를_삭제하려하면_에러가_발생한다() { + // given + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + final var member = 멤버_멤버2_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + final var reviewCreateRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + // when & then + assertThatThrownBy(() -> reviewService.deleteReview(reviewId, memberId)) + .isInstanceOf(NotAuthorOfReviewException.class); + } + } + private List 태그_아이디_변환(final Tag... tags) { return Stream.of(tags) .map(Tag::getId) From 6ddcdeb1605741fa68411710d1f1595d89574409 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Mon, 9 Oct 2023 21:37:16 +0900 Subject: [PATCH 05/29] =?UTF-8?q?feat:=20s3=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/funeat/common/ImageUploader.java | 1 + .../funeat/common/exception/CommonException.java | 6 ++++++ .../main/java/com/funeat/common/s3/S3Uploader.java | 13 +++++++++++++ .../funeat/review/application/ReviewService.java | 8 ++++---- .../java/com/funeat/common/TestImageUploader.java | 4 ++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/funeat/common/ImageUploader.java b/backend/src/main/java/com/funeat/common/ImageUploader.java index 754b1affd..d4650ddd4 100644 --- a/backend/src/main/java/com/funeat/common/ImageUploader.java +++ b/backend/src/main/java/com/funeat/common/ImageUploader.java @@ -5,4 +5,5 @@ public interface ImageUploader { String upload(final MultipartFile image); + void delete(final String fileName); } diff --git a/backend/src/main/java/com/funeat/common/exception/CommonException.java b/backend/src/main/java/com/funeat/common/exception/CommonException.java index e2e822c68..55be12d5d 100644 --- a/backend/src/main/java/com/funeat/common/exception/CommonException.java +++ b/backend/src/main/java/com/funeat/common/exception/CommonException.java @@ -22,4 +22,10 @@ public S3UploadFailException(final CommonErrorCode errorCode) { super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); } } + + public static class S3DeleteFailException extends CommonException { + public S3DeleteFailException(final CommonErrorCode errorCode) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } } diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index 3f9c86caa..5f01152e2 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -3,11 +3,13 @@ import static com.funeat.exception.CommonErrorCode.IMAGE_EXTENSION_ERROR_CODE; import static com.funeat.exception.CommonErrorCode.UNKNOWN_SERVER_ERROR_CODE; +import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.funeat.common.ImageUploader; import com.funeat.common.exception.CommonException.NotAllowedFileExtensionException; +import com.funeat.common.exception.CommonException.S3DeleteFailException; import com.funeat.common.exception.CommonException.S3UploadFailException; import java.io.IOException; import java.util.List; @@ -53,6 +55,17 @@ public String upload(final MultipartFile image) { } } + @Override + public void delete(final String fileName) { + // TODO : DB 저장 값에서 cloudfront 주소 빼기 + try { + final String key = folder + fileName; + amazonS3.deleteObject(bucket, key); + } catch (AmazonServiceException e) { + throw new S3DeleteFailException(UNKNOWN_SERVER_ERROR_CODE); + } + } + private void validateExtension(final MultipartFile image) { final String contentType = image.getContentType(); if (!INCLUDE_EXTENSIONS.contains(contentType)) { 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 e2bf4dae8..e6fc196d1 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -188,13 +188,14 @@ public void deleteReview(final Long reviewId, final Long memberId) { final Review review = reviewRepository.findById(reviewId) .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); final Product product = review.getProduct(); + final String image = review.getImage(); if (review.checkAuthor(member)) { deleteThingsRelatedToReview(reviewId, review); updateProductImage(product); - // TODO : s3에 있는 image삭제 + imageUploader.delete(image); } else { - throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); // TODO : memberId만 info로 넘겨도 괜찮은가? + throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); } } @@ -203,8 +204,7 @@ private void deleteThingsRelatedToReview(final Long reviewId, final Review revie reviewFavoriteRepository.deleteByReview(review); reviewRepository.deleteById(reviewId); } - - // TODO : 또다른 updateProductImage 메소드에 대해 질문 + private void updateProductImage(final Product product) { final Long productId = product.getId(); final PageRequest pageRequest = PageRequest.of(TOP, ONE); diff --git a/backend/src/test/java/com/funeat/common/TestImageUploader.java b/backend/src/test/java/com/funeat/common/TestImageUploader.java index 58d4ab6f8..642da2176 100644 --- a/backend/src/test/java/com/funeat/common/TestImageUploader.java +++ b/backend/src/test/java/com/funeat/common/TestImageUploader.java @@ -30,6 +30,10 @@ public String upload(final MultipartFile image) { } } + @Override + public void delete(final String fileName) { + } + private void deleteDirectory(Path directory) throws IOException { // 디렉토리 내부 파일 및 디렉토리 삭제 try (Stream pathStream = Files.walk(directory)) { From f6f2acd4d3beebae4c493d098130f2fc1ec3ce7c Mon Sep 17 00:00:00 2001 From: hanueleee Date: Mon, 9 Oct 2023 21:47:24 +0900 Subject: [PATCH 06/29] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=88=98=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/ReviewAcceptanceTest.java | 41 +++++++++++++++++++ .../funeat/acceptance/review/ReviewSteps.java | 10 +++++ 2 files changed, 51 insertions(+) diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index 07552c34c..5369489b4 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -12,6 +12,7 @@ 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.리뷰_작성_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_좋아요_요청; import static com.funeat.acceptance.review.ReviewSteps.여러명이_리뷰_좋아요_요청; @@ -611,6 +612,46 @@ class getRankingReviews_성공_테스트 { } } + @Nested + class deleteReview_성공_테스트 { + + @Test + void 자신이_작성한_리뷰를_삭제한다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 상품, 리뷰1); + + // then + STATUS_CODE를_검증한다(응답, 정상_처리); + } + } + + @Nested + class deleteReview_실패_테스트 { + + @Test + void 자신이_작성하지_않은_리뷰는_삭제할_수_없다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버2), 상품, 리뷰1); + + // then + STATUS_CODE를_검증한다(응답, 잘못된_요청); + } + } + private void RESPONSE_CODE와_MESSAGE를_검증한다(final ExtractableResponse response, final String expectedCode, final String expectedMessage) { assertSoftly(soft -> { diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java index d59d9eded..7cd44e61a 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java @@ -74,4 +74,14 @@ public class ReviewSteps { .then() .extract(); } + + public static ExtractableResponse 리뷰_삭제_요청(final String loginCookie, + final Long productId, final Long reviewId) { + return given() + .cookie("FUNEAT", loginCookie) + .when() + .delete("/api/products/{productId}/reviews/{reviewId}", productId, reviewId) + .then() + .extract(); + } } From bde3448b6764631a71b329a992dcf40be1e8bf49 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Tue, 10 Oct 2023 17:15:09 +0900 Subject: [PATCH 07/29] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/funeat/common/ImageUploader.java | 1 + .../java/com/funeat/review/application/ReviewService.java | 4 ++-- .../java/com/funeat/review/exception/ReviewErrorCode.java | 2 +- .../com/funeat/review/presentation/ReviewApiController.java | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/funeat/common/ImageUploader.java b/backend/src/main/java/com/funeat/common/ImageUploader.java index d4650ddd4..afd4b5c10 100644 --- a/backend/src/main/java/com/funeat/common/ImageUploader.java +++ b/backend/src/main/java/com/funeat/common/ImageUploader.java @@ -5,5 +5,6 @@ public interface ImageUploader { String upload(final MultipartFile image); + void delete(final String fileName); } 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 e6fc196d1..dd1e3dcbb 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -194,9 +194,9 @@ public void deleteReview(final Long reviewId, final Long memberId) { deleteThingsRelatedToReview(reviewId, review); updateProductImage(product); imageUploader.delete(image); - } else { - throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); + return; } + throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); } private void deleteThingsRelatedToReview(final Long reviewId, final Review review) { diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java index 986ee6ef8..05331dac9 100644 --- a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java +++ b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java @@ -5,7 +5,7 @@ public enum ReviewErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 리뷰입니다. 리뷰 id를 확인하세요.", "3001"), - NOT_AUTHOR_OF_REVIEW(HttpStatus.BAD_REQUEST, "리뷰의 작성자가 아닙니다.", "3002") + NOT_AUTHOR_OF_REVIEW(HttpStatus.BAD_REQUEST, "해당 리뷰를 작성한 회원이 아닙니다", "3002") ; private final HttpStatus status; diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index e9010cbf0..1a99dcd15 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -59,7 +59,7 @@ public ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, @Logging @DeleteMapping("/api/products/{productId}/reviews/{reviewId}") public ResponseEntity deleteReview(@PathVariable final Long reviewId, - @AuthenticationPrincipal final LoginInfo loginInfo) { + @AuthenticationPrincipal final LoginInfo loginInfo) { reviewService.deleteReview(reviewId, loginInfo.getId()); return ResponseEntity.ok().build(); From 6fa2d9bcc11ff5d6e47aeb752c61b9e109110837 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Tue, 10 Oct 2023 23:10:00 +0900 Subject: [PATCH 08/29] =?UTF-8?q?refactor:=20deleteAllByIdInBatch=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReviewFavoriteRepository.java | 3 +++ .../review/application/ReviewService.java | 26 +++++++++++++++---- .../persistence/ReviewTagRepository.java | 2 ++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java index 30a9f90df..7b312fb89 100644 --- a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java +++ b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java @@ -3,6 +3,7 @@ import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.review.domain.Review; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,4 +12,6 @@ public interface ReviewFavoriteRepository extends JpaRepository findByMemberAndReview(final Member member, final Review review); void deleteByReview(Review review); + + List findByReviewId(Long reviewId); } 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 dd1e3dcbb..38136f3ec 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -191,7 +191,7 @@ public void deleteReview(final Long reviewId, final Long memberId) { final String image = review.getImage(); if (review.checkAuthor(member)) { - deleteThingsRelatedToReview(reviewId, review); + deleteThingsRelatedToReview(reviewId); updateProductImage(product); imageUploader.delete(image); return; @@ -199,12 +199,28 @@ public void deleteReview(final Long reviewId, final Long memberId) { throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); } - private void deleteThingsRelatedToReview(final Long reviewId, final Review review) { - reviewTagRepository.deleteByReview(review); - reviewFavoriteRepository.deleteByReview(review); + private void deleteThingsRelatedToReview(final Long reviewId) { + deleteReviewTags(reviewId); + deleteReviewFavorites(reviewId); reviewRepository.deleteById(reviewId); } - + + private void deleteReviewTags(final Long reviewId) { + List reviewTags = reviewTagRepository.findByReviewId(reviewId); + List ids = reviewTags.stream() + .map(ReviewTag::getId) + .collect(Collectors.toList()); + reviewTagRepository.deleteAllByIdInBatch(ids); + } + + private void deleteReviewFavorites(final Long reviewId) { + List reviewFavorites = reviewFavoriteRepository.findByReviewId(reviewId); + List ids = reviewFavorites.stream() + .map(ReviewFavorite::getId) + .collect(Collectors.toList()); + reviewFavoriteRepository.deleteAllByIdInBatch(ids); + } + private void updateProductImage(final Product product) { final Long productId = product.getId(); final PageRequest pageRequest = PageRequest.of(TOP, ONE); diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java index 1d30bc852..c813a649f 100644 --- a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java @@ -19,4 +19,6 @@ public interface ReviewTagRepository extends JpaRepository { List findTop3TagsByReviewIn(final Long productId, final Pageable pageable); void deleteByReview(final Review review); + + List findByReviewId(Long reviewId); } From 6d540891c315769e23303038e2fb14fc1e8bf733 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Tue, 10 Oct 2023 23:38:37 +0900 Subject: [PATCH 09/29] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=8B=A4=ED=8C=A8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/ReviewAcceptanceTest.java | 37 +++++++++++- .../review/application/ReviewServiceTest.java | 59 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index 5369489b4..91dc67a26 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -616,7 +616,7 @@ class getRankingReviews_성공_테스트 { class deleteReview_성공_테스트 { @Test - void 자신이_작성한_리뷰를_삭제한다() { + void 자신이_작성한_리뷰를_삭제할_수_있다() { // given final var 카테고리 = 카테고리_즉석조리_생성(); 단일_카테고리_저장(카테고리); @@ -635,6 +635,41 @@ class deleteReview_성공_테스트 { @Nested class deleteReview_실패_테스트 { + @ParameterizedTest + @NullAndEmptySource + void 로그인하지_않는_사용자가_리뷰_삭제시_예외가_발생한다(final String cookie) { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(cookie, 상품, 리뷰1); + + // then + STATUS_CODE를_검증한다(응답, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(), LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void 존재하지_않는_리뷰를_삭제할_수_없다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 상품, 리뷰2); + + // then + STATUS_CODE를_검증한다(응답, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, REVIEW_NOT_FOUND.getCode(), REVIEW_NOT_FOUND.getMessage()); + } + @Test void 자신이_작성하지_않은_리뷰는_삭제할_수_없다() { // given 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 693c4b06c..a16f50dee 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -846,6 +846,65 @@ class deleteReview_성공_테스트 { @Nested class deleteReview_실패_테스트 { + @Test + void 존재하지_않는_사용자가_리뷰를_삭제하려하면_에러가_발생한다() { + // given + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + final var reviewCreateRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + final var wrongMemberId = 999L; + + // when & then + assertThatThrownBy(() -> reviewService.deleteReview(reviewId, wrongMemberId)) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + void 존재하지_않는_리뷰를_삭제하려하면_에러가_발생한다() { + // given + final var author = 멤버_멤버1_생성(); + final var authorId = 단일_멤버_저장(author); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + final var reviewCreateRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var wrongReviewId = 999L; + + // when & then + assertThatThrownBy(() -> reviewService.deleteReview(wrongReviewId, authorId)) + .isInstanceOf(ReviewNotFoundException.class); + } + @Test void 자신이_작성하지_않은_리뷰를_삭제하려하면_에러가_발생한다() { // given From 7534544641ab8d80ea326ac9f6d07d5d0adbddbc Mon Sep 17 00:00:00 2001 From: hanueleee Date: Wed, 11 Oct 2023 00:31:06 +0900 Subject: [PATCH 10/29] =?UTF-8?q?refactor:=20updateProductImage=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/ReviewService.java | 22 +++------------ .../presentation/ReviewApiController.java | 5 ++-- .../review/presentation/ReviewController.java | 3 ++- .../review/application/ReviewServiceTest.java | 27 +++++++++---------- 4 files changed, 22 insertions(+), 35 deletions(-) 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 38136f3ec..f2151b6bb 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -125,14 +125,11 @@ private ReviewFavorite saveReviewFavorite(final Member member, final Review revi } @Transactional - public void updateProductImage(final Long reviewId) { - final Review review = reviewRepository.findById(reviewId) - .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); + public void updateProductImage(final Long productId) { + final Product product = productRepository.findById(productId) + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); - final Product product = review.getProduct(); - final Long productId = product.getId(); final PageRequest pageRequest = PageRequest.of(TOP, ONE); - final List topFavoriteReview = reviewRepository.findPopularReviewWithImage(productId, pageRequest); if (!topFavoriteReview.isEmpty()) { final String topFavoriteReviewImage = topFavoriteReview.get(TOP).getImage(); @@ -192,7 +189,7 @@ public void deleteReview(final Long reviewId, final Long memberId) { if (review.checkAuthor(member)) { deleteThingsRelatedToReview(reviewId); - updateProductImage(product); + updateProductImage(product.getId()); imageUploader.delete(image); return; } @@ -220,15 +217,4 @@ private void deleteReviewFavorites(final Long reviewId) { .collect(Collectors.toList()); reviewFavoriteRepository.deleteAllByIdInBatch(ids); } - - private void updateProductImage(final Product product) { - final Long productId = product.getId(); - final PageRequest pageRequest = PageRequest.of(TOP, ONE); - - final List topFavoriteReview = reviewRepository.findPopularReviewWithImage(productId, pageRequest); - if (!topFavoriteReview.isEmpty()) { - final String topFavoriteReviewImage = topFavoriteReview.get(TOP).getImage(); - product.updateImage(topFavoriteReviewImage); - } - } } diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index 1a99dcd15..8e3fa46bc 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -47,11 +47,12 @@ public ResponseEntity writeReview(@PathVariable final Long productId, @Logging @PatchMapping("/api/products/{productId}/reviews/{reviewId}") - public ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, + public ResponseEntity toggleLikeReview(@PathVariable final Long productId, + @PathVariable final Long reviewId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestBody @Valid final ReviewFavoriteRequest request) { reviewService.likeReview(reviewId, loginInfo.getId(), request); - reviewService.updateProductImage(reviewId); + reviewService.updateProductImage(productId); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java index e46c3667a..0f86e3975 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java @@ -41,7 +41,8 @@ ResponseEntity writeReview(@PathVariable final Long productId, description = "리뷰 좋아요(취소) 성공." ) @PatchMapping - ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, + ResponseEntity toggleLikeReview(@PathVariable final Long productId, + @PathVariable final Long reviewId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestBody final ReviewFavoriteRequest request); 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 a16f50dee..11ad26376 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -613,7 +613,7 @@ class updateProductImage_성공_테스트 { final var expected = review.getImage(); // when - reviewService.updateProductImage(reviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -642,7 +642,7 @@ class updateProductImage_성공_테스트 { final var expected = secondReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -671,7 +671,7 @@ class updateProductImage_성공_테스트 { final var expected = firstReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -700,7 +700,7 @@ class updateProductImage_성공_테스트 { final var expected = secondReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -721,11 +721,11 @@ class updateProductImage_성공_테스트 { final var firstReview = 리뷰_이미지없음_평점1점_재구매O_생성(member, product, 3L); final var firstReviewId = 단일_리뷰_저장(firstReview); - reviewService.updateProductImage(firstReviewId); + reviewService.updateProductImage(product.getId()); final var secondReview = 리뷰_이미지없음_평점1점_재구매O_생성(member, product, 2L); final var secondReviewId = 단일_리뷰_저장(secondReview); - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var thirdReview = 리뷰_이미지test3_평점3점_재구매O_생성(member, product, 1L); final var thirdReviewId = 단일_리뷰_저장(thirdReview); @@ -733,7 +733,7 @@ class updateProductImage_성공_테스트 { final var expected = thirdReview.getImage(); // when - reviewService.updateProductImage(thirdReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -754,7 +754,7 @@ class updateProductImage_성공_테스트 { final var firstReview = 리뷰_이미지없음_평점1점_재구매O_생성(member, product, 3L); final var firstReviewId = 단일_리뷰_저장(firstReview); - reviewService.updateProductImage(firstReviewId); + reviewService.updateProductImage(product.getId()); final var secondReview = 리뷰_이미지없음_평점1점_재구매O_생성(member, product, 2L); final var secondReviewId = 단일_리뷰_저장(secondReview); @@ -762,7 +762,7 @@ class updateProductImage_성공_테스트 { final var expected = secondReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -774,7 +774,7 @@ class updateProductImage_성공_테스트 { class updateProductImage_실패_테스트 { @Test - void 존재하지_않는_리뷰로_상품_업데이트를_시도하면_예외가_발생한다() { + void 존재하지_않는_상품으로_상품_업데이트를_시도하면_예외가_발생한다() { // given final var member = 멤버_멤버1_생성(); 단일_멤버_저장(member); @@ -785,12 +785,11 @@ class updateProductImage_실패_테스트 { final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); 단일_상품_저장(product); - final var review = 리뷰_이미지test1_평점1점_재구매O_생성(member, product, 0L); - final var wrongReviewId = 단일_리뷰_저장(review) + 1L; + final var wrongProductId = 999L; // when & then - assertThatThrownBy(() -> reviewService.updateProductImage(wrongReviewId)) - .isInstanceOf(ReviewNotFoundException.class); + assertThatThrownBy(() -> reviewService.updateProductImage(wrongProductId)) + .isInstanceOf(ProductNotFoundException.class); } } From 6e44dcb7ffeb08c71a9308b96a46ab7dee09b528 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Wed, 11 Oct 2023 12:17:42 +0900 Subject: [PATCH 11/29] =?UTF-8?q?feat:=20s3=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=A7=80=EC=A0=95=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/funeat/common/s3/S3Uploader.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index 5f01152e2..973fb7ade 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -23,6 +23,7 @@ @Profile("!test") public class S3Uploader implements ImageUploader { + private static final int BEGIN_INDEX = 31; private static final List INCLUDE_EXTENSIONS = List.of("image/jpeg", "image/png", "image/webp"); @Value("${cloud.aws.s3.bucket}") @@ -56,10 +57,10 @@ public String upload(final MultipartFile image) { } @Override - public void delete(final String fileName) { - // TODO : DB 저장 값에서 cloudfront 주소 빼기 + public void delete(final String image) { + String imageName = image.substring(BEGIN_INDEX); try { - final String key = folder + fileName; + final String key = folder + imageName; amazonS3.deleteObject(bucket, key); } catch (AmazonServiceException e) { throw new S3DeleteFailException(UNKNOWN_SERVER_ERROR_CODE); From f457738bb5db262604da5b60e9a995ed420d2117 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Wed, 11 Oct 2023 12:23:35 +0900 Subject: [PATCH 12/29] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=EC=97=90?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EA=B0=80=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=EC=97=90=EB=A7=8C=20s3=20delete=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=8B=A4=ED=96=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/funeat/review/application/ReviewService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 f2151b6bb..018b6b74f 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -190,7 +190,7 @@ public void deleteReview(final Long reviewId, final Long memberId) { if (review.checkAuthor(member)) { deleteThingsRelatedToReview(reviewId); updateProductImage(product.getId()); - imageUploader.delete(image); + deleteS3Image(image); return; } throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); @@ -217,4 +217,10 @@ private void deleteReviewFavorites(final Long reviewId) { .collect(Collectors.toList()); reviewFavoriteRepository.deleteAllByIdInBatch(ids); } + + private void deleteS3Image(final String image) { + if (image != null) { + imageUploader.delete(image); + } + } } From 4842491b0627a87e80683a8fabf7dc6a28e12ad7 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Wed, 11 Oct 2023 13:44:59 +0900 Subject: [PATCH 13/29] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=84=B1=EA=B3=B5=EC=8B=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=BD=94=EB=93=9C=20204=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/funeat/review/presentation/ReviewApiController.java | 2 +- .../java/com/funeat/review/presentation/ReviewController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index 8e3fa46bc..83594bd76 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -63,7 +63,7 @@ public ResponseEntity deleteReview(@PathVariable final Long reviewId, @AuthenticationPrincipal final LoginInfo loginInfo) { reviewService.deleteReview(reviewId, loginInfo.getId()); - return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build(); } @GetMapping("/api/products/{productId}/reviews") diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java index 0f86e3975..e06c2dbb2 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java @@ -48,7 +48,7 @@ ResponseEntity toggleLikeReview(@PathVariable final Long productId, @Operation(summary = "리뷰 삭제", description = "자신이 작성한 리뷰를 삭제한다.") @ApiResponse( - responseCode = "200", + responseCode = "204", description = "리뷰 삭제 성공." ) @DeleteMapping("/api/products/{productId}/reviews/{reviewId}") From c0e02a0728bb7f8aa84979e08f66c152a04d0466 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Wed, 11 Oct 2023 15:37:18 +0900 Subject: [PATCH 14/29] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=84=B1=EA=B3=B5=EC=8B=9C=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20204=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=9D=B8=EC=88=98=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/funeat/acceptance/review/ReviewAcceptanceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index 91dc67a26..fbb37beaa 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -628,7 +628,7 @@ class deleteReview_성공_테스트 { final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 상품, 리뷰1); // then - STATUS_CODE를_검증한다(응답, 정상_처리); + STATUS_CODE를_검증한다(응답, 정상_처리_NO_CONTENT); } } From 20470eee10f41e89c769d1e435639803414a2676 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Wed, 11 Oct 2023 21:25:45 +0900 Subject: [PATCH 15/29] =?UTF-8?q?feat:=20s3=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/funeat/FuneatApplication.java | 3 +- .../review/application/ReviewDeleteEvent.java | 14 ++++++++ .../ReviewDeleteEventListener.java | 32 +++++++++++++++++++ .../review/application/ReviewService.java | 12 ++++--- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java create mode 100644 backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java diff --git a/backend/src/main/java/com/funeat/FuneatApplication.java b/backend/src/main/java/com/funeat/FuneatApplication.java index 9fae64b95..107e05700 100644 --- a/backend/src/main/java/com/funeat/FuneatApplication.java +++ b/backend/src/main/java/com/funeat/FuneatApplication.java @@ -2,12 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication +@EnableAsync public class FuneatApplication { public static void main(String[] args) { SpringApplication.run(FuneatApplication.class, args); } - } diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java new file mode 100644 index 000000000..6341986d1 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java @@ -0,0 +1,14 @@ +package com.funeat.review.application; + +public class ReviewDeleteEvent { + + private String image; + + public ReviewDeleteEvent(final String image) { + this.image = image; + } + + public String getImage() { + return image; + } +} diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java new file mode 100644 index 000000000..2e59760ab --- /dev/null +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java @@ -0,0 +1,32 @@ +package com.funeat.review.application; + +import com.funeat.common.ImageUploader; +import com.funeat.common.exception.CommonException.S3DeleteFailException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +public class ReviewDeleteEventListener { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + private final ImageUploader imageUploader; + + public ReviewDeleteEventListener(final ImageUploader imageUploader) { + this.imageUploader = imageUploader; + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void deleteReviewImageInS3(ReviewDeleteEvent event) { + String image = event.getImage(); + try { + imageUploader.delete(image); + } catch (S3DeleteFailException e) { + log.warn("S3 이미지 삭제가 실패했습니다. 이미지 경로 : {}", image); // TODO : 이미지 삭제 실패시 처리? + } + } +} 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 018b6b74f..1c7a3561f 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -59,11 +60,13 @@ public class ReviewService { private final ProductRepository productRepository; private final ReviewFavoriteRepository reviewFavoriteRepository; private final ImageUploader imageUploader; + private final ApplicationEventPublisher eventPublisher; public ReviewService(final ReviewRepository reviewRepository, final TagRepository tagRepository, final ReviewTagRepository reviewTagRepository, final MemberRepository memberRepository, final ProductRepository productRepository, - final ReviewFavoriteRepository reviewFavoriteRepository, final ImageUploader imageUploader) { + final ReviewFavoriteRepository reviewFavoriteRepository, + final ImageUploader imageUploader, final ApplicationEventPublisher eventPublisher) { this.reviewRepository = reviewRepository; this.tagRepository = tagRepository; this.reviewTagRepository = reviewTagRepository; @@ -71,6 +74,7 @@ public ReviewService(final ReviewRepository reviewRepository, final TagRepositor this.productRepository = productRepository; this.reviewFavoriteRepository = reviewFavoriteRepository; this.imageUploader = imageUploader; + this.eventPublisher = eventPublisher; } @Transactional @@ -190,7 +194,7 @@ public void deleteReview(final Long reviewId, final Long memberId) { if (review.checkAuthor(member)) { deleteThingsRelatedToReview(reviewId); updateProductImage(product.getId()); - deleteS3Image(image); + deleteReviewImage(image); return; } throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); @@ -218,9 +222,9 @@ private void deleteReviewFavorites(final Long reviewId) { reviewFavoriteRepository.deleteAllByIdInBatch(ids); } - private void deleteS3Image(final String image) { + private void deleteReviewImage(final String image) { if (image != null) { - imageUploader.delete(image); + eventPublisher.publishEvent(new ReviewDeleteEvent(image)); } } } From acb87e5776ce5232cec5b563efde33639885c515 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 00:00:06 +0900 Subject: [PATCH 16/29] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=9E=88=EC=9D=84=20=EB=95=8C=EB=A7=8C=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=9C=ED=96=89=ED=95=98=EB=8D=98=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9C=A0?= =?UTF-8?q?=EB=AC=B4=20=EC=83=81=EA=B4=80=EC=97=86=EC=9D=B4=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=9C=A0=EB=AC=B4=20=EC=B2=98=EB=A6=AC=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=A6=AC=EC=8A=A4=EB=84=88=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=ED=95=98=EB=8F=84=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/funeat/common/s3/S3Uploader.java | 5 +++++ .../review/application/ReviewDeleteEventListener.java | 8 +------- .../java/com/funeat/review/application/ReviewService.java | 8 +------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index 973fb7ade..421767131 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -14,6 +14,8 @@ import java.io.IOException; import java.util.List; import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -26,6 +28,8 @@ public class S3Uploader implements ImageUploader { private static final int BEGIN_INDEX = 31; private static final List INCLUDE_EXTENSIONS = List.of("image/jpeg", "image/png", "image/webp"); + private final Logger log = LoggerFactory.getLogger(this.getClass()); + @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -63,6 +67,7 @@ public void delete(final String image) { final String key = folder + imageName; amazonS3.deleteObject(bucket, key); } catch (AmazonServiceException e) { + log.error("S3 이미지 삭제가 실패했습니다. 이미지 경로 : {}", image); throw new S3DeleteFailException(UNKNOWN_SERVER_ERROR_CODE); } } diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java index 2e59760ab..3ae7d8454 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java @@ -1,9 +1,6 @@ package com.funeat.review.application; import com.funeat.common.ImageUploader; -import com.funeat.common.exception.CommonException.S3DeleteFailException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; @@ -12,7 +9,6 @@ @Component public class ReviewDeleteEventListener { - private final Logger log = LoggerFactory.getLogger(this.getClass()); private final ImageUploader imageUploader; public ReviewDeleteEventListener(final ImageUploader imageUploader) { @@ -23,10 +19,8 @@ public ReviewDeleteEventListener(final ImageUploader imageUploader) { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void deleteReviewImageInS3(ReviewDeleteEvent event) { String image = event.getImage(); - try { + if (image != null) { imageUploader.delete(image); - } catch (S3DeleteFailException e) { - log.warn("S3 이미지 삭제가 실패했습니다. 이미지 경로 : {}", image); // TODO : 이미지 삭제 실패시 처리? } } } 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 1c7a3561f..045a0c13d 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -192,9 +192,9 @@ public void deleteReview(final Long reviewId, final Long memberId) { final String image = review.getImage(); if (review.checkAuthor(member)) { + eventPublisher.publishEvent(new ReviewDeleteEvent(image)); deleteThingsRelatedToReview(reviewId); updateProductImage(product.getId()); - deleteReviewImage(image); return; } throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); @@ -221,10 +221,4 @@ private void deleteReviewFavorites(final Long reviewId) { .collect(Collectors.toList()); reviewFavoriteRepository.deleteAllByIdInBatch(ids); } - - private void deleteReviewImage(final String image) { - if (image != null) { - eventPublisher.publishEvent(new ReviewDeleteEvent(image)); - } - } } From 377e1bb3681a723be8f9a8eefbacb8c54807ffbd Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 00:03:01 +0900 Subject: [PATCH 17/29] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/funeat/common/EventTest.java | 64 +++++++ .../ReviewDeleteEventListenerTest.java | 178 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 backend/src/test/java/com/funeat/common/EventTest.java create mode 100644 backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java diff --git a/backend/src/test/java/com/funeat/common/EventTest.java b/backend/src/test/java/com/funeat/common/EventTest.java new file mode 100644 index 000000000..d95b2c5a8 --- /dev/null +++ b/backend/src/test/java/com/funeat/common/EventTest.java @@ -0,0 +1,64 @@ +package com.funeat.common; + +import com.funeat.member.domain.Member; +import com.funeat.member.persistence.MemberRepository; +import com.funeat.product.domain.Category; +import com.funeat.product.domain.Product; +import com.funeat.product.persistence.CategoryRepository; +import com.funeat.product.persistence.ProductRepository; +import com.funeat.review.application.ReviewService; +import com.funeat.review.persistence.ReviewRepository; +import com.funeat.tag.domain.Tag; +import com.funeat.tag.persistence.TagRepository; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; + +@SpringBootTest +@RecordApplicationEvents +@ExtendWith(MockitoExtension.class) +@DisplayNameGeneration(ReplaceUnderscores.class) +public class EventTest { + + @Autowired + protected ApplicationEvents events; + + @Autowired + protected ProductRepository productRepository; + + @Autowired + protected CategoryRepository categoryRepository; + + @Autowired + protected TagRepository tagRepository; + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected ReviewRepository reviewRepository; + + @Autowired + protected ReviewService reviewService; + + protected Long 단일_상품_저장(final Product product) { + return productRepository.save(product).getId(); + } + protected Long 단일_카테고리_저장(final Category category) { + return categoryRepository.save(category).getId(); + } + protected void 복수_태그_저장(final Tag... tagsToSave) { + final var tags = List.of(tagsToSave); + tagRepository.saveAll(tags); + } + + protected Long 단일_멤버_저장(final Member member) { + return memberRepository.save(member).getId(); + } +} diff --git a/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java new file mode 100644 index 000000000..3bfeb2856 --- /dev/null +++ b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java @@ -0,0 +1,178 @@ +package com.funeat.review.application; + +import static com.funeat.fixture.CategoryFixture.카테고리_즉석조리_생성; +import static com.funeat.fixture.ImageFixture.이미지_생성; +import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성; +import static com.funeat.fixture.MemberFixture.멤버_멤버2_생성; +import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격1000원_평점2점_생성; +import static com.funeat.fixture.ReviewFixture.리뷰추가요청_재구매O_생성; +import static com.funeat.fixture.TagFixture.태그_맛있어요_TASTE_생성; +import static com.funeat.fixture.TagFixture.태그_아침식사_ETC_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import com.funeat.common.EventTest; +import com.funeat.common.ImageUploader; +import com.funeat.tag.domain.Tag; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; + +class ReviewDeleteEventListenerTest extends EventTest { + + @MockBean + ImageUploader uploader; + + @Nested + class 리뷰_삭제_이벤트_발행_유무 { + + @Test + void 리뷰_작성자가_리뷰_삭제_시도시_리뷰_삭제_이벤트가_발행된다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + + final var request = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, memberId, image, request); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + // when + reviewService.deleteReview(reviewId, memberId); + + // then + final var count = events.stream(ReviewDeleteEvent.class).count(); + assertThat(count).isEqualTo(1); + } + + @Test + void 리뷰_작성자가_아닌_사람이_리뷰_삭제_시도시_리뷰_삭제_이벤트가_발행되지_않는다() { + // given + final var author = 멤버_멤버2_생성(); + final var authorId = 단일_멤버_저장(author); + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + + final var request = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, authorId, image, request); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + // when + try { + reviewService.deleteReview(reviewId, memberId); + } catch (Exception ignored) { + } + + // then + final var count = events.stream(ReviewDeleteEvent.class).count(); + assertThat(count).isEqualTo(0); + } + } + + @Nested + class 이미지_삭제_로직_작동_유무 { + + @Test + void 리뷰_삭제가_정상적으로_커밋되고_이미지가_존재하면_이미지_삭제_로직이_작동한다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + + final var request = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, memberId, image, request); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + // when + reviewService.deleteReview(reviewId, memberId); + + // then + verify(uploader, timeout(100).times(1)).delete(any()); + } + + @Test + void 리뷰_삭제가_정상적으로_커밋되었지만_이미지가_존재하지_않으면_이미지_삭제_로직이_작동하지않는다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var request = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, memberId, null, request); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + // when + reviewService.deleteReview(reviewId, memberId); + + // then + verify(uploader, timeout(100).times(0)).delete(any()); + } + } + + private List 태그_아이디_변환(final Tag... tags) { + return Stream.of(tags) + .map(Tag::getId) + .collect(Collectors.toList()); + } +} From d18886bfced9e6630ba3cf8fa91744f780a1ccbd Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 00:13:37 +0900 Subject: [PATCH 18/29] =?UTF-8?q?test:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewDeleteEventListenerTest.java | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java index 3bfeb2856..f188e6662 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java @@ -10,11 +10,15 @@ import static com.funeat.fixture.TagFixture.태그_아침식사_ETC_생성; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import com.funeat.common.EventTest; import com.funeat.common.ImageUploader; +import com.funeat.common.exception.CommonException; +import com.funeat.common.exception.CommonException.S3DeleteFailException; +import com.funeat.exception.CommonErrorCode; import com.funeat.tag.domain.Tag; import java.util.List; import java.util.stream.Collectors; @@ -29,7 +33,7 @@ class ReviewDeleteEventListenerTest extends EventTest { ImageUploader uploader; @Nested - class 리뷰_삭제_이벤트_발행_유무 { + class 리뷰_삭제_이벤트_발행 { @Test void 리뷰_작성자가_리뷰_삭제_시도시_리뷰_삭제_이벤트가_발행된다() { @@ -104,7 +108,7 @@ class 리뷰_삭제_이벤트_발행_유무 { } @Nested - class 이미지_삭제_로직_작동_유무 { + class 이미지_삭제_로직_작동 { @Test void 리뷰_삭제가_정상적으로_커밋되고_이미지가_존재하면_이미지_삭제_로직이_작동한다() { @@ -168,6 +172,42 @@ class 이미지_삭제_로직_작동_유무 { // then verify(uploader, timeout(100).times(0)).delete(any()); } + + @Test + void 이미지_삭제_로직이_실패해도_메인로직까지_롤백되어서는_안된다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점2점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_아침식사_ETC_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + final var image = 이미지_생성(); + + final var request = 리뷰추가요청_재구매O_생성(4L, tagIds); + reviewService.create(productId, memberId, image, request); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + doThrow(new S3DeleteFailException(CommonErrorCode.UNKNOWN_SERVER_ERROR_CODE)) + .when(uploader) + .delete(any()); + + // when + reviewService.deleteReview(reviewId, memberId); + + // then + assertThat(reviewRepository.findById(reviewId)).isEmpty(); + } } private List 태그_아이디_변환(final Tag... tags) { From ae3b56a8c18a859f898677353c67c3a908583510 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 00:22:58 +0900 Subject: [PATCH 19/29] =?UTF-8?q?refactor:=20ReviewTagRepositoryTest?= =?UTF-8?q?=EC=9D=98=20deleteByReview=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReviewTagRepositoryTest.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java index 8060f5fe3..3880fd609 100644 --- a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java +++ b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java @@ -88,25 +88,17 @@ class deleteByReview_성공_테스트 { 단일_상품_저장(product); final var tag1 = 태그_맛있어요_TASTE_생성(); - final var tag2 = 태그_단짠단짠_TASTE_생성(); - final var tag3 = 태그_갓성비_PRICE_생성(); - final var tag4 = 태그_간식_ETC_생성(); - 복수_태그_저장(tag1, tag2, tag3, tag4); + 단일_태그_저장(tag1); final var review1 = 리뷰_이미지test5_평점5점_재구매O_생성(member, product, 0L); final var review2 = 리뷰_이미지test3_평점3점_재구매X_생성(member, product, 0L); - final var review3 = 리뷰_이미지test4_평점4점_재구매O_생성(member, product, 0L); - 복수_리뷰_저장(review1, review2, review3); + 복수_리뷰_저장(review1, review2); final var reviewTag1_1 = 리뷰_태그_생성(review1, tag1); - final var reviewTag1_2 = 리뷰_태그_생성(review1, tag2); final var reviewTag2_1 = 리뷰_태그_생성(review2, tag1); - final var reviewTag2_2 = 리뷰_태그_생성(review2, tag2); - final var reviewTag2_3 = 리뷰_태그_생성(review2, tag3); - final var reviewTag3_1 = 리뷰_태그_생성(review3, tag1); - 복수_리뷰_태그_저장(reviewTag1_1, reviewTag1_2, reviewTag2_1, reviewTag2_2, reviewTag2_3, reviewTag3_1); + 복수_리뷰_태그_저장(reviewTag1_1, reviewTag2_1); - final var expected = List.of(reviewTag2_1, reviewTag2_2, reviewTag2_3, reviewTag3_1); + final var expected = List.of(reviewTag2_1); // when reviewTagRepository.deleteByReview(review1); From fa638e37482a0f2521c98cc279b14577f2d08237 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 00:36:45 +0900 Subject: [PATCH 20/29] =?UTF-8?q?feat:=20application.yml=EC=97=90=20?= =?UTF-8?q?=EC=8A=A4=EB=A0=88=EB=93=9C=20=ED=92=80=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d77ffa4b2..29a24f666 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -6,6 +6,11 @@ spring: enabled: true maxFileSize: 10MB maxRequestSize: 15MB + task: + execution: + pool: + core-size: { THREAD_CORE_SIZE } + max-size: { THREAD_MAX_SIZE } springdoc: swagger-ui: From e64cfd68e195fbc88524934e8af2d9e878408ada Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 10:14:05 +0900 Subject: [PATCH 21/29] =?UTF-8?q?refactor:=20member=EB=A5=BC=20equals?= =?UTF-8?q?=EB=A1=9C=20=EB=B9=84=EA=B5=90=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/funeat/review/domain/Review.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/funeat/review/domain/Review.java b/backend/src/main/java/com/funeat/review/domain/Review.java index f01788dd8..562148d57 100644 --- a/backend/src/main/java/com/funeat/review/domain/Review.java +++ b/backend/src/main/java/com/funeat/review/domain/Review.java @@ -89,7 +89,7 @@ public void minusFavoriteCount() { } public boolean checkAuthor(Member member) { - return this.member == member; + return Objects.equals(this.member, member); } public Long getId() { From 438b3863da74c8dfbd51544dc2583ffef7dafea7 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 16:16:39 +0900 Subject: [PATCH 22/29] =?UTF-8?q?chore:=20=EC=BB=A8=EB=B2=A4=EC=85=98=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/funeat/FuneatApplication.java | 2 +- .../src/main/java/com/funeat/common/s3/S3Uploader.java | 4 ++-- .../member/persistence/ReviewFavoriteRepository.java | 4 ++-- .../com/funeat/review/application/ReviewDeleteEvent.java | 2 +- .../review/application/ReviewDeleteEventListener.java | 4 ++-- .../java/com/funeat/review/application/ReviewService.java | 8 ++++---- .../src/main/java/com/funeat/review/domain/Review.java | 2 +- .../funeat/review/persistence/ReviewTagRepository.java | 2 +- .../review/application/ReviewDeleteEventListenerTest.java | 3 +-- 9 files changed, 15 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/com/funeat/FuneatApplication.java b/backend/src/main/java/com/funeat/FuneatApplication.java index 107e05700..ed5ccb77a 100644 --- a/backend/src/main/java/com/funeat/FuneatApplication.java +++ b/backend/src/main/java/com/funeat/FuneatApplication.java @@ -4,8 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; -@SpringBootApplication @EnableAsync +@SpringBootApplication public class FuneatApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index 421767131..607bd39cd 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -62,11 +62,11 @@ public String upload(final MultipartFile image) { @Override public void delete(final String image) { - String imageName = image.substring(BEGIN_INDEX); + final String imageName = image.substring(BEGIN_INDEX); try { final String key = folder + imageName; amazonS3.deleteObject(bucket, key); - } catch (AmazonServiceException e) { + } catch (final AmazonServiceException e) { log.error("S3 이미지 삭제가 실패했습니다. 이미지 경로 : {}", image); throw new S3DeleteFailException(UNKNOWN_SERVER_ERROR_CODE); } diff --git a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java index 7b312fb89..aa40e9566 100644 --- a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java +++ b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java @@ -11,7 +11,7 @@ public interface ReviewFavoriteRepository extends JpaRepository findByMemberAndReview(final Member member, final Review review); - void deleteByReview(Review review); + void deleteByReview(final Review review); - List findByReviewId(Long reviewId); + List findByReviewId(final Long reviewId); } diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java index 6341986d1..7c69eee3c 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java @@ -2,7 +2,7 @@ public class ReviewDeleteEvent { - private String image; + private final String image; public ReviewDeleteEvent(final String image) { this.image = image; diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java index 3ae7d8454..4274813d4 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java @@ -17,8 +17,8 @@ public ReviewDeleteEventListener(final ImageUploader imageUploader) { @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void deleteReviewImageInS3(ReviewDeleteEvent event) { - String image = event.getImage(); + public void deleteReviewImageInS3(final ReviewDeleteEvent event) { + final String image = event.getImage(); if (image != null) { imageUploader.delete(image); } 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 045a0c13d..3be317ea8 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -207,16 +207,16 @@ private void deleteThingsRelatedToReview(final Long reviewId) { } private void deleteReviewTags(final Long reviewId) { - List reviewTags = reviewTagRepository.findByReviewId(reviewId); - List ids = reviewTags.stream() + final List reviewTags = reviewTagRepository.findByReviewId(reviewId); + final List ids = reviewTags.stream() .map(ReviewTag::getId) .collect(Collectors.toList()); reviewTagRepository.deleteAllByIdInBatch(ids); } private void deleteReviewFavorites(final Long reviewId) { - List reviewFavorites = reviewFavoriteRepository.findByReviewId(reviewId); - List ids = reviewFavorites.stream() + final List reviewFavorites = reviewFavoriteRepository.findByReviewId(reviewId); + final List ids = reviewFavorites.stream() .map(ReviewFavorite::getId) .collect(Collectors.toList()); reviewFavoriteRepository.deleteAllByIdInBatch(ids); diff --git a/backend/src/main/java/com/funeat/review/domain/Review.java b/backend/src/main/java/com/funeat/review/domain/Review.java index 562148d57..d990666d3 100644 --- a/backend/src/main/java/com/funeat/review/domain/Review.java +++ b/backend/src/main/java/com/funeat/review/domain/Review.java @@ -88,7 +88,7 @@ public void minusFavoriteCount() { this.favoriteCount--; } - public boolean checkAuthor(Member member) { + public boolean checkAuthor(final Member member) { return Objects.equals(this.member, member); } diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java index c813a649f..3f885dbf4 100644 --- a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java @@ -20,5 +20,5 @@ public interface ReviewTagRepository extends JpaRepository { void deleteByReview(final Review review); - List findByReviewId(Long reviewId); + List findByReviewId(final Long reviewId); } diff --git a/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java index f188e6662..5edf33f36 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java @@ -16,7 +16,6 @@ import com.funeat.common.EventTest; import com.funeat.common.ImageUploader; -import com.funeat.common.exception.CommonException; import com.funeat.common.exception.CommonException.S3DeleteFailException; import com.funeat.exception.CommonErrorCode; import com.funeat.tag.domain.Tag; @@ -30,7 +29,7 @@ class ReviewDeleteEventListenerTest extends EventTest { @MockBean - ImageUploader uploader; + private ImageUploader uploader; @Nested class 리뷰_삭제_이벤트_발행 { From e846e9168c9a549c1f2dc9003e670c7444239ff7 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 16:26:33 +0900 Subject: [PATCH 23/29] =?UTF-8?q?refactor:=20=EC=84=B8=EC=85=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/resources/application.yml | 4 ---- .../test/java/com/funeat/acceptance/auth/LoginSteps.java | 4 ++-- .../java/com/funeat/acceptance/member/MemberSteps.java | 8 ++++---- .../java/com/funeat/acceptance/recipe/RecipeSteps.java | 6 +++--- .../java/com/funeat/acceptance/review/ReviewSteps.java | 8 ++++---- backend/src/test/resources/application.yml | 6 ------ 6 files changed, 13 insertions(+), 23 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 29a24f666..02d41833c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -28,7 +28,3 @@ server: max: { MAX_THREADS } max-connections: { MAX_CONNECTIONS } accept-count: { ACCEPT_COUNT } - servlet: - session: - cookie: - name: FUNEAT diff --git a/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java b/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java index dd75b30ab..21dd1bf77 100644 --- a/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java @@ -29,7 +29,7 @@ public class LoginSteps { public static ExtractableResponse 로그아웃_요청(final String loginCookie) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .when() .post("/api/logout") .then() @@ -44,6 +44,6 @@ public class LoginSteps { .then() .extract() .response() - .getCookie("FUNEAT"); + .getCookie("JSESSIONID"); } } diff --git a/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java b/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java index 32ad97500..ca5600fdc 100644 --- a/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java @@ -13,7 +13,7 @@ public class MemberSteps { public static ExtractableResponse 사용자_정보_조회_요청(final String loginCookie) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .when() .get("/api/members") .then() @@ -24,7 +24,7 @@ public class MemberSteps { final MultiPartSpecification image, final MemberRequest request) { final var requestSpec = given() - .cookie("FUNEAT", loginCookie); + .cookie("JSESSIONID", loginCookie); if (Objects.nonNull(image)) { requestSpec.multiPart(image); @@ -43,7 +43,7 @@ public class MemberSteps { final Long page) { return given() .when() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .queryParam("sort", sort) .queryParam("page", page) .get("/api/members/reviews") @@ -55,7 +55,7 @@ public class MemberSteps { final Long page) { return given() .when() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .queryParam("sort", sort) .queryParam("page", page) .get("/api/members/recipes") diff --git a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java index f82c84f93..4ff18dc2e 100644 --- a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java @@ -19,7 +19,7 @@ public class RecipeSteps { final List images, final RecipeCreateRequest recipeRequest) { final var requestSpec = given() - .cookie("FUNEAT", loginCookie); + .cookie("JSESSIONID", loginCookie); if (Objects.nonNull(images) && !images.isEmpty()) { images.forEach(requestSpec::multiPart); @@ -35,7 +35,7 @@ public class RecipeSteps { public static ExtractableResponse 레시피_상세_정보_요청(final String loginCookie, final Long recipeId) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .when() .get("/api/recipes/{recipeId}", recipeId) .then() @@ -55,7 +55,7 @@ public class RecipeSteps { public static ExtractableResponse 레시피_좋아요_요청(final String loginCookie, final Long recipeId, final RecipeFavoriteRequest request) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .contentType("application/json") .body(request) .when() diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java index 7cd44e61a..f41192bfa 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java @@ -20,7 +20,7 @@ public class ReviewSteps { final MultiPartSpecification image, final ReviewCreateRequest request) { final var requestSpec = given() - .cookie("FUNEAT", loginCookie); + .cookie("JSESSIONID", loginCookie); if (Objects.nonNull(image)) { requestSpec.multiPart(image); @@ -37,7 +37,7 @@ public class ReviewSteps { public static ExtractableResponse 리뷰_좋아요_요청(final String loginCookie, final Long productId, final Long reviewId, final ReviewFavoriteRequest request) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .contentType("application/json") .body(request) .when() @@ -58,7 +58,7 @@ public class ReviewSteps { public static ExtractableResponse 정렬된_리뷰_목록_조회_요청(final String loginCookie, final Long productId, final String sort, final Long page) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .queryParam("sort", sort) .queryParam("page", page) .when() @@ -78,7 +78,7 @@ public class ReviewSteps { public static ExtractableResponse 리뷰_삭제_요청(final String loginCookie, final Long productId, final Long reviewId) { return given() - .cookie("FUNEAT", loginCookie) + .cookie("JSESSIONID", loginCookie) .when() .delete("/api/products/{productId}/reviews/{reviewId}", productId, reviewId) .then() diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index b4dda6f4f..ffc258d0b 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -19,12 +19,6 @@ logging: level: org.hibernate.type.descriptor.sql: trace -server: - servlet: - session: - cookie: - name: FUNEAT - cloud: aws: region: From 32acf16b56f04995b8786e205eccaba2f43f408b Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 16:38:54 +0900 Subject: [PATCH 24/29] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/funeat/common/s3/S3Uploader.java | 4 ++-- .../funeat/review/application/ReviewDeleteEventListener.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index 607bd39cd..f79dd1243 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -25,7 +25,7 @@ @Profile("!test") public class S3Uploader implements ImageUploader { - private static final int BEGIN_INDEX = 31; + private static final int BEGIN_FILE_NAME_INDEX_WITHOUT_CLOUDFRONT_PATH = 31; private static final List INCLUDE_EXTENSIONS = List.of("image/jpeg", "image/png", "image/webp"); private final Logger log = LoggerFactory.getLogger(this.getClass()); @@ -62,7 +62,7 @@ public String upload(final MultipartFile image) { @Override public void delete(final String image) { - final String imageName = image.substring(BEGIN_INDEX); + final String imageName = image.substring(BEGIN_FILE_NAME_INDEX_WITHOUT_CLOUDFRONT_PATH); try { final String key = folder + imageName; amazonS3.deleteObject(bucket, key); diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java index 4274813d4..2009e3936 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java @@ -1,6 +1,7 @@ package com.funeat.review.application; import com.funeat.common.ImageUploader; +import io.micrometer.core.instrument.util.StringUtils; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; @@ -19,7 +20,7 @@ public ReviewDeleteEventListener(final ImageUploader imageUploader) { @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void deleteReviewImageInS3(final ReviewDeleteEvent event) { final String image = event.getImage(); - if (image != null) { + if (StringUtils.isBlank(image)) { imageUploader.delete(image); } } From e0560df7b462ee5cb92063fd01c0af8360494c91 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 17:01:05 +0900 Subject: [PATCH 25/29] =?UTF-8?q?refactor:=20reviewId=20=EB=8C=80=EC=8B=A0?= =?UTF-8?q?=20review=EB=A1=9C=20delete=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/ReviewFavoriteRepository.java | 2 +- .../review/application/ReviewService.java | 18 +++++----- .../persistence/ReviewTagRepository.java | 2 +- .../ReviewFavoriteRepositoryTest.java | 33 +++++++++++++++++ .../persistence/ReviewTagRepositoryTest.java | 36 +++++++++++++++++++ 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java index aa40e9566..f1ae40e5d 100644 --- a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java +++ b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java @@ -13,5 +13,5 @@ public interface ReviewFavoriteRepository extends JpaRepository findByReviewId(final Long reviewId); + List findByReview(final Review review); } 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 3be317ea8..d72b7aee7 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -193,29 +193,29 @@ public void deleteReview(final Long reviewId, final Long memberId) { if (review.checkAuthor(member)) { eventPublisher.publishEvent(new ReviewDeleteEvent(image)); - deleteThingsRelatedToReview(reviewId); + deleteThingsRelatedToReview(review); updateProductImage(product.getId()); return; } throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); } - private void deleteThingsRelatedToReview(final Long reviewId) { - deleteReviewTags(reviewId); - deleteReviewFavorites(reviewId); - reviewRepository.deleteById(reviewId); + private void deleteThingsRelatedToReview(final Review review) { + deleteReviewTags(review); + deleteReviewFavorites(review); + reviewRepository.deleteById(review.getId()); } - private void deleteReviewTags(final Long reviewId) { - final List reviewTags = reviewTagRepository.findByReviewId(reviewId); + private void deleteReviewTags(final Review review) { + final List reviewTags = reviewTagRepository.findByReview(review); final List ids = reviewTags.stream() .map(ReviewTag::getId) .collect(Collectors.toList()); reviewTagRepository.deleteAllByIdInBatch(ids); } - private void deleteReviewFavorites(final Long reviewId) { - final List reviewFavorites = reviewFavoriteRepository.findByReviewId(reviewId); + private void deleteReviewFavorites(final Review review) { + final List reviewFavorites = reviewFavoriteRepository.findByReview(review); final List ids = reviewFavorites.stream() .map(ReviewFavorite::getId) .collect(Collectors.toList()); diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java index 3f885dbf4..cbdf3c3bf 100644 --- a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java @@ -20,5 +20,5 @@ public interface ReviewTagRepository extends JpaRepository { void deleteByReview(final Review review); - List findByReviewId(final Long reviewId); + List findByReview(final Review review); } diff --git a/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java b/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java index 533e38c8b..fcb190d28 100644 --- a/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java +++ b/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java @@ -148,4 +148,37 @@ class deleteByReview_성공_테스트 { .isEqualTo(expected); } } + + @Nested + class findByReview_성공_테스트 { + + @Test + void 리뷰로_해당_리뷰에_달린_좋아요를_조회할_수_있다() { + // given + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + 단일_상품_저장(product); + + final var review = 리뷰_이미지test4_평점4점_재구매O_생성(member, product, 0L); + 단일_리뷰_저장(review); + + final var reviewFavorite = ReviewFavorite.create(member, review, true); + 단일_리뷰_좋아요_저장(reviewFavorite); + + final var expected = List.of(reviewFavorite); + + // when + final var actual = reviewFavoriteRepository.findByReview(review); + + // then + assertThat(actual).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + } } diff --git a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java index 3880fd609..4f5bbf152 100644 --- a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java +++ b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java @@ -110,6 +110,42 @@ class deleteByReview_성공_테스트 { } } + @Nested + class findByReview_성공_테스트 { + + @Test + void 해당_리뷰에_달린_태그를_확인할_수_있다() { + // given + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(category); + + final var product = 상품_삼각김밥_가격3000원_평점2점_생성(category); + 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + 단일_태그_저장(tag1); + + final var review = 리뷰_이미지test5_평점5점_재구매O_생성(member, product, 0L); + 단일_리뷰_저장(review); + + final var reviewTag = 리뷰_태그_생성(review, tag1); + 단일_리뷰_태그_저장(reviewTag); + + final var expected = List.of(reviewTag); + + // when + final var actual = reviewTagRepository.findByReview(review); + + // then + assertThat(actual).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + } + private ReviewTag 리뷰_태그_생성(final Review review, final Tag tag) { return ReviewTag.createReviewTag(review, tag); } From f5ace44b9f4cf06344b9d707e009750ca21a0fdc Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 17:03:54 +0900 Subject: [PATCH 26/29] =?UTF-8?q?refactor:=20s3=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=82=AD=EC=A0=9C=20=EC=8B=A4=ED=8C=A8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=AC=B8=EA=B5=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main/java/com/funeat/common/s3/S3Uploader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index f79dd1243..97e6241b7 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -67,7 +67,7 @@ public void delete(final String image) { final String key = folder + imageName; amazonS3.deleteObject(bucket, key); } catch (final AmazonServiceException e) { - log.error("S3 이미지 삭제가 실패했습니다. 이미지 경로 : {}", image); + log.error("S3 이미지 삭제에 실패했습니다. 이미지 경로 : {}", image); throw new S3DeleteFailException(UNKNOWN_SERVER_ERROR_CODE); } } From a30cba3ef6f9d5d334fe02928495c4a73c4ff9b7 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Fri, 13 Oct 2023 18:07:37 +0900 Subject: [PATCH 27/29] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=EC=8B=9C=20deleteById=20=EB=8C=80=EC=8B=A0?= =?UTF-8?q?=20delete=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/funeat/review/application/ReviewService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d72b7aee7..b335bc7b6 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -203,7 +203,7 @@ public void deleteReview(final Long reviewId, final Long memberId) { private void deleteThingsRelatedToReview(final Review review) { deleteReviewTags(review); deleteReviewFavorites(review); - reviewRepository.deleteById(review.getId()); + reviewRepository.delete(review); } private void deleteReviewTags(final Review review) { From 0fdb1efd91f4143c88a5f42a60dc0a54389d4ab2 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Sat, 14 Oct 2023 01:39:55 +0900 Subject: [PATCH 28/29] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20api=20=EC=88=98=EC=A0=95=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/MemberApiController.java | 11 +++ .../member/presentation/MemberController.java | 11 +++ .../presentation/ReviewApiController.java | 9 -- .../review/presentation/ReviewController.java | 9 -- .../member/MemberAcceptanceTest.java | 83 +++++++++++++++++++ .../funeat/acceptance/member/MemberSteps.java | 9 ++ .../review/ReviewAcceptanceTest.java | 76 ----------------- .../funeat/acceptance/review/ReviewSteps.java | 10 --- 8 files changed, 114 insertions(+), 104 deletions(-) diff --git a/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java b/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java index 6e28cfac6..d5f18fb2f 100644 --- a/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java +++ b/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java @@ -15,7 +15,9 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -69,4 +71,13 @@ public ResponseEntity getMemberRecipe(@AuthenticationPrin return ResponseEntity.ok().body(response); } + + @Logging + @DeleteMapping("/reviews/{reviewId}") + public ResponseEntity deleteReview(@PathVariable final Long reviewId, + @AuthenticationPrincipal final LoginInfo loginInfo) { + reviewService.deleteReview(reviewId, loginInfo.getId()); + + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/funeat/member/presentation/MemberController.java b/backend/src/main/java/com/funeat/member/presentation/MemberController.java index 9a4ede8da..2cc920d7c 100644 --- a/backend/src/main/java/com/funeat/member/presentation/MemberController.java +++ b/backend/src/main/java/com/funeat/member/presentation/MemberController.java @@ -12,7 +12,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @@ -55,4 +57,13 @@ ResponseEntity getMemberReview(@AuthenticationPrincipal f @GetMapping ResponseEntity getMemberRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, @PageableDefault final Pageable pageable); + + @Operation(summary = "리뷰 삭제", description = "자신이 작성한 리뷰를 삭제한다.") + @ApiResponse( + responseCode = "204", + description = "리뷰 삭제 성공." + ) + @DeleteMapping + ResponseEntity deleteReview(@PathVariable final Long reviewId, + @AuthenticationPrincipal final LoginInfo loginInfo); } diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index 83594bd76..e4af15b45 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -57,15 +57,6 @@ public ResponseEntity toggleLikeReview(@PathVariable final Long productId, return ResponseEntity.noContent().build(); } - @Logging - @DeleteMapping("/api/products/{productId}/reviews/{reviewId}") - public ResponseEntity deleteReview(@PathVariable final Long reviewId, - @AuthenticationPrincipal final LoginInfo loginInfo) { - reviewService.deleteReview(reviewId, loginInfo.getId()); - - return ResponseEntity.noContent().build(); - } - @GetMapping("/api/products/{productId}/reviews") public ResponseEntity getSortingReviews(@AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long productId, diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java index e06c2dbb2..ae50731cd 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java @@ -46,15 +46,6 @@ ResponseEntity toggleLikeReview(@PathVariable final Long productId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestBody final ReviewFavoriteRequest request); - @Operation(summary = "리뷰 삭제", description = "자신이 작성한 리뷰를 삭제한다.") - @ApiResponse( - responseCode = "204", - description = "리뷰 삭제 성공." - ) - @DeleteMapping("/api/products/{productId}/reviews/{reviewId}") - ResponseEntity deleteReview(@PathVariable final Long reviewId, - @AuthenticationPrincipal final LoginInfo loginInfo); - @Operation(summary = "리뷰를 정렬후 조회", description = "리뷰를 정렬후 조회한다.") @ApiResponse( responseCode = "200", diff --git a/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java index 9af331623..8b44b7989 100644 --- a/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java @@ -7,7 +7,10 @@ import static com.funeat.acceptance.common.CommonSteps.인증되지_않음; import static com.funeat.acceptance.common.CommonSteps.잘못된_요청; 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.common.CommonSteps.페이지를_검증한다; +import static com.funeat.acceptance.member.MemberSteps.리뷰_삭제_요청; import static com.funeat.acceptance.member.MemberSteps.사용자_꿀조합_조회_요청; import static com.funeat.acceptance.member.MemberSteps.사용자_리뷰_조회_요청; import static com.funeat.acceptance.member.MemberSteps.사용자_정보_수정_요청; @@ -32,17 +35,22 @@ import static com.funeat.fixture.PageFixture.총_데이터_개수; import static com.funeat.fixture.PageFixture.총_페이지; import static com.funeat.fixture.PageFixture.최신순; +import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격1000원_평점3점_생성; import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격1000원_평점5점_생성; import static com.funeat.fixture.RecipeFixture.레시피; import static com.funeat.fixture.RecipeFixture.레시피1; import static com.funeat.fixture.RecipeFixture.레시피2; import static com.funeat.fixture.RecipeFixture.레시피추가요청_생성; +import static com.funeat.fixture.ReviewFixture.리뷰1; +import static com.funeat.fixture.ReviewFixture.리뷰2; import static com.funeat.fixture.ReviewFixture.리뷰추가요청_재구매O_생성; import static com.funeat.fixture.ReviewFixture.리뷰추가요청_재구매X_생성; import static com.funeat.fixture.ScoreFixture.점수_1점; import static com.funeat.fixture.ScoreFixture.점수_2점; import static com.funeat.fixture.ScoreFixture.점수_3점; +import static com.funeat.fixture.ScoreFixture.점수_4점; import static com.funeat.fixture.TagFixture.태그_맛있어요_TASTE_생성; +import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -299,6 +307,81 @@ class getMemberRecipes_실패_테스트 { } } + @Nested + class deleteReview_성공_테스트 { + + @Test + void 자신이_작성한_리뷰를_삭제할_수_있다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 리뷰1); + + // then + STATUS_CODE를_검증한다(응답, 정상_처리_NO_CONTENT); + } + } + + @Nested + class deleteReview_실패_테스트 { + + @ParameterizedTest + @NullAndEmptySource + void 로그인하지_않는_사용자가_리뷰_삭제시_예외가_발생한다(final String cookie) { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(cookie, 리뷰1); + + // then + STATUS_CODE를_검증한다(응답, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(), LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void 존재하지_않는_리뷰를_삭제할_수_없다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 리뷰2); + + // then + STATUS_CODE를_검증한다(응답, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(응답, REVIEW_NOT_FOUND.getCode(), REVIEW_NOT_FOUND.getMessage()); + } + + @Test + void 자신이_작성하지_않은_리뷰는_삭제할_수_없다() { + // given + final var 카테고리 = 카테고리_즉석조리_생성(); + 단일_카테고리_저장(카테고리); + final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); + final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); + 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); + + // when + final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버2), 리뷰1); + + // then + STATUS_CODE를_검증한다(응답, 잘못된_요청); + } + } + private void 사용자_리뷰_조회_결과를_검증한다(final ExtractableResponse response, final int expectedReviewSize) { final var actual = response.jsonPath().getList("reviews", MemberReviewDto.class); diff --git a/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java b/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java index ca5600fdc..681efb26a 100644 --- a/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java @@ -62,4 +62,13 @@ public class MemberSteps { .then() .extract(); } + + public static ExtractableResponse 리뷰_삭제_요청(final String loginCookie, final Long reviewId) { + return given() + .cookie("JSESSIONID", loginCookie) + .when() + .delete("/api/members/reviews/{reviewId}", reviewId) + .then() + .extract(); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index fbb37beaa..07552c34c 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -12,7 +12,6 @@ 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.리뷰_작성_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_좋아요_요청; import static com.funeat.acceptance.review.ReviewSteps.여러명이_리뷰_좋아요_요청; @@ -612,81 +611,6 @@ class getRankingReviews_성공_테스트 { } } - @Nested - class deleteReview_성공_테스트 { - - @Test - void 자신이_작성한_리뷰를_삭제할_수_있다() { - // given - final var 카테고리 = 카테고리_즉석조리_생성(); - 단일_카테고리_저장(카테고리); - final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); - final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); - 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); - - // when - final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 상품, 리뷰1); - - // then - STATUS_CODE를_검증한다(응답, 정상_처리_NO_CONTENT); - } - } - - @Nested - class deleteReview_실패_테스트 { - - @ParameterizedTest - @NullAndEmptySource - void 로그인하지_않는_사용자가_리뷰_삭제시_예외가_발생한다(final String cookie) { - // given - final var 카테고리 = 카테고리_즉석조리_생성(); - 단일_카테고리_저장(카테고리); - final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); - final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); - 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); - - // when - final var 응답 = 리뷰_삭제_요청(cookie, 상품, 리뷰1); - - // then - STATUS_CODE를_검증한다(응답, 인증되지_않음); - RESPONSE_CODE와_MESSAGE를_검증한다(응답, LOGIN_MEMBER_NOT_FOUND.getCode(), LOGIN_MEMBER_NOT_FOUND.getMessage()); - } - - @Test - void 존재하지_않는_리뷰를_삭제할_수_없다() { - // given - final var 카테고리 = 카테고리_즉석조리_생성(); - 단일_카테고리_저장(카테고리); - final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); - final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); - 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); - - // when - final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버1), 상품, 리뷰2); - - // then - STATUS_CODE를_검증한다(응답, 찾을수_없음); - RESPONSE_CODE와_MESSAGE를_검증한다(응답, REVIEW_NOT_FOUND.getCode(), REVIEW_NOT_FOUND.getMessage()); - } - - @Test - void 자신이_작성하지_않은_리뷰는_삭제할_수_없다() { - // given - final var 카테고리 = 카테고리_즉석조리_생성(); - 단일_카테고리_저장(카테고리); - final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리)); - final var 태그 = 단일_태그_저장(태그_맛있어요_TASTE_생성()); - 리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매O_생성(점수_4점, List.of(태그))); - - // when - final var 응답 = 리뷰_삭제_요청(로그인_쿠키_획득(멤버2), 상품, 리뷰1); - - // then - STATUS_CODE를_검증한다(응답, 잘못된_요청); - } - } - private void RESPONSE_CODE와_MESSAGE를_검증한다(final ExtractableResponse response, final String expectedCode, final String expectedMessage) { assertSoftly(soft -> { diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java index f41192bfa..e9e4f45e0 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java @@ -74,14 +74,4 @@ public class ReviewSteps { .then() .extract(); } - - public static ExtractableResponse 리뷰_삭제_요청(final String loginCookie, - final Long productId, final Long reviewId) { - return given() - .cookie("JSESSIONID", loginCookie) - .when() - .delete("/api/products/{productId}/reviews/{reviewId}", productId, reviewId) - .then() - .extract(); - } } From aea8f017e31129dd3f2b2775fbe4270af880ab07 Mon Sep 17 00:00:00 2001 From: hanueleee Date: Sun, 15 Oct 2023 16:41:57 +0900 Subject: [PATCH 29/29] =?UTF-8?q?style:=20EventTest=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=A4=84=EB=B0=94=EA=BF=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/test/java/com/funeat/common/EventTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/test/java/com/funeat/common/EventTest.java b/backend/src/test/java/com/funeat/common/EventTest.java index d95b2c5a8..dec401bec 100644 --- a/backend/src/test/java/com/funeat/common/EventTest.java +++ b/backend/src/test/java/com/funeat/common/EventTest.java @@ -50,9 +50,11 @@ public class EventTest { protected Long 단일_상품_저장(final Product product) { return productRepository.save(product).getId(); } + protected Long 단일_카테고리_저장(final Category category) { return categoryRepository.save(category).getId(); } + protected void 복수_태그_저장(final Tag... tagsToSave) { final var tags = List.of(tagsToSave); tagRepository.saveAll(tags);