diff --git a/src/main/java/com/jeju/nanaland/domain/common/service/VideoFileService.java b/src/main/java/com/jeju/nanaland/domain/common/service/VideoFileService.java index 4f43c409..3a7785f9 100644 --- a/src/main/java/com/jeju/nanaland/domain/common/service/VideoFileService.java +++ b/src/main/java/com/jeju/nanaland/domain/common/service/VideoFileService.java @@ -1,28 +1,20 @@ package com.jeju.nanaland.domain.common.service; -import static com.jeju.nanaland.global.exception.ErrorCode.*; - import com.jeju.nanaland.domain.common.entity.VideoFile; import com.jeju.nanaland.domain.common.repository.VideoFileRepository; -import com.jeju.nanaland.global.exception.ServerErrorException; -import com.jeju.nanaland.global.image_upload.S3VideoService; +import com.jeju.nanaland.global.file.service.FileUploadService; import com.jeju.nanaland.global.image_upload.dto.S3VideoDto; -import java.io.File; -import java.io.IOException; -import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @RequiredArgsConstructor public class VideoFileService { - private final S3VideoService s3VideoService; private final VideoFileRepository videoFileRepository; - private final FileService fileService; + private final FileUploadService fileUploadService; public VideoFile saveS3VideoFile(S3VideoDto s3VideoDto) { VideoFile videoFile = VideoFile.builder() @@ -32,15 +24,8 @@ public VideoFile saveS3VideoFile(S3VideoDto s3VideoDto) { } // S3에 저장될 경로 지정 - public VideoFile uploadAndSaveVideoFile(File file, String directory) { - try { - MultipartFile multipartFile = fileService.convertFileToMultipartFile(file); - CompletableFuture futureVideoDto = s3VideoService.uploadVideoToS3(multipartFile, directory); - S3VideoDto s3VideoDto = futureVideoDto.join(); - return saveS3VideoFile(s3VideoDto); - } catch (IOException e) { - log.error("파일 업로드 오류: {}", e.getMessage()); - throw new ServerErrorException(FILE_FAIL_ERROR.getMessage()); - } + public VideoFile getAndSaveVideoFile(String fileKey) { + S3VideoDto s3VideoDto = fileUploadService.getCloudVideoUrls(fileKey); + return saveS3VideoFile(s3VideoDto); } } diff --git a/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java b/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java index 551d309d..38594111 100644 --- a/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java +++ b/src/main/java/com/jeju/nanaland/domain/member/controller/MemberController.java @@ -220,6 +220,7 @@ public BaseResponse withdrawal( description = "유저 닉네임, 설명, 프로필 사진 수정") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "파일키 형식이 맞지 않는 등 입력값이 올바르지 않은 경우", content = @Content), @ApiResponse(responseCode = "401", description = "accessToken이 유효하지 않은 경우", content = @Content), @ApiResponse(responseCode = "500", description = "이미지 업로드에 실패한 경우", content = @Content) }) diff --git a/src/main/java/com/jeju/nanaland/domain/member/service/MemberProfileService.java b/src/main/java/com/jeju/nanaland/domain/member/service/MemberProfileService.java index 71d80a79..3bb793a0 100644 --- a/src/main/java/com/jeju/nanaland/domain/member/service/MemberProfileService.java +++ b/src/main/java/com/jeju/nanaland/domain/member/service/MemberProfileService.java @@ -17,6 +17,7 @@ import com.jeju.nanaland.global.exception.ErrorCode; import com.jeju.nanaland.global.exception.NotFoundException; import com.jeju.nanaland.global.exception.ServerErrorException; +import com.jeju.nanaland.global.file.data.FileCategory; import com.jeju.nanaland.global.file.service.FileUploadService; import com.jeju.nanaland.global.image_upload.dto.S3ImageDto; import java.util.ArrayList; @@ -54,6 +55,7 @@ public void updateProfile(MemberInfoDto memberInfoDto, MemberRequest.ProfileUpda Member member = memberInfoDto.getMember(); validateNickname(profileUpdateDto.getNickname(), member); if (fileKey != null) { + fileUploadService.validateFileExtension(fileKey, FileCategory.MEMBER_PROFILE); S3ImageDto s3ImageDto = fileUploadService.getCloudImageUrls(fileKey); member.getProfileImageFile().updateImageFile(s3ImageDto.getOriginUrl(), s3ImageDto.getThumbnailUrl()); } diff --git a/src/main/java/com/jeju/nanaland/domain/report/controller/ReportController.java b/src/main/java/com/jeju/nanaland/domain/report/controller/ReportController.java index ab9675dd..9c934963 100644 --- a/src/main/java/com/jeju/nanaland/domain/report/controller/ReportController.java +++ b/src/main/java/com/jeju/nanaland/domain/report/controller/ReportController.java @@ -9,21 +9,17 @@ import com.jeju.nanaland.global.BaseResponse; import com.jeju.nanaland.global.auth.AuthMember; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -37,43 +33,31 @@ public class ReportController { @Operation(summary = "정보 수정 제안", description = "게시물 id와 카테고리를 통해 게시물 정보 수정 제안 요청") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청 (이메일 형식 오류, category로 NANA 요청)", content = @Content), + @ApiResponse(responseCode = "400", description = "잘못된 요청 (이메일 형식 오류, category로 NANA 요청, 파일키 형식 오류)", content = @Content), @ApiResponse(responseCode = "404", description = "해당 게시물이 없는 경우", content = @Content), @ApiResponse(responseCode = "500", description = "사진파일 업로드 실패 또는 관리자에게로 메일 전송 실패", content = @Content) }) - @PostMapping(value = "/info-fix", - consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/info-fix") public BaseResponse requestPostInfoFix( @AuthMember MemberInfoDto memberInfoDto, - @RequestPart("reqDto") @Valid ReportRequest.InfoFixDto reqDto, - @Parameter( - description = "정보 수정 요청 이미지파일 리스트", - content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) - ) - @RequestPart(value = "multipartFileList", required = false) List imageList) { + @RequestBody @Valid ReportRequest.InfoFixDto reqDto) { - reportService.requestPostInfoFix(memberInfoDto, reqDto, imageList); + reportService.requestPostInfoFix(memberInfoDto, reqDto); return BaseResponse.success(POST_INFO_FIX_REPORT_SUCCESS); } @Operation(summary = "신고 기능") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "400", description = "잘못된 요청인 경우", content = @Content), + @ApiResponse(responseCode = "400", description = "파일키 형식이 맞지 않는 등 입력값이 올바르지 않은 경우", content = @Content), @ApiResponse(responseCode = "404", description = "해당 게시물이 없는 경우", content = @Content), @ApiResponse(responseCode = "500", description = "사진파일 업로드 실패 또는 관리자에게로 메일 전송 실패", content = @Content) }) - @PostMapping(value = "/claim", - consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @PostMapping(value = "/claim") public BaseResponse requestClaimReport( @AuthMember MemberInfoDto memberInfoDto, - @RequestPart("reqDto") @Valid ReportRequest.ClaimReportDto reqDto, - @Parameter( - description = "신고 요청 파일 리스트", - content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE) - ) - @RequestPart(value = "multipartFileList", required = false) List fileList) { - reportService.requestClaimReport(memberInfoDto, reqDto, fileList); + @RequestBody @Valid ReportRequest.ClaimReportDto reqDto) { + reportService.requestClaimReport(memberInfoDto, reqDto); return BaseResponse.success(POST_REVIEW_REPORT_SUCCESS); } } diff --git a/src/main/java/com/jeju/nanaland/domain/report/dto/ReportRequest.java b/src/main/java/com/jeju/nanaland/domain/report/dto/ReportRequest.java index 7bfcde20..32afd0d5 100644 --- a/src/main/java/com/jeju/nanaland/domain/report/dto/ReportRequest.java +++ b/src/main/java/com/jeju/nanaland/domain/report/dto/ReportRequest.java @@ -10,6 +10,7 @@ import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -61,6 +62,9 @@ public static class InfoFixDto { example = "test@naver.com" ) private String email; + + @Schema(description = "파일 키 리스트", example = "[\"test/fileKey1.jpg\", \"test/fileKey2.jpeg\", \"test/fileKey3.png\"]") + private List fileKeys; } @Data @@ -110,5 +114,8 @@ public static class ClaimReportDto { example = "test@naver.com" ) private String email; + + @Schema(description = "파일 키 리스트", example = "[\"test/fileKey1.jpg\", \"test/fileKey2.jpeg\", \"test/fileKey3.png\"]") + private List fileKeys; } } diff --git a/src/main/java/com/jeju/nanaland/domain/report/entity/claim/ClaimReportVideoFile.java b/src/main/java/com/jeju/nanaland/domain/report/entity/claim/ClaimReportVideoFile.java index c5abfafc..43316728 100644 --- a/src/main/java/com/jeju/nanaland/domain/report/entity/claim/ClaimReportVideoFile.java +++ b/src/main/java/com/jeju/nanaland/domain/report/entity/claim/ClaimReportVideoFile.java @@ -28,7 +28,7 @@ public class ClaimReportVideoFile { private Long id; @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) - @JoinColumn(name = "image_file_id", nullable = false) + @JoinColumn(name = "video_file_id", nullable = false) private VideoFile videoFile; @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) diff --git a/src/main/java/com/jeju/nanaland/domain/report/service/ReportService.java b/src/main/java/com/jeju/nanaland/domain/report/service/ReportService.java index 9cdd6a2f..dfa80e81 100644 --- a/src/main/java/com/jeju/nanaland/domain/report/service/ReportService.java +++ b/src/main/java/com/jeju/nanaland/domain/report/service/ReportService.java @@ -1,7 +1,6 @@ package com.jeju.nanaland.domain.report.service; import static com.jeju.nanaland.global.exception.ErrorCode.ALREADY_REPORTED; -import static com.jeju.nanaland.global.exception.ErrorCode.IMAGE_BAD_REQUEST; import static com.jeju.nanaland.global.exception.ErrorCode.MEMBER_NOT_FOUND; import static com.jeju.nanaland.global.exception.ErrorCode.NANA_INFO_FIX_FORBIDDEN; import static com.jeju.nanaland.global.exception.ErrorCode.NOT_FOUND_EXCEPTION; @@ -13,7 +12,6 @@ import com.jeju.nanaland.domain.common.dto.CompositeDto; import com.jeju.nanaland.domain.common.entity.ImageFile; import com.jeju.nanaland.domain.common.entity.VideoFile; -import com.jeju.nanaland.domain.common.service.FileService; import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.common.service.MailService; import com.jeju.nanaland.domain.common.service.VideoFileService; @@ -43,26 +41,22 @@ import com.jeju.nanaland.domain.review.repository.ReviewRepository; import com.jeju.nanaland.global.exception.BadRequestException; import com.jeju.nanaland.global.exception.NotFoundException; -import java.io.File; +import com.jeju.nanaland.global.file.data.FileCategory; +import com.jeju.nanaland.global.file.service.FileUploadService; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor @Slf4j public class ReportService { - private static final int MAX_IMAGE_COUNT = 5; private final MemberRepository memberRepository; private final ClaimReportVideoFileRepository claimReportVideoFileRepository; private final ClaimReportRepository claimReportRepository; @@ -77,25 +71,19 @@ public class ReportService { private final VideoFileService videoFileService; private final MailService mailService; private final ReportStrategyFactory reportStrategyFactory; - private final FileService fileService; - @Value("${cloud.aws.s3.infoFixReportImageDirectory}") - private String INFO_FIX_REPORT_IMAGE_DIRECTORY; - @Value("${cloud.aws.s3.claimReportFileDirectory}") - private String CLAIM_REPORT_FILE_DIRECTORY; + private final FileUploadService fileUploadService; /** * 정보 수정 제안 * * @param memberInfoDto 회원 정보 * @param reqDto 수정 요청 DTO - * @param files 수정 요청 이미지 파일 리스트 * @throws NotFoundException 존재하는 게시물이 없는 경우 */ @Transactional - public void requestPostInfoFix(MemberInfoDto memberInfoDto, ReportRequest.InfoFixDto reqDto, - List files) { + public void requestPostInfoFix(MemberInfoDto memberInfoDto, ReportRequest.InfoFixDto reqDto) { // 수정 요청 유효성 검사 - validateInfoFixReportRequest(reqDto, files); + validateInfoFixReportRequest(reqDto); // 해당 게시물 정보 가져오기 CompositeDto compositeDto = findCompositeDto(Category.valueOf(reqDto.getCategory()), @@ -118,40 +106,26 @@ public void requestPostInfoFix(MemberInfoDto memberInfoDto, ReportRequest.InfoFi infoFixReportRepository.save(infoFixReport); // 이미지 저장 - CompletableFuture> futureImageUrls = saveImagesAndGetUrls(files, infoFixReport, - INFO_FIX_REPORT_IMAGE_DIRECTORY); + List imageUrls = saveImagesAndGetUrls(reqDto.getFileKeys(), infoFixReport); // 이메일 전송 - futureImageUrls.thenAccept(imageUrls -> mailService.sendEmailReport(infoFixReport, imageUrls)); + mailService.sendEmailReport(infoFixReport, imageUrls); } /** * 정보 수정 제안 요청 유효성 확인 * * @param reqDto 수정 요청 DTO - * @param files 수정 요청 이미지 파일 리스트 * @throws BadRequestException 카테고리가 올바르지 않은 경우 */ - private void validateInfoFixReportRequest(InfoFixDto reqDto, List files) { + private void validateInfoFixReportRequest(InfoFixDto reqDto) { Category category = Category.valueOf(reqDto.getCategory()); // 나나스픽 전처리 if (List.of(Category.NANA, Category.NANA_CONTENT).contains(category)) { throw new BadRequestException(NANA_INFO_FIX_FORBIDDEN.getMessage()); } - checkFileCountLimit(files); - } - - /** - * 파일 개수 유효성 확인 - * - * @param files 파일 리스트 - * @throws BadRequestException 파일 개수가 초과된 경우 - */ - private void checkFileCountLimit(List files) { - if (files != null && files.size() > MAX_IMAGE_COUNT) { - throw new BadRequestException(IMAGE_BAD_REQUEST.getMessage()); - } + fileUploadService.validateFileKeys(reqDto.getFileKeys(), FileCategory.INFO_FIX_REPORT); } /** @@ -178,13 +152,11 @@ private CompositeDto findCompositeDto(Category category, Long postId, Language l * * @param memberInfoDto 회원 정보 * @param reqDto 신고 요청 DTO - * @param files 파일 리스트 */ @Transactional - public void requestClaimReport(MemberInfoDto memberInfoDto, ReportRequest.ClaimReportDto reqDto, - List files) { + public void requestClaimReport(MemberInfoDto memberInfoDto, ReportRequest.ClaimReportDto reqDto) { // 요청 유효성 확인 - validateClaimReportRequest(memberInfoDto, reqDto, files); + validateClaimReportRequest(memberInfoDto, reqDto); // claimReport 저장 ClaimReport claimReport = ClaimReport.builder() @@ -198,22 +170,15 @@ public void requestClaimReport(MemberInfoDto memberInfoDto, ReportRequest.ClaimR claimReportRepository.save(claimReport); // 이미지, 동영상 저장 - List imageFiles = filterFilesByType(files, "image/"); - List videoFiles = filterFilesByType(files, "video/"); + List imageFileKeys = filterFilesByType(reqDto.getFileKeys(), true); + List videoFileKeys = filterFilesByType(reqDto.getFileKeys(), false); - CompletableFuture> futureImageUrls = saveImagesAndGetUrls(imageFiles, claimReport, - CLAIM_REPORT_FILE_DIRECTORY); - CompletableFuture> futureVideoUrls = saveVideosAndGetUrls(videoFiles, claimReport); + List imageUrls = saveImagesAndGetUrls(imageFileKeys, claimReport); + List videoUrls = saveVideosAndGetUrls(videoFileKeys, claimReport); - CompletableFuture.allOf(futureImageUrls, futureVideoUrls) - .thenApply(v -> { - List imageUrls = futureImageUrls.join(); - List videoUrls = futureVideoUrls.join(); - List combinedUrls = new ArrayList<>(imageUrls); - combinedUrls.addAll(videoUrls); - return combinedUrls; - }) - .thenAccept(combinedUrls -> mailService.sendEmailReport(claimReport, combinedUrls)); + List combinedUrls = new ArrayList<>(imageUrls); + combinedUrls.addAll(videoUrls); + mailService.sendEmailReport(claimReport, combinedUrls); } /** @@ -221,11 +186,9 @@ public void requestClaimReport(MemberInfoDto memberInfoDto, ReportRequest.ClaimR * * @param memberInfoDto 회원 정보 * @param reqDto 신고 요청 DTO - * @param files 파일 리스트 * @throws BadRequestException 이미 신고한 적이 있는 경우 */ - private void validateClaimReportRequest(MemberInfoDto memberInfoDto, ClaimReportDto reqDto, - List files) { + private void validateClaimReportRequest(MemberInfoDto memberInfoDto, ClaimReportDto reqDto) { // 타입별 유효성 확인 ClaimReportType claimReportType = ClaimReportType.valueOf(reqDto.getReportType()); if (claimReportType == ClaimReportType.REVIEW) { @@ -241,7 +204,7 @@ private void validateClaimReportRequest(MemberInfoDto memberInfoDto, ClaimReport throw new BadRequestException(ALREADY_REPORTED.getMessage()); } - checkFileCountLimit(files); + fileUploadService.validateFileKeys(reqDto.getFileKeys(), FileCategory.CLAIM_REPORT); } /** @@ -283,96 +246,67 @@ private void validateMemberReportRequest(MemberInfoDto memberInfoDto, /** * 타입별 파일 필터링 * - * @param files 파일 리스트 - * @param type 파일 타입 + * @param fileKeys 파일 키 리스트 + * @param isImage 파일 타입 (이미지이면 true, 영상이면 false) * @return 필터링된 파일 리스트 */ - private List filterFilesByType(List files, String type) { - if (files == null) { + private List filterFilesByType(List fileKeys, boolean isImage) { + if (fileKeys == null) { return new ArrayList<>(); } - return files.stream() - .filter(file -> file.getContentType() != null && file.getContentType().startsWith(type)) + return fileKeys.stream() + .filter(fileKey -> + (isImage && FileCategory.CLAIM_REPORT.isImage(fileKey)) + || (!isImage && FileCategory.CLAIM_REPORT.isVideo(fileKey))) .collect(Collectors.toList()); } /** * 이미지 파일 저장, 이미지 URL 리스트 얻기 * - * @param multipartFiles 이미지 파일 리스트 + * @param fileKeys 파일 키 리스트 * @param report 요청 (InfoFixReport, ClaimReport) - * @param directory 파일 저장 위치 * @return 이미지 URL 리스트 */ - private CompletableFuture> saveImagesAndGetUrls(List multipartFiles, Report report, - String directory) { - if (multipartFiles == null || multipartFiles.isEmpty()) { - return CompletableFuture.completedFuture(Collections.emptyList()); + private List saveImagesAndGetUrls(List fileKeys, Report report) { + if (fileKeys == null || fileKeys.isEmpty()) { + return new ArrayList<>(); } // 이미지 저장 - List> futureImageFiles = multipartFiles.stream() - .map(multipartFile -> { - File file = fileService.convertMultipartFileToFile(multipartFile); - return CompletableFuture.supplyAsync(() -> - imageFileService.uploadAndSaveImageFile(file, false, directory)); - }) + List imageFiles = fileKeys.stream() + .map(imageFileService::getAndSaveImageFile) .toList(); - return CompletableFuture.allOf(futureImageFiles.toArray(new CompletableFuture[0])) - .thenApply(v -> { - // ImageFile 리스트로 변환 - List imageFiles = futureImageFiles.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList()); - - // 이미지와 Report 매핑 저장 - ReportStrategy reportStrategy = reportStrategyFactory.findStrategy(report.getReportType()); - reportStrategy.saveReportImages(report, imageFiles); - - // 이미지 URL 리스트 반환 - return imageFiles.stream() - .map(ImageFile::getOriginUrl) - .collect(Collectors.toList()); - }); + ReportStrategy reportStrategy = reportStrategyFactory.findStrategy(report.getReportType()); + reportStrategy.saveReportImages(report, imageFiles); + return imageFiles.stream() + .map(ImageFile::getOriginUrl) + .collect(Collectors.toList()); } /** * 동영상 파일 저장, 동영상 URL 리스트 얻기 * - * @param multipartFiles 동영상 파일 리스트 + * @param fileKeys 동영상 파일 키 리스트 * @param report 요청 (InfoFixReport, ClaimReport) * @return 동영상 URL 리스트 */ - private CompletableFuture> saveVideosAndGetUrls(List multipartFiles, ClaimReport report) { - if (multipartFiles == null || multipartFiles.isEmpty()) { - return CompletableFuture.completedFuture(Collections.emptyList()); + private List saveVideosAndGetUrls(List fileKeys, ClaimReport report) { + if (fileKeys == null || fileKeys.isEmpty()) { + return new ArrayList<>(); } // 동영상 파일 저장 - List> futureVideoFiles = multipartFiles.stream() - .map(multipartFile -> { - File file = fileService.convertMultipartFileToFile(multipartFile); - return CompletableFuture.supplyAsync(() -> - videoFileService.uploadAndSaveVideoFile(file, CLAIM_REPORT_FILE_DIRECTORY)); - }) + List videoFiles = fileKeys.stream() + .map(videoFileService::getAndSaveVideoFile) .toList(); + List reportVideoFiles = createReportVideoFiles(videoFiles, report); + claimReportVideoFileRepository.saveAll(reportVideoFiles); - return CompletableFuture.allOf(futureVideoFiles.toArray(new CompletableFuture[0])) - .thenApply(v -> { - // VideoFile 리스트로 변환 - List videoFiles = futureVideoFiles.stream() - .map(CompletableFuture::join) - .toList(); - - // 동영상과 Report 매핑 생성 및 저장 - List reportVideoFiles = createReportVideoFiles(videoFiles, report); - claimReportVideoFileRepository.saveAll(reportVideoFiles); - - // 이미지 URL 리스트 반환 - return videoFiles.stream() - .map(VideoFile::getOriginUrl) - .collect(Collectors.toList()); - }); + // 동영상 URL 리스트 반환 + return videoFiles.stream() + .map(VideoFile::getOriginUrl) + .collect(Collectors.toList()); } /** diff --git a/src/main/java/com/jeju/nanaland/domain/review/controller/ReviewController.java b/src/main/java/com/jeju/nanaland/domain/review/controller/ReviewController.java index 84ce2af2..4fcb61b0 100644 --- a/src/main/java/com/jeju/nanaland/domain/review/controller/ReviewController.java +++ b/src/main/java/com/jeju/nanaland/domain/review/controller/ReviewController.java @@ -67,6 +67,7 @@ public BaseResponse getReviewList( @Operation(summary = "리뷰 생성", description = "게시물에 대한 리뷰 작성") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "파일키 형식이 맞지 않는 등 입력값이 올바르지 않은 경우", content = @Content), @ApiResponse(responseCode = "401", description = "accessToken이 유효하지 않은 경우", content = @Content), @ApiResponse(responseCode = "404", description = "입력한 값이 존재하지 않는 경우", content = @Content), @ApiResponse(responseCode = "500", description = "서버측 에러", content = @Content) diff --git a/src/main/java/com/jeju/nanaland/domain/review/service/ReviewService.java b/src/main/java/com/jeju/nanaland/domain/review/service/ReviewService.java index 68799613..058639e0 100644 --- a/src/main/java/com/jeju/nanaland/domain/review/service/ReviewService.java +++ b/src/main/java/com/jeju/nanaland/domain/review/service/ReviewService.java @@ -5,7 +5,6 @@ import static com.jeju.nanaland.global.exception.ErrorCode.MEMBER_REVIEW_NOT_FOUND; import static com.jeju.nanaland.global.exception.ErrorCode.NOT_FOUND_EXCEPTION; import static com.jeju.nanaland.global.exception.ErrorCode.NOT_MY_REVIEW; -import static com.jeju.nanaland.global.exception.ErrorCode.REVIEW_IMAGE_BAD_REQUEST; import static com.jeju.nanaland.global.exception.ErrorCode.REVIEW_IMAGE_IMAGE_INFO_NOT_MATCH; import static com.jeju.nanaland.global.exception.ErrorCode.REVIEW_INVALID_CATEGORY; import static com.jeju.nanaland.global.exception.ErrorCode.REVIEW_KEYWORD_DUPLICATION; @@ -47,6 +46,8 @@ import com.jeju.nanaland.global.exception.BadRequestException; import com.jeju.nanaland.global.exception.ErrorCode; import com.jeju.nanaland.global.exception.NotFoundException; +import com.jeju.nanaland.global.file.data.FileCategory; +import com.jeju.nanaland.global.file.service.FileUploadService; import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.Arrays; @@ -83,6 +84,7 @@ public class ReviewService { private final RedisTemplate redisTemplate; private final MemberRepository memberRepository; private final RestaurantRepository restaurantRepository; + private final FileUploadService fileUploadService; @Value("${cloud.aws.s3.reviewDirectory}") private String reviewImageDirectoryPath; @@ -111,15 +113,9 @@ public ReviewListDto getReviewList(MemberInfoDto memberInfoDto, Category categor @Transactional public void saveReview(MemberInfoDto memberInfoDto, Long id, Category category, CreateReviewDto createReviewDto) { - if (category != Category.EXPERIENCE && category != Category.RESTAURANT) { - throw new BadRequestException(REVIEW_INVALID_CATEGORY.getMessage()); - } + validateReviewRequest(category, createReviewDto); Post post = getPostById(id, category); - List fileKeys = createReviewDto.getFileKeys(); - if (fileKeys != null && fileKeys.size() > 5) { - throw new BadRequestException(REVIEW_IMAGE_BAD_REQUEST.getMessage()); - } // 리뷰 저장 Review review = reviewRepository.save(Review.builder() @@ -146,6 +142,7 @@ public void saveReview(MemberInfoDto memberInfoDto, Long id, Category category, .build())); // reviewImageFile + List fileKeys = createReviewDto.getFileKeys(); if (fileKeys != null && !fileKeys.isEmpty()) { List reviewImageFiles = fileKeys.stream() .map((fileKey -> { @@ -160,6 +157,14 @@ public void saveReview(MemberInfoDto memberInfoDto, Long id, Category category, } } + private void validateReviewRequest(Category category, CreateReviewDto createReviewDto) { + + if (category != Category.EXPERIENCE && category != Category.RESTAURANT) { + throw new BadRequestException(REVIEW_INVALID_CATEGORY.getMessage()); + } + fileUploadService.validateFileKeys(createReviewDto.getFileKeys(), FileCategory.REVIEW); + } + // 리뷰를 위한 게시글 검색 자동완성 public List getAutoCompleteSearchResultForReview( diff --git a/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java b/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java index 247b40d8..d9e6ec45 100644 --- a/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java +++ b/src/main/java/com/jeju/nanaland/global/exception/ErrorCode.java @@ -19,8 +19,7 @@ public enum ErrorCode { BAD_REQUEST_EXCEPTION(BAD_REQUEST, "잘못된 요청입니다."), REQUEST_VALIDATION_EXCEPTION(BAD_REQUEST, "입력 형태가 잘못된 요청입니다."), MEMBER_CONSENT_BAD_REQUEST(BAD_REQUEST, "TERMS_OF_USE는 필수로 동의해야 합니다."), - IMAGE_BAD_REQUEST(BAD_REQUEST, "이미지는 최대 5장까지 가능합니다."), - REVIEW_IMAGE_BAD_REQUEST(BAD_REQUEST, "리뷰 이미지는 최대 5장까지 가능합니다."), + FILE_LIMIT_BAD_REQUEST(BAD_REQUEST, "파일은 최대 5개까지 가능합니다"), START_DATE_AFTER_END_DATE(BAD_REQUEST, "endDate가 startDate보다 앞서 있습니다."), INVALID_EXPERIENCE_TYPE(BAD_REQUEST, "이색체험 타입은 ACTIVITY, CULTURE_AND_ARTS 만 가능합니다."), INVALID_RESTAURANT_KEYWORD_TYPE(BAD_REQUEST, "잘못된 맛집 키워드입니다."), diff --git a/src/main/java/com/jeju/nanaland/global/file/data/FileCategory.java b/src/main/java/com/jeju/nanaland/global/file/data/FileCategory.java index 59e5c5e9..b9e16db4 100644 --- a/src/main/java/com/jeju/nanaland/global/file/data/FileCategory.java +++ b/src/main/java/com/jeju/nanaland/global/file/data/FileCategory.java @@ -17,4 +17,20 @@ public enum FileCategory { FileCategory(List allowedExtensions) { this.allowedExtensions = allowedExtensions; } + + public boolean isImage(String fileKey) { + String extension = getExtension(fileKey); + return allowedExtensions.contains(extension) && + Arrays.asList("jpeg", "jpg", "png", "webp").contains(extension); + } + + public boolean isVideo(String filename) { + String extension = getExtension(filename); + return allowedExtensions.contains(extension) && + Arrays.asList("mp4", "mov", "webm").contains(extension); + } + + private String getExtension(String fileKey) { + return fileKey.substring(fileKey.lastIndexOf(".") + 1).toLowerCase(); + } } diff --git a/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java b/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java index 81d385c7..dc681810 100644 --- a/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java +++ b/src/main/java/com/jeju/nanaland/global/file/service/FileUploadService.java @@ -1,5 +1,6 @@ package com.jeju.nanaland.global.file.service; +import static com.jeju.nanaland.global.exception.ErrorCode.FILE_LIMIT_BAD_REQUEST; import static com.jeju.nanaland.global.exception.ErrorCode.FILE_S3_NOT_FOUNE; import static com.jeju.nanaland.global.exception.ErrorCode.FILE_UPLOAD_FAIL; import static com.jeju.nanaland.global.exception.ErrorCode.INVALID_FILE_EXTENSION_TYPE; @@ -13,8 +14,8 @@ import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest; import com.amazonaws.services.s3.model.InitiateMultipartUploadResult; -import com.amazonaws.services.s3.model.PartETag; import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PartETag; import com.jeju.nanaland.global.exception.BadRequestException; import com.jeju.nanaland.global.exception.NotFoundException; import com.jeju.nanaland.global.exception.ServerErrorException; @@ -25,6 +26,7 @@ import com.jeju.nanaland.global.file.dto.FileResponse.InitResultDto; import com.jeju.nanaland.global.file.dto.FileResponse.PresignedUrlInfo; import com.jeju.nanaland.global.image_upload.dto.S3ImageDto; +import com.jeju.nanaland.global.image_upload.dto.S3VideoDto; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -47,6 +49,9 @@ @Slf4j public class FileUploadService { + private static final int MAX_IMAGE_COUNT = 5; + private static final int PRESIGNEDURL_EXPIRATION = 30; + private static final long MAX_FILE_SIZE = 30 * 1024 * 1024L; private final AmazonS3 amazonS3; private final AmazonS3Client amazonS3Client; @Value("${cloud.aws.cloudfront.domain}") @@ -61,8 +66,6 @@ public class FileUploadService { private String infoFixReportDirectory; @Value("${cloud.aws.s3.claimReportFileDirectory}") private String claimReportDirectory; - private static final int PRESIGNEDURL_EXPIRATION = 30; - private static final long MAX_FILE_SIZE = 30 * 1024 * 1024L; public FileResponse.InitResultDto uploadInit(FileRequest.InitCommandDto initCommandDto) { // 파일 크기 유효성 검사 @@ -70,7 +73,7 @@ public FileResponse.InitResultDto uploadInit(FileRequest.InitCommandDto initComm // 파일 형식 유효성 검사 String contentType = validateFileExtension(initCommandDto.getOriginalFileName(), - initCommandDto.getFileCategory()); + FileCategory.valueOf(initCommandDto.getFileCategory())); // S3 key 생성 String fileKey = generateUniqueFileKey(initCommandDto.getOriginalFileName(), @@ -89,28 +92,29 @@ public FileResponse.InitResultDto uploadInit(FileRequest.InitCommandDto initComm InitiateMultipartUploadResult initResponse = amazonS3.initiateMultipartUpload(initRequest); String uploadId = initResponse.getUploadId(); - List presignedUrlInfos = new ArrayList<>(); - // 파트별 Pre-Signed URL 발급 - for (int partNumber = 1; partNumber <= initCommandDto.getPartCount(); partNumber++) { - GeneratePresignedUrlRequest presignedUrlRequest = new GeneratePresignedUrlRequest(bucket, fileKey) - .withMethod(HttpMethod.PUT) - .withExpiration(getPresignedUrlExpiration()) - .withKey(fileKey); - - presignedUrlRequest.addRequestParameter("partNumber", String.valueOf(partNumber)); - presignedUrlRequest.addRequestParameter("uploadId", uploadId); - URL presignedUrl = amazonS3.generatePresignedUrl(presignedUrlRequest); - - presignedUrlInfos.add(PresignedUrlInfo.builder() - .partNumber(partNumber) - .preSignedUrl(presignedUrl.toString()) - .build()); - } - return InitResultDto.builder() - .uploadId(uploadId) - .fileKey(fileKey) - .presignedUrlInfos(presignedUrlInfos) - .build(); + List presignedUrlInfos = new ArrayList<>(); + // 파트별 Pre-Signed URL 발급 + for (int partNumber = 1; partNumber <= initCommandDto.getPartCount(); partNumber++) { + GeneratePresignedUrlRequest presignedUrlRequest = new GeneratePresignedUrlRequest(bucket, + fileKey) + .withMethod(HttpMethod.PUT) + .withExpiration(getPresignedUrlExpiration()) + .withKey(fileKey); + + presignedUrlRequest.addRequestParameter("partNumber", String.valueOf(partNumber)); + presignedUrlRequest.addRequestParameter("uploadId", uploadId); + URL presignedUrl = amazonS3.generatePresignedUrl(presignedUrlRequest); + + presignedUrlInfos.add(PresignedUrlInfo.builder() + .partNumber(partNumber) + .preSignedUrl(presignedUrl.toString()) + .build()); + } + return InitResultDto.builder() + .uploadId(uploadId) + .fileKey(fileKey) + .presignedUrlInfos(presignedUrlInfos) + .build(); } catch (Exception e) { log.error("Pre-Signed URL Init 실패 : {}", e.getMessage()); throw new ServerErrorException(FILE_UPLOAD_FAIL.getMessage()); @@ -123,7 +127,15 @@ private void validateFileSize(@NotNull Long fileSize) { } } - private String validateFileExtension(@NotBlank String originalFileName, String fileCategory) { + /** + * 파일 확장자 유효성 확인 + * + * @param originalFileName 파일명 + * @param fileCategory 파일 카테고리 + * @return 파일 content Type + */ + public String validateFileExtension(@NotBlank String originalFileName, + FileCategory fileCategory) { if (originalFileName == null || !originalFileName.contains(".")) { throw new BadRequestException(NO_FILE_EXTENSION.getMessage()); } @@ -132,7 +144,7 @@ private String validateFileExtension(@NotBlank String originalFileName, String f .substring(originalFileName.lastIndexOf('.') + 1) .toLowerCase(); - if (!FileCategory.valueOf(fileCategory).getAllowedExtensions().contains(extension)) { + if (!fileCategory.getAllowedExtensions().contains(extension)) { throw new BadRequestException(INVALID_FILE_EXTENSION_TYPE.getMessage()); } @@ -212,4 +224,33 @@ public S3ImageDto getCloudImageUrls(String fileKey) { .thumbnailUrl(thumbnailUrl) .build(); } + + public S3VideoDto getCloudVideoUrls(String fileKey) { + if (!amazonS3Client.doesObjectExist(bucket, fileKey)) { + throw new NotFoundException(FILE_S3_NOT_FOUNE.getMessage()); + } + String originUrl = cloudFrontDomain + "/" + fileKey; + + return S3VideoDto.builder() + .originUrl(originUrl) + .build(); + } + + /** + * 파일 개수 유효성 확인 + * + * @param fileKeys 파일키 리스트 + * @throws BadRequestException 파일 개수가 초과된 경우 + */ + public void validateFileKeys(List fileKeys, FileCategory fileCategory) { + if (fileKeys == null) { + return; + } + + if (fileKeys.size() > MAX_IMAGE_COUNT) { + throw new BadRequestException(FILE_LIMIT_BAD_REQUEST.getMessage()); + } + + fileKeys.forEach(fileKey -> validateFileExtension(fileKey, fileCategory)); + } } diff --git a/src/test/java/com/jeju/nanaland/domain/report/service/ReportServiceTest.java b/src/test/java/com/jeju/nanaland/domain/report/service/ReportServiceTest.java index a1da31d7..94f8ef2d 100644 --- a/src/test/java/com/jeju/nanaland/domain/report/service/ReportServiceTest.java +++ b/src/test/java/com/jeju/nanaland/domain/report/service/ReportServiceTest.java @@ -1,7 +1,7 @@ package com.jeju.nanaland.domain.report.service; import static com.jeju.nanaland.global.exception.ErrorCode.ALREADY_REPORTED; -import static com.jeju.nanaland.global.exception.ErrorCode.IMAGE_BAD_REQUEST; +import static com.jeju.nanaland.global.exception.ErrorCode.FILE_LIMIT_BAD_REQUEST; import static com.jeju.nanaland.global.exception.ErrorCode.MEMBER_NOT_FOUND; import static com.jeju.nanaland.global.exception.ErrorCode.NANA_INFO_FIX_FORBIDDEN; import static com.jeju.nanaland.global.exception.ErrorCode.NOT_FOUND_EXCEPTION; @@ -10,11 +10,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import com.jeju.nanaland.domain.common.data.Category; import com.jeju.nanaland.domain.common.data.Language; +import com.jeju.nanaland.domain.common.entity.ImageFile; +import com.jeju.nanaland.domain.common.entity.VideoFile; import com.jeju.nanaland.domain.common.service.FileService; import com.jeju.nanaland.domain.common.service.ImageFileService; import com.jeju.nanaland.domain.common.service.MailService; @@ -26,6 +30,7 @@ import com.jeju.nanaland.domain.member.entity.enums.TravelType; import com.jeju.nanaland.domain.member.repository.MemberRepository; import com.jeju.nanaland.domain.report.dto.ReportRequest; +import com.jeju.nanaland.domain.report.entity.ReportType; import com.jeju.nanaland.domain.report.entity.claim.ClaimReportStrategy; import com.jeju.nanaland.domain.report.entity.infoFix.FixType; import com.jeju.nanaland.domain.report.entity.infoFix.InfoFixReport; @@ -41,7 +46,8 @@ import com.jeju.nanaland.domain.review.repository.ReviewRepository; import com.jeju.nanaland.global.exception.BadRequestException; import com.jeju.nanaland.global.exception.NotFoundException; -import java.nio.charset.StandardCharsets; +import com.jeju.nanaland.global.file.data.FileCategory; +import com.jeju.nanaland.global.file.service.FileUploadService; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -58,8 +64,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; @ExtendWith(MockitoExtension.class) @Execution(ExecutionMode.CONCURRENT) @@ -94,25 +98,25 @@ class ReportServiceTest { ClaimReportStrategy claimReportStrategy; @Mock FileService fileService; + @Mock + FileUploadService fileUploadService; MemberInfoDto memberInfoDto, memberInfoDto2; - private static List createImageMultipartFiles(int itemCount) { - List files = new ArrayList<>(); + private static List createImageFileKeys(int itemCount) { + List fileKeys = new ArrayList<>(); for (int i = 0; i < itemCount; i++) { - files.add(new MockMultipartFile("image", "test.png", "image/png", - "test file".getBytes(StandardCharsets.UTF_8))); + fileKeys.add("test/" + i + ".jpg"); } - return files; + return fileKeys; } - private static List createVideoMultipartFiles(int itemCount) { - List files = new ArrayList<>(); + private static List createVideoFileKeys(int itemCount) { + List fileKeys = new ArrayList<>(); for (int i = 0; i < itemCount; i++) { - files.add(new MockMultipartFile("video", "test.mp4", "video/mp4", - "test file".getBytes(StandardCharsets.UTF_8))); + fileKeys.add("test/" + itemCount + ".mp4"); } - return files; + return fileKeys; } @BeforeEach @@ -173,7 +177,7 @@ void requestPostInfoFixFail_invalidCategory(Category category) { // when: 정보 수정 제안 // then: ErrorCode 검증 - assertThatThrownBy(() -> reportService.requestPostInfoFix(memberInfoDto, infoFixDto, null)) + assertThatThrownBy(() -> reportService.requestPostInfoFix(memberInfoDto, infoFixDto)) .isInstanceOf(BadRequestException.class) .hasMessage(NANA_INFO_FIX_FORBIDDEN.getMessage()); } @@ -183,13 +187,17 @@ void requestPostInfoFixFail_invalidCategory(Category category) { void requestPostInfoFixFail_fileCountOverLimit() { // given: 파일 개수가 초과되도록 설정 ReportRequest.InfoFixDto infoFixDto = createInfoFixDto(Category.MARKET); - List files = createImageMultipartFiles(6); + List fileKeys = createImageFileKeys(6); + infoFixDto.setFileKeys(fileKeys); + doThrow(new BadRequestException(FILE_LIMIT_BAD_REQUEST.getMessage())) + .when(fileUploadService) + .validateFileKeys(any(), any(FileCategory.class)); // when: 정보 수정 제안 // then: ErrorCode 검증 - assertThatThrownBy(() -> reportService.requestPostInfoFix(memberInfoDto, infoFixDto, files)) + assertThatThrownBy(() -> reportService.requestPostInfoFix(memberInfoDto, infoFixDto)) .isInstanceOf(BadRequestException.class) - .hasMessage(IMAGE_BAD_REQUEST.getMessage()); + .hasMessage(FILE_LIMIT_BAD_REQUEST.getMessage()); } @Test @@ -202,7 +210,7 @@ void requestPostInfoFixFail_postNotFound() { // when: 정보 수정 제안 // then: ErrorCode 검증 - assertThatThrownBy(() -> reportService.requestPostInfoFix(memberInfoDto, infoFixDto, null)) + assertThatThrownBy(() -> reportService.requestPostInfoFix(memberInfoDto, infoFixDto)) .isInstanceOf(NotFoundException.class) .hasMessage(NOT_FOUND_EXCEPTION.getMessage()); } @@ -213,14 +221,18 @@ void requestPostInfoFixSuccess() { // given: 정보 수정 제안 요청 설정 ReportRequest.InfoFixDto infoFixDto = createInfoFixDto(Category.MARKET); int itemCount = 3; - List files = createImageMultipartFiles(itemCount); + List fileKeys = createImageFileKeys(itemCount); + infoFixDto.setFileKeys(fileKeys); doReturn(MarketCompositeDto.builder().build()).when(marketRepository) .findCompositeDtoById(any(), any(Language.class)); + doReturn(mock(ImageFile.class)).when(imageFileService) + .getAndSaveImageFile(any()); + doReturn(infoFixReportStrategy).when(reportStrategyFactory).findStrategy(any(ReportType.class)); doReturn(null).when(infoFixReportRepository).save(any(InfoFixReport.class)); // when: 정보 수정 제안 - reportService.requestPostInfoFix(memberInfoDto, infoFixDto, files); + reportService.requestPostInfoFix(memberInfoDto, infoFixDto); // then: 정보 수정 제안 요청 검증 verify(infoFixReportRepository).save(any(InfoFixReport.class)); @@ -241,7 +253,7 @@ void requestClaimReportFail_reviewNotFound() { // when: 리뷰 신고 요청 // then: ErrorCode 검증 assertThatThrownBy( - () -> reportService.requestClaimReport(memberInfoDto, claimReportDto, null)) + () -> reportService.requestClaimReport(memberInfoDto, claimReportDto)) .isInstanceOf(NotFoundException.class) .hasMessage(REVIEW_NOT_FOUND.getMessage()); } @@ -257,7 +269,7 @@ void requestClaimReportFail_selfReportedReview() { // when: 리뷰 신고 요청 // then: ErrorCode 검증 assertThatThrownBy( - () -> reportService.requestClaimReport(memberInfoDto, claimReportDto, null)) + () -> reportService.requestClaimReport(memberInfoDto, claimReportDto)) .isInstanceOf(BadRequestException.class) .hasMessage(SELF_REPORT_NOT_ALLOWED.getMessage()); } @@ -275,7 +287,7 @@ void requestClaimReportFail_alreadyReported() { // when: 리뷰 신고 요청 // then: ErrorCode 검증 assertThatThrownBy( - () -> reportService.requestClaimReport(memberInfoDto, claimReportDto, null)) + () -> reportService.requestClaimReport(memberInfoDto, claimReportDto)) .isInstanceOf(BadRequestException.class) .hasMessage(ALREADY_REPORTED.getMessage()); } @@ -286,7 +298,12 @@ void requestClaimReportFail_fileCountOverLimit() { // given: 파일 개수가 초과되도록 설정 ReportRequest.ClaimReportDto claimReportDto = createClaimReportDto(ClaimReportType.REVIEW); Review review = createReview(memberInfoDto2.getMember()); - List files = createImageMultipartFiles(6); + List fileKeys = createImageFileKeys(6); + claimReportDto.setFileKeys(fileKeys); + + doThrow(new BadRequestException(FILE_LIMIT_BAD_REQUEST.getMessage())) + .when(fileUploadService) + .validateFileKeys(any(), any(FileCategory.class)); doReturn(Optional.of(review)).when(reviewRepository).findById(any()); doReturn(Optional.empty()).when(claimReportRepository) .findByMemberAndReferenceIdAndClaimReportType(any(Member.class), any(), any(ClaimReportType.class)); @@ -294,9 +311,9 @@ void requestClaimReportFail_fileCountOverLimit() { // when: 리뷰 신고 요청 // then: ErrorCode 검증 assertThatThrownBy( - () -> reportService.requestClaimReport(memberInfoDto, claimReportDto, files)) + () -> reportService.requestClaimReport(memberInfoDto, claimReportDto)) .isInstanceOf(BadRequestException.class) - .hasMessage(IMAGE_BAD_REQUEST.getMessage()); + .hasMessage(FILE_LIMIT_BAD_REQUEST.getMessage()); } @Test @@ -307,17 +324,24 @@ void requestReviewClaimReportSuccess() { Review review = createReview(memberInfoDto2.getMember()); int imageCount = 3; int videoCount = 2; - List imageMultipartFiles = createImageMultipartFiles(imageCount); - List videoMultipartFiles = createVideoMultipartFiles(videoCount); - List files = new ArrayList<>(imageMultipartFiles); - files.addAll(videoMultipartFiles); + List imageFileKeys = createImageFileKeys(imageCount); + List videoFileKeys = createVideoFileKeys(videoCount); + List fileKeys = new ArrayList<>(imageFileKeys); + fileKeys.addAll(videoFileKeys); + claimReportDto.setFileKeys(fileKeys); + + doReturn(claimReportStrategy).when(reportStrategyFactory).findStrategy(any(ReportType.class)); doReturn(Optional.of(review)).when(reviewRepository).findById(any()); + doReturn(mock(ImageFile.class)).when(imageFileService) + .getAndSaveImageFile(any()); + doReturn(mock(VideoFile.class)).when(videoFileService) + .getAndSaveVideoFile(any()); doReturn(Optional.empty()).when(claimReportRepository) .findByMemberAndReferenceIdAndClaimReportType(any(Member.class), any(), any(ClaimReportType.class)); // when: 리뷰 신고 요청 - reportService.requestClaimReport(memberInfoDto, claimReportDto, files); + reportService.requestClaimReport(memberInfoDto, claimReportDto); // then: 리뷰 신고 요청 검증 verify(claimReportRepository).save(any(ClaimReport.class)); @@ -333,7 +357,7 @@ void requestClaimReportFail_selfReportedMember() { // when: 유저 신고 요청 // then: ErrorCode 검증 assertThatThrownBy( - () -> reportService.requestClaimReport(memberInfoDto, claimReportDto, null)) + () -> reportService.requestClaimReport(memberInfoDto, claimReportDto)) .isInstanceOf(BadRequestException.class) .hasMessage(SELF_REPORT_NOT_ALLOWED.getMessage()); } @@ -349,7 +373,7 @@ void requestClaimReportFail_memberNotFound() { // when: 유저 신고 요청 // then: ErrorCode 검증 assertThatThrownBy( - () -> reportService.requestClaimReport(memberInfoDto, claimReportDto, null)) + () -> reportService.requestClaimReport(memberInfoDto, claimReportDto)) .isInstanceOf(NotFoundException.class) .hasMessage(MEMBER_NOT_FOUND.getMessage()); } @@ -361,18 +385,25 @@ void requestMemberClaimReportSuccess() { ReportRequest.ClaimReportDto claimReportDto = createClaimReportDto(ClaimReportType.MEMBER); int imageCount = 3; int videoCount = 2; - List imageMultipartFiles = createImageMultipartFiles(imageCount); - List videoMultipartFiles = createVideoMultipartFiles(videoCount); - List files = new ArrayList<>(imageMultipartFiles); - files.addAll(videoMultipartFiles); + List imageFileKeys = createImageFileKeys(imageCount); + List videoFileKeys = createVideoFileKeys(videoCount); + + List fileKeys = new ArrayList<>(imageFileKeys); + fileKeys.addAll(videoFileKeys); + claimReportDto.setFileKeys(fileKeys); doReturn(2L).when(memberInfoDto.getMember()).getId(); doReturn(Optional.of(memberInfoDto2.getMember())).when(memberRepository).findById(any()); + doReturn(claimReportStrategy).when(reportStrategyFactory).findStrategy(any(ReportType.class)); + doReturn(mock(ImageFile.class)).when(imageFileService) + .getAndSaveImageFile(any()); + doReturn(mock(VideoFile.class)).when(videoFileService) + .getAndSaveVideoFile(any()); doReturn(Optional.empty()).when(claimReportRepository) .findByMemberAndReferenceIdAndClaimReportType(any(Member.class), any(), any(ClaimReportType.class)); // when: 유저 신고 요청 - reportService.requestClaimReport(memberInfoDto, claimReportDto, files); + reportService.requestClaimReport(memberInfoDto, claimReportDto); // then: 유저 신고 요청 검증 verify(claimReportRepository).save(any(ClaimReport.class));