From 5d0e548d0c3b0cfcd56e08792e09f0e769198c45 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 23 Sep 2025 15:50:19 +0900 Subject: [PATCH] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20=EC=88=98=EB=9D=BD=20api=EB=A5=BC=20formda?= =?UTF-8?q?ta=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminReportController.java | 15 ++++----- .../dto/request/ImageUpdateRequestDto.java | 16 ---------- .../domain/report/service/ReportService.java | 31 ++++++++++++++----- .../global/aws/s3/AmazonS3Manager.java | 23 ++++++++++++++ 4 files changed, 53 insertions(+), 32 deletions(-) delete mode 100644 src/main/java/com/DecodEat/domain/report/dto/request/ImageUpdateRequestDto.java diff --git a/src/main/java/com/DecodEat/domain/report/controller/AdminReportController.java b/src/main/java/com/DecodEat/domain/report/controller/AdminReportController.java index dd11479..2e049a6 100644 --- a/src/main/java/com/DecodEat/domain/report/controller/AdminReportController.java +++ b/src/main/java/com/DecodEat/domain/report/controller/AdminReportController.java @@ -1,17 +1,17 @@ package com.DecodEat.domain.report.controller; -import com.DecodEat.domain.report.dto.request.ImageUpdateRequestDto; import com.DecodEat.domain.report.dto.response.ReportResponseDto; import com.DecodEat.domain.report.service.ReportService; import com.DecodEat.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -26,7 +26,6 @@ public class AdminReportController { summary = "상품 수정 요청 조회 (관리자)", description = "관리자가 모든 상품 정보 수정 요청을 페이지별로 조회합니다. 영양 정보 수정과 이미지 확인 요청을 모두 포함합니다.") @Parameters({ - // @PreAuthorize("hasRole('ADMIN')") // Spring Security 사용 시 권한 설정 @Parameter(name = "page", description = "페이지 번호, 0부터 시작합니다.", example = "0"), @Parameter(name = "size", description = "한 페이지에 보여줄 항목 수", example = "10") }) @@ -42,7 +41,6 @@ public ApiResponse getReports( summary = "상품 수정 요청 거절 (관리자)", description = "관리자가 상품 정보 수정 요청을 거절합니다. 해당 신고 내역의 상태를 REJECTED로 변경합니다.") @Parameter(name = "reportId", description = "거절할 신고의 ID", example = "1") - // @PreAuthorize("hasRole('ADMIN')") // Spring Security 사용 시 권한 설정 @PatchMapping("/{reportId}/reject") public ApiResponse rejectReport(@PathVariable Long reportId) { return ApiResponse.onSuccess(reportService.rejectReport(reportId)); @@ -57,13 +55,12 @@ public ApiResponse rejectReport(@PathVariable Long reportId) @Parameters({ @Parameter(name = "reportId", description = "수락할 신고의 ID", example = "1", required = true) }) - // @PreAuthorize("hasRole('ADMIN')") // Spring Security 사용 시 권한 설정 - @PatchMapping("/{reportId}/accept") + @PatchMapping(value = "/{reportId}/accept", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse acceptReport( @PathVariable Long reportId, - @Parameter(name = "imageUpdateRequestDto", description = "이미지 신고 시에만 사용. 새 이미지 URL을 제공하여 교체하거나, 본문을 비워 기존 이미지를 삭제합니다.") - @RequestBody(required = false) ImageUpdateRequestDto requestDto) { - return ApiResponse.onSuccess(reportService.acceptReport(reportId, requestDto)); + @Parameter(name = "newImageUrl", description = "이미지 신고 시에만 사용. 교체할 새 이미지 파일을 첨부하거나, 보내지 않으면 기존 이미지가 삭제됩니다.") + @RequestPart(value = "newImageUrl", required = false) MultipartFile newImageUrl) { + return ApiResponse.onSuccess(reportService.acceptReport(reportId, newImageUrl)); } @Operation(summary = "신고 상세 조회 API", description = "관리자가 특정 신고 내역의 상세 정보를 조회합니다.") diff --git a/src/main/java/com/DecodEat/domain/report/dto/request/ImageUpdateRequestDto.java b/src/main/java/com/DecodEat/domain/report/dto/request/ImageUpdateRequestDto.java deleted file mode 100644 index 47517c7..0000000 --- a/src/main/java/com/DecodEat/domain/report/dto/request/ImageUpdateRequestDto.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.DecodEat.domain.report.dto.request; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@Schema(description = "이미지 신고 수락 시, 새 이미지로 교체할 경우 사용하는 DTO") -public class ImageUpdateRequestDto { - - @Schema(description = "새로 등록할 상품의 대표 이미지 URL", - example = "http://example.image.jpg", - nullable = true) - private String newImageUrl; -} \ No newline at end of file diff --git a/src/main/java/com/DecodEat/domain/report/service/ReportService.java b/src/main/java/com/DecodEat/domain/report/service/ReportService.java index a38d706..0dbbdf3 100644 --- a/src/main/java/com/DecodEat/domain/report/service/ReportService.java +++ b/src/main/java/com/DecodEat/domain/report/service/ReportService.java @@ -4,7 +4,6 @@ import com.DecodEat.domain.products.entity.ProductNutrition; import com.DecodEat.domain.products.repository.ProductRepository; import com.DecodEat.domain.report.converter.ReportConverter; -import com.DecodEat.domain.report.dto.request.ImageUpdateRequestDto; import com.DecodEat.domain.report.dto.request.ProductNutritionUpdateRequestDto; import com.DecodEat.domain.report.dto.response.ReportResponseDto; import com.DecodEat.domain.report.entity.*; @@ -14,6 +13,7 @@ import com.DecodEat.domain.report.repository.ReportRecordRepository; import com.DecodEat.domain.users.entity.User; import com.DecodEat.global.apiPayload.code.status.ErrorStatus; +import com.DecodEat.global.aws.s3.AmazonS3Manager; import com.DecodEat.global.exception.GeneralException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -22,6 +22,9 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.UUID; import static com.DecodEat.global.apiPayload.code.status.ErrorStatus.*; @@ -33,6 +36,7 @@ public class ReportService { private final NutritionReportRepository nutritionReportRepository; private final ImageReportRepository imageReportRepository; private final ReportRecordRepository reportRecordRepository; + private final AmazonS3Manager amazonS3Manager; public ReportResponseDto requestUpdateNutrition(User user, Long productId, ProductNutritionUpdateRequestDto requestDto){ @@ -103,7 +107,7 @@ public ReportResponseDto rejectReport(Long reportId){ * @param reportId 수락할 신고의 ID * @return 처리 결과를 담은 DTO */ - public ReportResponseDto acceptReport(Long reportId, ImageUpdateRequestDto requestDto){ + public ReportResponseDto acceptReport(Long reportId, MultipartFile newImageUrl){ // 1. ID로 신고 내역 조회 ReportRecord reportRecord = reportRecordRepository.findById(reportId) .orElseThrow(() -> new GeneralException(REPORT_NOT_FOUND)); @@ -115,16 +119,29 @@ public ReportResponseDto acceptReport(Long reportId, ImageUpdateRequestDto reque Product product = reportRecord.getProduct(); - // 3. 신고 유횽에 따른 로직 분기 + // 3. 신고 유형에 따른 로직 분기 if (reportRecord instanceof NutritionReport) { ProductNutrition productNutrition = product.getProductNutrition(); productNutrition.updateFromReport((NutritionReport) reportRecord); } else if (reportRecord instanceof ImageReport) { - // 새로운 이미지가 없는 경우 이미지 삭제 -> null로 처리 - // 새로운 이미지가 있는 경우 해당 이미지로 변경 - String newImageUrl = (requestDto != null) ? requestDto.getNewImageUrl() : null; - product.updateProductImage(newImageUrl); + + String oldImageUrl = product.getProductImage(); + + // 새로운 이미지가 있는 경우 새로운 이미지로 변경 + if(newImageUrl != null && !newImageUrl.isEmpty()) { + String imageKey = "products/" + UUID.randomUUID() + "_" + newImageUrl.getOriginalFilename(); + String uploadedImageUrl = amazonS3Manager.uploadFile(imageKey, newImageUrl); + product.updateProductImage(uploadedImageUrl); + } else { + // 새로운 이미지가 없는 경우 이미지 삭제 -> null로 처리 + product.updateProductImage(null); + } + + if(oldImageUrl != null && !oldImageUrl.isEmpty()) { + String oldImageKey = amazonS3Manager.getKeyFromUrl(oldImageUrl); + amazonS3Manager.deleteFile(oldImageKey); + } } // 4. reportstatus 상태를 accepted 변경 diff --git a/src/main/java/com/DecodEat/global/aws/s3/AmazonS3Manager.java b/src/main/java/com/DecodEat/global/aws/s3/AmazonS3Manager.java index 608a45d..6f2c30d 100644 --- a/src/main/java/com/DecodEat/global/aws/s3/AmazonS3Manager.java +++ b/src/main/java/com/DecodEat/global/aws/s3/AmazonS3Manager.java @@ -2,6 +2,7 @@ import com.DecodEat.global.config.AmazonConfig; import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import lombok.RequiredArgsConstructor; @@ -10,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.net.URL; @Slf4j @Component @@ -31,4 +33,25 @@ public String uploadFile(String keyName, MultipartFile file){ return amazonS3.getUrl(amazonConfig.getBucket(), keyName).toString(); } + + public void deleteFile(String keyName){ + try { + amazonS3.deleteObject(new DeleteObjectRequest(amazonConfig.getBucket(), keyName)); + log.info("S3 파일 삭제 성공: key={}", keyName); + } catch (Exception e) { + log.error("S3 파일 삭제 중 에러 발생: key={}", keyName, e); + } + + } + + public String getKeyFromUrl(String fileUrl) { + try { + URL url = new URL(fileUrl); + // URL 경로에서 맨 앞의 '/'를 제외한 부분이 key가 됨 + return url.getPath().substring(1); + } catch (Exception e) { + log.error("URL로부터 S3 key 추출 중 에러 발생: url={}", fileUrl, e); + throw new RuntimeException("URL parsing error", e); + } + } }