-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/36 s3 이미지 업로드 기능 구현 #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.somemore.global.configure; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; | ||
| import software.amazon.awssdk.auth.credentials.AwsCredentials; | ||
| import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; | ||
| import software.amazon.awssdk.regions.Region; | ||
| import software.amazon.awssdk.services.s3.S3Client; | ||
| import software.amazon.awssdk.services.s3.presigner.S3Presigner; | ||
|
|
||
| @Configuration | ||
| public class S3Config { | ||
| @Value("${cloud.aws.credentials.access-key}") | ||
| private String accessKey; | ||
|
|
||
| @Value("${cloud.aws.credentials.secret-key}") | ||
| private String secretKey; | ||
|
|
||
| @Value("${cloud.aws.region.static}") | ||
| private String region; | ||
|
|
||
| @Bean | ||
| public AwsCredentials basicAWSCredentials() { | ||
| return AwsBasicCredentials.create(accessKey, secretKey); | ||
| } | ||
|
|
||
| @Bean | ||
| public S3Presigner s3Presigner(AwsCredentials awsCredentials) { | ||
| return S3Presigner.builder() | ||
| .region(Region.of(region)) | ||
| .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public S3Client s3Client(AwsCredentials awsCredentials) { | ||
| return S3Client.builder() | ||
| .region(Region.of(region)) | ||
| .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) | ||
| .build(); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.somemore.global.exception; | ||
|
|
||
| public class ImageUploadException extends RuntimeException{ | ||
|
|
||
| public ImageUploadException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.somemore.imageupload.dto; | ||
|
|
||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| public record ImageUploadRequestDto( | ||
| MultipartFile imageFile | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.somemore.imageupload.service; | ||
|
|
||
| import com.somemore.global.exception.ImageUploadException; | ||
| import com.somemore.imageupload.dto.ImageUploadRequestDto; | ||
| import com.somemore.imageupload.usecase.ImageUploadUseCase; | ||
| import com.somemore.imageupload.util.ImageUploadUtils; | ||
| import com.somemore.imageupload.validator.ImageUploadValidator; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Service; | ||
| import software.amazon.awssdk.core.sync.RequestBody; | ||
| import software.amazon.awssdk.services.s3.S3Client; | ||
| import software.amazon.awssdk.services.s3.model.PutObjectRequest; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| import static com.somemore.global.exception.ExceptionMessage.UPLOAD_FAILED; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Service | ||
| public class ImageUploadService implements ImageUploadUseCase { | ||
|
|
||
| private final S3Client s3Client; | ||
| private final ImageUploadValidator imageUploadValidator; | ||
|
|
||
| @Value("${cloud.aws.s3.bucket}") | ||
| private String bucket; | ||
|
|
||
| @Value("${cloud.aws.s3.base-url}") | ||
| private String baseUrl; | ||
|
|
||
| @Override | ||
| public String uploadImage(ImageUploadRequestDto requestDto) { | ||
| imageUploadValidator.validateFileSize(requestDto.imageFile()); | ||
| imageUploadValidator.validateFileType(requestDto.imageFile()); | ||
|
|
||
| String fileName = ImageUploadUtils.generateUniqueFileName(requestDto.imageFile().getOriginalFilename()); | ||
|
|
||
| PutObjectRequest request = PutObjectRequest.builder() | ||
| .bucket(bucket) | ||
| .key(fileName) | ||
| .contentType(requestDto.imageFile().getContentType()) | ||
| .build(); | ||
|
||
|
|
||
| try { | ||
| s3Client.putObject(request, RequestBody.fromInputStream( | ||
| requestDto.imageFile().getInputStream(), | ||
| requestDto.imageFile().getSize() | ||
| )); | ||
|
||
|
|
||
| return ImageUploadUtils.generateS3Url(baseUrl, fileName); | ||
| } catch (IOException e) { | ||
| throw new ImageUploadException(UPLOAD_FAILED.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.somemore.imageupload.usecase; | ||
|
|
||
| import com.somemore.imageupload.dto.ImageUploadRequestDto; | ||
|
|
||
| public interface ImageUploadUseCase { | ||
| String uploadImage(ImageUploadRequestDto requestDto); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.somemore.imageupload.util; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| public final class ImageUploadUtils { | ||
|
|
||
| private ImageUploadUtils() { | ||
| throw new UnsupportedOperationException("인스턴스화 할 수 없는 클래스 입니다."); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 명시적으로 처리하는 것을 처음 봤어요! 신기하네용
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분도 ENUM으로 처리하면 좋을 것 같습니다
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 좋은 의견 감사합니다 반영하겠습니다 |
||
|
|
||
| public static String generateUniqueFileName(String originalFileName) { | ||
| String uuid = UUID.randomUUID().toString(); | ||
| String fileExtension = extractFileExtension(originalFileName); | ||
| return uuid + fileExtension; | ||
| } | ||
|
|
||
| public static String extractFileExtension(String fileName) { | ||
| return fileName.substring(fileName.lastIndexOf(".")); | ||
| } | ||
|
||
|
|
||
| public static String generateS3Url(String baseUrl, String fileName) { | ||
| return String.format("%s/%s", baseUrl, fileName); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.somemore.imageupload.validator; | ||
|
|
||
| import com.somemore.global.exception.ImageUploadException; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| import static com.somemore.global.exception.ExceptionMessage.*; | ||
|
|
||
| @Component | ||
| public class DefaultImageUploadValidator implements ImageUploadValidator { | ||
|
|
||
| private static final long MAX_FILE_SIZE = 8L * 1024 * 1024; // 8MB | ||
|
|
||
| public void validateFileSize(MultipartFile file) { | ||
| if (file == null || file.isEmpty()) { | ||
| throw new ImageUploadException(EMPTY_FILE.getMessage()); | ||
| } | ||
|
|
||
| if (file.getSize() > MAX_FILE_SIZE) { | ||
| throw new ImageUploadException(FILE_SIZE_EXCEEDED.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| public void validateFileType(MultipartFile file) { | ||
| String contentType = file.getContentType(); | ||
| if (!isAllowedImageType(contentType)) { | ||
| throw new ImageUploadException(INVALID_FILE_TYPE.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| private boolean isAllowedImageType(String contentType) { | ||
| return contentType != null && ( | ||
| contentType.equals("image/jpeg") || | ||
| contentType.equals("image/png") || | ||
| contentType.equals("image/gif") || | ||
| contentType.equals("image/webp") | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.somemore.imageupload.validator; | ||
|
|
||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| public interface ImageUploadValidator { | ||
|
|
||
| void validateFileSize(MultipartFile file); | ||
| void validateFileType(MultipartFile file); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분 조금 어색한데, 괜찮은지 여쭤보고 싶습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
더 명시적인 이름으로 수정해보겠습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
변수명 집어넣다가 실수했네요 수정하겠습니다