diff --git a/src/main/java/com/shwimping/be/global/application/NCPStorageService.java b/src/main/java/com/shwimping/be/global/application/NCPStorageService.java index 08bb5c6..e59b7e3 100644 --- a/src/main/java/com/shwimping/be/global/application/NCPStorageService.java +++ b/src/main/java/com/shwimping/be/global/application/NCPStorageService.java @@ -26,6 +26,9 @@ public class NCPStorageService { @Value("${cloud.aws.s3.bucket}") private String bucket; + @Value("${cloud.aws.cdn.domain}") + private String cdnDomain; + public String uploadFile(MultipartFile multipartFile, String path) { // UUID를 사용하여 파일 이름 생성 String fileName = UUID.randomUUID() + "_" + Objects.requireNonNull(multipartFile.getOriginalFilename()); @@ -40,7 +43,7 @@ public String uploadFile(MultipartFile multipartFile, String path) { amazonS3.putObject(new PutObjectRequest(bucket, fileLocation, multipartFile.getInputStream(), metadata) .withCannedAcl(CannedAccessControlList.PublicRead)); - return amazonS3.getUrl(bucket, fileLocation).toString(); + return cdnDomain + "/" + fileLocation; } catch (IOException e) { throw new FileConvertFailException(GlobalErrorCode.FILE_CONVERT_FAIL); } @@ -48,12 +51,14 @@ public String uploadFile(MultipartFile multipartFile, String path) { public void deleteFile(String fileUrl) { // URL에서 파일 이름 추출 - String fileName = fileUrl.substring(fileUrl.indexOf(bucket) + bucket.length() + 1); + String fileName = fileUrl.substring(cdnDomain.length() + 1); + + log.info("fileName: {}", fileName); + try { amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); } catch (Exception e) { log.error("파일 삭제 실패: {}", e.getMessage()); - // 필요 시 적절한 예외 처리 추가 } } } diff --git a/src/main/java/com/shwimping/be/global/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/shwimping/be/global/exception/handler/GlobalExceptionHandler.java index ad8e889..2e52f22 100644 --- a/src/main/java/com/shwimping/be/global/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/shwimping/be/global/exception/handler/GlobalExceptionHandler.java @@ -9,16 +9,22 @@ import com.shwimping.be.global.exception.response.ErrorResponse.ValidationError; import com.shwimping.be.global.exception.response.ErrorResponse.ValidationErrors; import com.shwimping.be.place.exception.PlaceNotFoundException; +import com.shwimping.be.review.exception.CanNotDeleteReviewException; +import com.shwimping.be.review.exception.ReviewNotFoundException; +import jakarta.servlet.http.HttpServletRequest; import java.util.List; import com.shwimping.be.user.exception.InvalidEmailException; import com.shwimping.be.user.exception.InvalidPasswordException; import com.shwimping.be.user.exception.UserNotFoundException; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -26,17 +32,34 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -@Slf4j @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + private static final Logger log = LoggerFactory.getLogger("ErrorLogger"); + private static final String LOG_FORMAT_INFO = "\n[🔵INFO] - ({} {})\n(id: {}, role: {})\n{}\n {}: {}"; + private static final String LOG_FORMAT_ERROR = "\n[🔴ERROR] - ({} {})\n(id: {}, role: {})"; + @ExceptionHandler(PlaceNotFoundException.class) - public ResponseEntity handlePlaceNotFound(PlaceNotFoundException e) { + public ResponseEntity handlePlaceNotFound(PlaceNotFoundException e, HttpServletRequest request) { + logInfo(e.getErrorCode(), e, request); + return handleExceptionInternal(e.getErrorCode()); + } + + @ExceptionHandler(CanNotDeleteReviewException.class) + public ResponseEntity handleCanNotDeleteReview(CanNotDeleteReviewException e, HttpServletRequest request) { + logInfo(e.getErrorCode(), e, request); + return handleExceptionInternal(e.getErrorCode()); + } + + @ExceptionHandler(ReviewNotFoundException.class) + public ResponseEntity handleReviewNotFound(ReviewNotFoundException e, HttpServletRequest request) { + logInfo(e.getErrorCode(), e, request); return handleExceptionInternal(e.getErrorCode()); } @ExceptionHandler(FileConvertFailException.class) - public ResponseEntity handleFileConvertFail(FileConvertFailException e) { + public ResponseEntity handleFileConvertFail(FileConvertFailException e, HttpServletRequest request) { + logInfo(e.getErrorCode(), e, request); return handleExceptionInternal(e.getErrorCode()); } @@ -53,12 +76,14 @@ public ResponseEntity handleMethodArgumentNotValid( } @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgument() { + public ResponseEntity handleIllegalArgument(IllegalArgumentException e, HttpServletRequest request) { + logInfo(GlobalErrorCode.INVALID_PARAMETER, e, request); return handleExceptionInternal(GlobalErrorCode.INVALID_PARAMETER); } @ExceptionHandler(Exception.class) - public ResponseEntity handleAllException() { + public ResponseEntity handleAllException(Exception e, HttpServletRequest request) { + logError(e, request); return handleExceptionInternal(GlobalErrorCode.INTERNAL_SERVER_ERROR); } @@ -121,4 +146,33 @@ private ErrorResponse makeErrorResponse(BindException e) { .results(new ValidationErrors(validationErrorList)) .build(); } + + private void logInfo(ErrorCode ec, Exception e, HttpServletRequest request) { + log.info(LOG_FORMAT_INFO, request.getMethod(), request.getRequestURI(), getUserId(), + getRole(), ec.getHttpStatus(), e.getClass().getName(), e.getMessage()); + } + + private void logError(Exception e, HttpServletRequest request) { + log.error(LOG_FORMAT_ERROR, request.getMethod(), request.getRequestURI(), getUserId(), getRole(), e); + } + + private String getUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.isAuthenticated()) { + return authentication.getName(); // 사용자의 id + } else { + return "anonymous"; + } + } + + private String getRole() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.isAuthenticated()) { + return authentication.getAuthorities().toString(); // 사용자의 role + } else { + return "anonymous"; + } + } } diff --git a/src/main/java/com/shwimping/be/review/application/ReviewService.java b/src/main/java/com/shwimping/be/review/application/ReviewService.java index 2994e4c..dba6be7 100644 --- a/src/main/java/com/shwimping/be/review/application/ReviewService.java +++ b/src/main/java/com/shwimping/be/review/application/ReviewService.java @@ -1,11 +1,15 @@ package com.shwimping.be.review.application; +import com.amazonaws.util.StringUtils; import com.shwimping.be.global.application.NCPStorageService; import com.shwimping.be.place.application.PlaceService; import com.shwimping.be.place.domain.Place; import com.shwimping.be.review.dto.request.ReviewUploadRequest; import com.shwimping.be.review.dto.response.ReviewSimpleResponse; import com.shwimping.be.review.dto.response.ReviewSimpleResponseList; +import com.shwimping.be.review.exception.CanNotDeleteReviewException; +import com.shwimping.be.review.exception.ReviewNotFoundException; +import com.shwimping.be.review.exception.errorcode.ReviewErrorCode; import com.shwimping.be.review.repository.ReviewRepository; import com.shwimping.be.user.application.UserService; import com.shwimping.be.user.domain.User; @@ -50,4 +54,24 @@ public void uploadReview(Long userId, ReviewUploadRequest reviewUploadRequest, M reviewRepository.save(reviewUploadRequest.toEntity(place, user, imageUrl)); } + + @Transactional + public void deleteReview(Long userId, Long reviewId) { + reviewRepository.findById(reviewId) + .ifPresentOrElse( + review -> { + if (review.getUser().getId().equals(userId)) { + if (StringUtils.hasValue(review.getReviewImageUrl())) { + ncpStorageService.deleteFile(review.getReviewImageUrl()); + } + reviewRepository.delete(review); + } else { + throw new CanNotDeleteReviewException(ReviewErrorCode.CANNOT_DELETE_REVIEW); + } + }, + () -> { + throw new ReviewNotFoundException(ReviewErrorCode.REVIEW_NOT_FOUND); + } + ); + } } diff --git a/src/main/java/com/shwimping/be/review/exception/CanNotDeleteReviewException.java b/src/main/java/com/shwimping/be/review/exception/CanNotDeleteReviewException.java new file mode 100644 index 0000000..536f3c4 --- /dev/null +++ b/src/main/java/com/shwimping/be/review/exception/CanNotDeleteReviewException.java @@ -0,0 +1,11 @@ +package com.shwimping.be.review.exception; + +import com.shwimping.be.global.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class CanNotDeleteReviewException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/shwimping/be/review/exception/ReviewNotFoundException.java b/src/main/java/com/shwimping/be/review/exception/ReviewNotFoundException.java new file mode 100644 index 0000000..3425988 --- /dev/null +++ b/src/main/java/com/shwimping/be/review/exception/ReviewNotFoundException.java @@ -0,0 +1,11 @@ +package com.shwimping.be.review.exception; + +import com.shwimping.be.global.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ReviewNotFoundException extends RuntimeException { + private final ErrorCode errorCode; +} diff --git a/src/main/java/com/shwimping/be/review/exception/errorcode/ReviewErrorCode.java b/src/main/java/com/shwimping/be/review/exception/errorcode/ReviewErrorCode.java new file mode 100644 index 0000000..645a7bf --- /dev/null +++ b/src/main/java/com/shwimping/be/review/exception/errorcode/ReviewErrorCode.java @@ -0,0 +1,18 @@ +package com.shwimping.be.review.exception.errorcode; + +import com.shwimping.be.global.exception.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ReviewErrorCode implements ErrorCode { + + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "Review not found"), + CANNOT_DELETE_REVIEW(HttpStatus.FORBIDDEN, "Author of the review can only delete the review"), + ; + + private final HttpStatus httpStatus; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/shwimping/be/review/presentation/ReviewController.java b/src/main/java/com/shwimping/be/review/presentation/ReviewController.java index 8b101d7..b9b0bee 100644 --- a/src/main/java/com/shwimping/be/review/presentation/ReviewController.java +++ b/src/main/java/com/shwimping/be/review/presentation/ReviewController.java @@ -13,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +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.PostMapping; @@ -62,4 +63,18 @@ public ResponseEntity> uploadReview( .status(HttpStatus.OK) .body(ResponseTemplate.EMPTY_RESPONSE); } + + // 리뷰 삭제 + @Operation(summary = "리뷰 삭제", description = "리뷰 삭제, 본인의 리뷰만 삭제 가능") + @DeleteMapping("/{reviewId}") + public ResponseEntity> deleteReview( + @AuthenticationPrincipal Long userId, + @PathVariable Long reviewId) { + + reviewService.deleteReview(userId, reviewId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(ResponseTemplate.EMPTY_RESPONSE); + } } diff --git a/src/main/java/com/shwimping/be/user/repository/init/UserInitializer.java b/src/main/java/com/shwimping/be/user/repository/init/UserInitializer.java index 94e0915..8425348 100644 --- a/src/main/java/com/shwimping/be/user/repository/init/UserInitializer.java +++ b/src/main/java/com/shwimping/be/user/repository/init/UserInitializer.java @@ -2,29 +2,30 @@ import com.shwimping.be.global.util.DummyDataInit; -import com.shwimping.be.global.util.NCPProperties; import com.shwimping.be.user.domain.User; import com.shwimping.be.user.domain.type.Provider; import com.shwimping.be.user.repository.UserRepository; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.core.annotation.Order; import org.springframework.security.crypto.password.PasswordEncoder; -import java.util.ArrayList; -import java.util.List; - @Slf4j @RequiredArgsConstructor @Order(1) @DummyDataInit public class UserInitializer implements ApplicationRunner { + @Value("${cloud.aws.cdn.domain}") + private String defaultProfileImageUrl; + private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; - private final NCPProperties ncpProperties; private static final String DUMMY_PROFILE_IMAGE_URL = "/profile/ic_profile.svg"; @@ -39,7 +40,7 @@ public void run(ApplicationArguments args) { .nickname("관리자") .fcmToken("fcmToken") .isAlarmAllowed(true) - .profileImageUrl(ncpProperties.s3().endpoint() + ncpProperties.s3().bucket() + DUMMY_PROFILE_IMAGE_URL) + .profileImageUrl(defaultProfileImageUrl + DUMMY_PROFILE_IMAGE_URL) .email("admin@naver.com") .password(passwordEncoder.encode("adminPassword")) .provider(Provider.SELF) @@ -50,7 +51,7 @@ public void run(ApplicationArguments args) { .nickname("user1") .fcmToken("fcmToken") .isAlarmAllowed(true) - .profileImageUrl(ncpProperties.s3().endpoint() + ncpProperties.s3().bucket() + DUMMY_PROFILE_IMAGE_URL) + .profileImageUrl(defaultProfileImageUrl + DUMMY_PROFILE_IMAGE_URL) .email("user1@naver.com") .password(passwordEncoder.encode("user1Password")) .provider(Provider.SELF) @@ -61,7 +62,7 @@ public void run(ApplicationArguments args) { .nickname("user2") .fcmToken("fcmToken") .isAlarmAllowed(true) - .profileImageUrl(ncpProperties.s3().endpoint() + ncpProperties.s3().bucket() + DUMMY_PROFILE_IMAGE_URL) + .profileImageUrl(defaultProfileImageUrl + DUMMY_PROFILE_IMAGE_URL) .email("user2@naver.com") .password(passwordEncoder.encode("user2Password")) .provider(Provider.SELF)