From bec54b43cf3f843e0c5555854eeef82c836abaa8 Mon Sep 17 00:00:00 2001 From: DOEKYONG Date: Tue, 15 Aug 2023 00:47:04 +0900 Subject: [PATCH] =?UTF-8?q?#80=20feat=20:=20=ED=8C=8C=EC=9D=BC=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20api=20=EA=B5=AC=ED=98=84,=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- be/issue/build.gradle | 3 ++ .../global/exception/ErrorCode.java | 10 +++++- .../exception/GlobalExceptionHandler.java | 10 +++++- .../issue/controller/IssueController.java | 9 +++++ .../issue/dto/IssueFileResponseDto.java | 10 ++++++ .../issue/service/IssueService.java | 36 +++++++++++++++++++ 6 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueFileResponseDto.java diff --git a/be/issue/build.gradle b/be/issue/build.gradle index 0c9bdcb6f..b7cda9bc6 100644 --- a/be/issue/build.gradle +++ b/be/issue/build.gradle @@ -38,7 +38,10 @@ dependencies { annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' // 패스워드 암호화 implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' + // OAuth 서버 요청 implementation 'org.springframework.boot:spring-boot-starter-webflux' + // 파일 업로드 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java b/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java index 0ee0d3549..f7b5994fc 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/exception/ErrorCode.java @@ -3,6 +3,7 @@ import java.time.DateTimeException; import org.springframework.http.HttpStatus; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; @@ -55,8 +56,12 @@ public enum ErrorCode implements StatusCode { DUPLICATE_OBJECT_FOUND(HttpStatus.BAD_REQUEST, "중복된 항목 선택입니다."), NOT_EXIST_ISSUE(HttpStatus.BAD_REQUEST, "존재하지 않는 이슈입니다."), ALREADY_DELETED_ISSUE(HttpStatus.BAD_REQUEST, "이미 삭제된 이슈입니다."), - NOT_FOUND_ISSUES(HttpStatus.BAD_REQUEST, "이슈를 찾을 수 없습니다."); + NOT_FOUND_ISSUES(HttpStatus.BAD_REQUEST, "이슈를 찾을 수 없습니다."), + // -- [File] -- // + EMPTY_FILE_EXCEPTION(HttpStatus.BAD_REQUEST, "파일을 선택해 주세요."), + FAILED_UPLOAD_FILE(HttpStatus.BAD_REQUEST, "파일 업로드에 실패했습니다."), + MAX_FILE_SIZE(HttpStatus.BAD_REQUEST, "업로드 할 수 있는 최대 크기는 20MB 입니다."); private HttpStatus status; private String message; @@ -96,6 +101,9 @@ public static StatusCode from(RuntimeException e) { if (e instanceof DateTimeException) { return ErrorCode.NOT_FOUND_DATE; } + if (e instanceof MaxUploadSizeExceededException) { + return ErrorCode.MAX_FILE_SIZE; + } return ErrorCode.ILLEGAL_ARGUMENT_EXCEPTION; } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java b/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java index dc49b3445..96daa72aa 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java +++ b/be/issue/src/main/java/codesquad/issueTracker/global/exception/GlobalExceptionHandler.java @@ -8,9 +8,9 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import codesquad.issueTracker.global.common.ApiResponse; -import io.jsonwebtoken.ExpiredJwtException; @RestControllerAdvice public class GlobalExceptionHandler { @@ -47,5 +47,13 @@ public ResponseEntity> handleDateTimeException(DateTimeExcep .body(ApiResponse.fail(statusCode.getStatus(), statusCode.getMessage())); } + @ExceptionHandler(MaxUploadSizeExceededException.class) + protected ResponseEntity> handleMaxUploadSizeExceededException( + MaxUploadSizeExceededException e) { + StatusCode statusCode = ErrorCode.from(e); + return ResponseEntity.status(statusCode.getStatus()) + .body(ApiResponse.fail(statusCode.getStatus(), statusCode.getMessage())); + } + } diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java b/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java index cbdc4958f..28b266ee7 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/controller/IssueController.java @@ -14,9 +14,12 @@ 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; import codesquad.issueTracker.global.common.ApiResponse; +import codesquad.issueTracker.issue.dto.IssueFileResponseDto; import codesquad.issueTracker.issue.dto.IssueLabelResponseDto; import codesquad.issueTracker.issue.dto.IssueMilestoneResponseDto; import codesquad.issueTracker.issue.dto.IssueOptionResponseDto; @@ -129,4 +132,10 @@ public ApiResponse patchMilestone(@PathVariable Long id, @RequestBody Mo issueService.modifyMilestone(id, request); return ApiResponse.success(SUCCESS.getStatus(), SUCCESS.getMessage()); } + + @PostMapping("/upload") + public ApiResponse uploadFile(@RequestPart(value = "file") MultipartFile multipartFile) { + IssueFileResponseDto response = issueService.uploadImg(multipartFile); + return ApiResponse.success(SUCCESS.getStatus(), response); + } } diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueFileResponseDto.java b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueFileResponseDto.java new file mode 100644 index 000000000..ed0c8050c --- /dev/null +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/dto/IssueFileResponseDto.java @@ -0,0 +1,10 @@ +package codesquad.issueTracker.issue.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class IssueFileResponseDto { + private String imgUrl; +} diff --git a/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java b/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java index 06c2ab767..468f70a0a 100644 --- a/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java +++ b/be/issue/src/main/java/codesquad/issueTracker/issue/service/IssueService.java @@ -1,17 +1,27 @@ package codesquad.issueTracker.issue.service; +import java.io.IOException; +import java.io.InputStream; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; import codesquad.issueTracker.global.common.Status; import codesquad.issueTracker.global.exception.CustomException; import codesquad.issueTracker.global.exception.ErrorCode; import codesquad.issueTracker.issue.domain.Issue; +import codesquad.issueTracker.issue.dto.IssueFileResponseDto; import codesquad.issueTracker.issue.dto.IssueLabelResponseDto; import codesquad.issueTracker.issue.dto.IssueMilestoneResponseDto; import codesquad.issueTracker.issue.dto.IssueOptionResponseDto; @@ -43,10 +53,13 @@ @Transactional(readOnly = true) public class IssueService { + @Value("${cloud.aws.s3.bucket}") + private String bucketName; private final IssueRepository issueRepository; private final LabelService labelService; private final UserService userService; private final MilestoneService milestoneService; + private final AmazonS3Client amazonS3Client; @Transactional public Long save(IssueWriteRequestDto request, Long id) { @@ -239,4 +252,27 @@ public Long modifyMilestone(Long id, ModifyIssueMilestoneDto request) { } return issueRepository.updateMilestone(id, milestoneId); } + + public IssueFileResponseDto uploadImg(MultipartFile multipartFile) { + validateFileExists(multipartFile); + String fileName = multipartFile.getOriginalFilename(); + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(multipartFile.getContentType()); + + try (InputStream inputStream = multipartFile.getInputStream()) { + amazonS3Client.putObject(new PutObjectRequest(bucketName, fileName, inputStream, objectMetadata) + .withCannedAcl(CannedAccessControlList.PublicRead)); + } catch (IOException e) { + throw new CustomException(ErrorCode.FAILED_UPLOAD_FILE); + } + String url = amazonS3Client.getUrl(bucketName, fileName).toString(); + IssueFileResponseDto response = new IssueFileResponseDto(url); + return response; + } + + private void validateFileExists(MultipartFile multipartFile) { + if (multipartFile.isEmpty()) { + throw new CustomException(ErrorCode.EMPTY_FILE_EXCEPTION); + } + } }