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 all 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
3 changes: 2 additions & 1 deletion backend/src/main/java/com/funeat/FuneatApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import com.funeat.common.repository.BaseRepositoryImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableAsync
@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = BaseRepositoryImpl.class)
public class FuneatApplication {

public static void main(String[] args) {
SpringApplication.run(FuneatApplication.class, args);
}

}
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()));
}
}
}
19 changes: 19 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,15 +3,19 @@
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;
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;
Expand All @@ -21,8 +25,11 @@
@Profile("!test")
public class S3Uploader implements ImageUploader {

private static final int BEGIN_FILE_NAME_INDEX_WITHOUT_CLOUDFRONT_PATH = 31;
private static final List<String> 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;

Expand Down Expand Up @@ -53,6 +60,18 @@ public String upload(final MultipartFile image) {
}
}

@Override
public void delete(final String image) {
final String imageName = image.substring(BEGIN_FILE_NAME_INDEX_WITHOUT_CLOUDFRONT_PATH);
try {
final String key = folder + imageName;
amazonS3.deleteObject(bucket, key);
} catch (final AmazonServiceException e) {
log.error("S3 이미지 삭제에 실패했습니다. 이미지 경로 : {}", image);
hanueleee marked this conversation as resolved.
Show resolved Hide resolved
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(final Review review);

List<ReviewFavorite> findByReview(final Review review);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,4 +71,13 @@ public ResponseEntity<MemberRecipesResponse> getMemberRecipe(@AuthenticationPrin

return ResponseEntity.ok().body(response);
}

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

return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,4 +57,13 @@ ResponseEntity<MemberReviewsResponse> getMemberReview(@AuthenticationPrincipal f
@GetMapping
ResponseEntity<MemberRecipesResponse> getMemberRecipe(@AuthenticationPrincipal final LoginInfo loginInfo,
@PageableDefault final Pageable pageable);

@Operation(summary = "리뷰 삭제", description = "자신이 작성한 리뷰를 삭제한다.")
@ApiResponse(
responseCode = "204",
description = "리뷰 삭제 성공."
)
@DeleteMapping
ResponseEntity<Void> deleteReview(@PathVariable final Long reviewId,
@AuthenticationPrincipal final LoginInfo loginInfo);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.funeat.review.application;

public class ReviewDeleteEvent {

private final String image;

public ReviewDeleteEvent(final String image) {
this.image = image;
}

public String getImage() {
return image;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
public class ReviewDeleteEventListener {

private final ImageUploader imageUploader;

public ReviewDeleteEventListener(final ImageUploader imageUploader) {
this.imageUploader = imageUploader;
}

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void deleteReviewImageInS3(final ReviewDeleteEvent event) {
final String image = event.getImage();
if (StringUtils.isBlank(image)) {
imageUploader.delete(image);
}
}
}
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 @@ -27,6 +28,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 All @@ -35,6 +37,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;
Expand All @@ -58,18 +61,21 @@ 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;
this.memberRepository = memberRepository;
this.productRepository = productRepository;
this.reviewFavoriteRepository = reviewFavoriteRepository;
this.imageUploader = imageUploader;
this.eventPublisher = eventPublisher;
}

@Transactional
Expand Down Expand Up @@ -124,14 +130,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 @@ -180,6 +183,46 @@ 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)) {
eventPublisher.publishEvent(new ReviewDeleteEvent(image));
deleteThingsRelatedToReview(review);
updateProductImage(product.getId());
return;
}
throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId);
}

private void deleteThingsRelatedToReview(final Review review) {
deleteReviewTags(review);
deleteReviewFavorites(review);
reviewRepository.delete(review);
}

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

private void deleteReviewFavorites(final Review review) {
final List<ReviewFavorite> reviewFavorites = reviewFavoriteRepository.findByReview(review);
final List<Long> ids = reviewFavorites.stream()
.map(ReviewFavorite::getId)
.collect(Collectors.toList());
reviewFavoriteRepository.deleteAllByIdInBatch(ids);
}

public Optional<MostFavoriteReviewResponse> getMostFavoriteReview(final Long productId) {
final Product findProduct = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId));
Expand Down
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(final Member member) {
return Objects.equals(this.member, member);
}

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> findByReview(final Review review);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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 @@ -49,11 +50,12 @@ 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,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 @@ -42,7 +43,8 @@ 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);

Expand Down
Loading