From d874a7d2f852f15fc279573d01dbd1e9dc909c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=84=B8=EC=A4=80?= <74056843+sejoon00@users.noreply.github.com> Date: Fri, 14 Feb 2025 02:56:45 +0900 Subject: [PATCH] =?UTF-8?q?[feat/#50]=20presigned=20url=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ImageUploadController.java | 39 +++++++++++++++++++ .../domain/problem/ProblemImageType.java | 19 +++++++++ .../problem/repository/ProblemRepository.java | 6 +++ .../problem/service/ImageUploadService.java | 39 +++++++++++++++++++ .../global/error/exception/ErrorCode.java | 8 ++-- .../moplus_server/global/utils/s3/S3Util.java | 22 +++++++++++ 6 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java create mode 100644 src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java new file mode 100644 index 0000000..66bfc71 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/controller/ImageUploadController.java @@ -0,0 +1,39 @@ +package com.moplus.moplus_server.domain.problem.controller; + +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemImageType; +import com.moplus.moplus_server.domain.problem.service.ImageUploadService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "이미지 업로드 API", description = "이미지 업로드 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/images") +public class ImageUploadController { + + private final ImageUploadService imageUploadService; + + @Operation(summary = "이미지 업로드를 위한 presigned URL 발급") + @GetMapping("/problem/{problemId}/presigned-url") + public ResponseEntity getProblemImagePresignedUrl( + @PathVariable("problemId") String problemId, + @RequestParam(value = "image-type") ProblemImageType imageType) { + String presignedUrl = imageUploadService.generateProblemImagePresignedUrl(problemId, imageType); + return ResponseEntity.ok(presignedUrl); + } + + @Operation(summary = "이미지 업로드 완료 후 URL 조회") + @GetMapping("/{fileName}") + public ResponseEntity getImageUrl( + @PathVariable("fileName") String fileName) { + String imageUrl = imageUploadService.getImageUrl(fileName); + return ResponseEntity.ok(imageUrl); + } +} diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java new file mode 100644 index 0000000..305bb72 --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/domain/problem/ProblemImageType.java @@ -0,0 +1,19 @@ +package com.moplus.moplus_server.domain.problem.domain.problem; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ProblemImageType { + MAIN_PROBLEM("main-problem", "문제 이미지"), + MAIN_ANALYSIS("main-analysis", "분석 이미지"), + MAIN_HANDWRITING_EXPLANATION("main-handwriting-explanation", "손글씨 설명 이미지"), + READING_TIP("reading-tip", "읽기 팁 이미지"), + SENIOR_TIP("senior-tip", "선배 팁 이미지"), + PRESCRIPTION("prescription", "처방전 이미지"), + CHILD_PROBLEM("child-problem", "하위 문제 이미지"); + + private final String type; + private final String description; +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java index 04e7d94..6f62f77 100644 --- a/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java +++ b/src/main/java/com/moplus/moplus_server/domain/problem/repository/ProblemRepository.java @@ -16,6 +16,12 @@ default void existsByIdElseThrow(Long id) { } } + default void existsByProblemAdminIdElseThrow(ProblemAdminId problemAdminId) { + if (!existsByProblemAdminId(problemAdminId)) { + throw new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND); + } + } + default Problem findByIdElseThrow(Long id) { return findById(id).orElseThrow(() -> new NotFoundException(ErrorCode.PROBLEM_NOT_FOUND)); } diff --git a/src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java b/src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java new file mode 100644 index 0000000..9617adf --- /dev/null +++ b/src/main/java/com/moplus/moplus_server/domain/problem/service/ImageUploadService.java @@ -0,0 +1,39 @@ +package com.moplus.moplus_server.domain.problem.service; + +import com.amazonaws.HttpMethod; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemAdminId; +import com.moplus.moplus_server.domain.problem.domain.problem.ProblemImageType; +import com.moplus.moplus_server.domain.problem.repository.ProblemRepository; +import com.moplus.moplus_server.global.utils.s3.S3Util; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ImageUploadService { + + private static final String PROBLEM_IMAGE_PREFIX = "problems/"; + private final S3Util s3Util; + private final ProblemRepository problemRepository; + + public String generateProblemImagePresignedUrl(String problemId, ProblemImageType imageType) { + problemRepository.existsByProblemAdminIdElseThrow(new ProblemAdminId(problemId)); + String fileName = generateProblemImageFileName(problemId, imageType); + return s3Util.getS3PresignedUrl(fileName, HttpMethod.PUT); + } + + private String generateProblemImageFileName(String problemId, ProblemImageType imageType) { + String uuid = UUID.randomUUID().toString(); + return String.format("%s%s/%s/%s.jpg", + PROBLEM_IMAGE_PREFIX, + problemId, + imageType.getType(), + uuid + ); + } + + public String getImageUrl(String fileName) { + return s3Util.getS3ObjectUrl(fileName); + } +} \ No newline at end of file diff --git a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java index 25f5519..199a216 100644 --- a/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java +++ b/src/main/java/com/moplus/moplus_server/global/error/exception/ErrorCode.java @@ -28,10 +28,10 @@ public enum ErrorCode { PRACTICE_TEST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 모의고사를 찾을 수 없습니다"), //문항 - PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 문제를 찾을 수 없습니다"), - PROBLEM_ALREADY_EXIST(HttpStatus.CONFLICT, "해당 문제는 이미 존재합니다"), - INVALID_MULTIPLE_CHOICE_ANSWER(HttpStatus.BAD_REQUEST, "객관식 문제의 정답은 1~5 사이의 숫자여야 합니다"), - INVALID_SHORT_NUMBER_ANSWER(HttpStatus.BAD_REQUEST, "주관식 문제의 정답은 0~999 사이의 숫자여야 합니다"), + PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 문항을 찾을 수 없습니다"), + PROBLEM_ALREADY_EXIST(HttpStatus.CONFLICT, "해당 문항는 이미 존재합니다"), + INVALID_MULTIPLE_CHOICE_ANSWER(HttpStatus.BAD_REQUEST, "객관식 문항의 정답은 1~5 사이의 숫자여야 합니다"), + INVALID_SHORT_NUMBER_ANSWER(HttpStatus.BAD_REQUEST, "주관식 문항의 정답은 0~999 사이의 숫자여야 합니다"), INVALID_CONFIRM_PROBLEM(HttpStatus.BAD_REQUEST, "유효하지 않은 문항들 : "), INVALID_DIFFICULTY(HttpStatus.BAD_REQUEST, "난이도는 1~10 사이의 숫자여야 합니다"), diff --git a/src/main/java/com/moplus/moplus_server/global/utils/s3/S3Util.java b/src/main/java/com/moplus/moplus_server/global/utils/s3/S3Util.java index d2abde8..68189f1 100644 --- a/src/main/java/com/moplus/moplus_server/global/utils/s3/S3Util.java +++ b/src/main/java/com/moplus/moplus_server/global/utils/s3/S3Util.java @@ -1,10 +1,13 @@ package com.moplus.moplus_server.global.utils.s3; +import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.PutObjectRequest; import com.moplus.moplus_server.global.error.exception.ErrorCode; import com.moplus.moplus_server.global.error.exception.NotFoundException; import java.io.File; +import java.util.Date; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -29,5 +32,24 @@ public String getS3ObjectUrl(String fileName) { return amazonS3.getUrl(bucketName, fileName).toString(); } + public String getS3PresignedUrl(String fileName, HttpMethod httpMethod) { + + GeneratePresignedUrlRequest generatePresignedUrlRequest = + new GeneratePresignedUrlRequest(bucketName, fileName) + .withMethod(httpMethod) + .withExpiration(getPreSignedUrlExpiration()); + + return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString(); + } + + private Date getPreSignedUrlExpiration() { + final int PRESIGNED_EXPIRATION = 1000 * 60 * 30; //30분 + + Date expiration = new Date(); + var expTimeMillis = expiration.getTime(); + expTimeMillis += PRESIGNED_EXPIRATION; + expiration.setTime(expTimeMillis); + return expiration; + } }