Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 리뷰 삭제 기능 구현 #735

Merged
merged 30 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
99a55ef
feat: 해당 review에 달린 tag를 삭제하는 기능 추가
hanueleee Oct 9, 2023
714167b
feat: 해당 review에 달린 favorite을 삭제하는 기능 추가
hanueleee Oct 9, 2023
e6103d3
feat: NotAuthorOfReviewException 추가
hanueleee Oct 9, 2023
fd8c80d
feat: 리뷰 삭제 기능 구현
hanueleee Oct 9, 2023
6ddcdeb
feat: s3 이미지 삭제 기능 구현
hanueleee Oct 9, 2023
f6f2acd
test: 리뷰 삭제 기능에 대한 인수테스트 작성
hanueleee Oct 9, 2023
bde3448
refactor: 리뷰 반영
hanueleee Oct 10, 2023
6fa2d9b
refactor: deleteAllByIdInBatch적용
hanueleee Oct 10, 2023
6d54089
test: 리뷰 삭제 실패 케이스 추가
hanueleee Oct 10, 2023
7534544
refactor: updateProductImage 메서드 중복 제거
hanueleee Oct 10, 2023
6e44dcb
feat: s3 파일 경로 지정 로직 추가
hanueleee Oct 11, 2023
f457738
refactor: 리뷰에 이미지가 존재할 때에만 s3 delete 로직 실행하도록 수정
hanueleee Oct 11, 2023
4842491
refactor: 리뷰 삭제 성공시 상태코드 204 반환
hanueleee Oct 11, 2023
c0e02a0
test: 리뷰 삭제 성공시 상태코드 204 반환하도록 인수테스트 수정
hanueleee Oct 11, 2023
20470ee
feat: s3 이미지 삭제 로직 이벤트 처리
hanueleee Oct 11, 2023
acb87e5
refactor: 이미지 있을 때만 이벤트 발행하던 로직을 이미지 유무 상관없이 이벤트 발행하도록 수정 (이미지 유무 처리를…
hanueleee Oct 12, 2023
377e1bb
test: 리뷰 삭제 이벤트 관련 테스트 추가
hanueleee Oct 12, 2023
d18886b
test: 리뷰 삭제 이벤트 관련 테스트 보완
hanueleee Oct 12, 2023
ae3b56a
refactor: ReviewTagRepositoryTest의 deleteByReview 테스트 간소화
hanueleee Oct 12, 2023
fa638e3
feat: application.yml에 스레드 풀 설정 추가
hanueleee Oct 12, 2023
e64cfd6
refactor: member를 equals로 비교하도록 수정
hanueleee Oct 13, 2023
438b386
chore: 컨벤션 적용
hanueleee Oct 13, 2023
e846e91
refactor: 세션 이름 복구
hanueleee Oct 13, 2023
32acf16
refactor: 리뷰 반영
hanueleee Oct 13, 2023
e0560df
refactor: reviewId 대신 review로 delete하도록 수정
hanueleee Oct 13, 2023
f5ace44
refactor: s3 이미지 삭제 실패 로그 문구 수정
hanueleee Oct 13, 2023
a30cba3
refactor: 리뷰 삭제시 deleteById 대신 delete로 수정
hanueleee Oct 13, 2023
0fdb1ef
feat: 리뷰 삭제 api 수정 사항 적용
hanueleee Oct 13, 2023
aea8f01
style: EventTest 메소드 줄바꿈
hanueleee Oct 15, 2023
05b0030
Merge branch 'develop' into feat/issue-734
hanueleee Oct 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/main/java/com/funeat/common/ImageUploader.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
public interface ImageUploader {

String upload(final MultipartFile image);

void delete(final String fileName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
}
13 changes: 13 additions & 0 deletions backend/src/main/java/com/funeat/common/s3/S3Uploader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
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;

public interface ReviewFavoriteRepository extends JpaRepository<ReviewFavorite, Long> {

Optional<ReviewFavorite> findByMemberAndReview(final Member member, final Review review);

void deleteByReview(Review review);

List<ReviewFavorite> findByReviewId(Long reviewId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -123,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<Review> topFavoriteReview = reviewRepository.findPopularReviewWithImage(productId, pageRequest);
if (!topFavoriteReview.isEmpty()) {
final String topFavoriteReviewImage = topFavoriteReview.get(TOP).getImage();
Expand Down Expand Up @@ -178,4 +177,44 @@ 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();
final String image = review.getImage();

if (review.checkAuthor(member)) {
deleteThingsRelatedToReview(reviewId);
updateProductImage(product.getId());
imageUploader.delete(image);
return;
}
throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId);
}

private void deleteThingsRelatedToReview(final Long reviewId) {
deleteReviewTags(reviewId);
deleteReviewFavorites(reviewId);
reviewRepository.deleteById(reviewId);
}

private void deleteReviewTags(final Long reviewId) {
List<ReviewTag> reviewTags = reviewTagRepository.findByReviewId(reviewId);
List<Long> ids = reviewTags.stream()
.map(ReviewTag::getId)
.collect(Collectors.toList());
reviewTagRepository.deleteAllByIdInBatch(ids);
}

private void deleteReviewFavorites(final Long reviewId) {
List<ReviewFavorite> reviewFavorites = reviewFavoriteRepository.findByReviewId(reviewId);
List<Long> ids = reviewFavorites.stream()
.map(ReviewFavorite::getId)
.collect(Collectors.toList());
reviewFavoriteRepository.deleteAllByIdInBatch(ids);
}
}
4 changes: 4 additions & 0 deletions backend/src/main/java/com/funeat/review/domain/Review.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public void minusFavoriteCount() {
this.favoriteCount--;
}

public boolean checkAuthor(Member member) {
return this.member == member;
}

hanueleee marked this conversation as resolved.
Show resolved Hide resolved
public Long getId() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,4 +17,8 @@ public interface ReviewTagRepository extends JpaRepository<ReviewTag, Long> {
+ "GROUP BY rt.tag "
+ "ORDER BY cnt DESC")
List<Tag> findTop3TagsByReviewIn(final Long productId, final Pageable pageable);

void deleteByReview(final Review review);

List<ReviewTag> findByReviewId(Long reviewId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,15 +47,25 @@ public ResponseEntity<Void> writeReview(@PathVariable final Long productId,

@Logging
@PatchMapping("/api/products/{productId}/reviews/{reviewId}")
public ResponseEntity<Void> toggleLikeReview(@PathVariable final Long reviewId,
public ResponseEntity<Void> 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();
}

@Logging
@DeleteMapping("/api/products/{productId}/reviews/{reviewId}")
public ResponseEntity<Void> deleteReview(@PathVariable final Long reviewId,
@AuthenticationPrincipal final LoginInfo loginInfo) {
reviewService.deleteReview(reviewId, loginInfo.getId());

return ResponseEntity.ok().build();
hanueleee marked this conversation as resolved.
Show resolved Hide resolved
}

@GetMapping("/api/products/{productId}/reviews")
public ResponseEntity<SortingReviewsResponse> getSortingReviews(@AuthenticationPrincipal final LoginInfo loginInfo,
@PathVariable final Long productId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,10 +41,20 @@ ResponseEntity<Void> writeReview(@PathVariable final Long productId,
description = "리뷰 좋아요(취소) 성공."
)
@PatchMapping
ResponseEntity<Void> toggleLikeReview(@PathVariable final Long reviewId,
ResponseEntity<Void> toggleLikeReview(@PathVariable final Long productId,
@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<Void> deleteReview(@PathVariable final Long reviewId,
@AuthenticationPrincipal final LoginInfo loginInfo);

@Operation(summary = "리뷰를 정렬후 조회", description = "리뷰를 정렬후 조회한다.")
@ApiResponse(
responseCode = "200",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.여러명이_리뷰_좋아요_요청;
Expand Down Expand Up @@ -611,6 +612,81 @@ 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_실패_테스트 {
hanueleee marked this conversation as resolved.
Show resolved Hide resolved

@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> response, final String expectedCode,
final String expectedMessage) {
assertSoftly(soft -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,14 @@ public class ReviewSteps {
.then()
.extract();
}

public static ExtractableResponse<Response> 리뷰_삭제_요청(final String loginCookie,
final Long productId, final Long reviewId) {
return given()
.cookie("FUNEAT", loginCookie)
hanueleee marked this conversation as resolved.
Show resolved Hide resolved
.when()
.delete("/api/products/{productId}/reviews/{reviewId}", productId, reviewId)
.then()
.extract();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Path> pathStream = Files.walk(directory)) {
Expand Down
Loading