Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ jobs:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
FRONT_URL: ${{secrets.FRONT_URL}}
BACK_URL: ${{secrets.BACK_URL}}
BUCKET_NAME: ${{secrets.BUCKET_NAME}}
BUCKET_REGION: ${{secrets.BUCKET_REGION}}
IMG_BASE_URL: ${{secrets.BASE_URL}}
S3_ACCESS_KEY: ${{secrets.S3_ACCESS_KEY}}
S3_SECRET_KEY: ${{secrets.S3_SECRET_KEY}}

steps:
- name: Github Repository 파일 불러오기
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ jobs:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
FRONT_URL: ${{secrets.FRONT_URL}}
BACK_URL: ${{secrets.BACK_URL}}
BUCKET_NAME: ${{secrets.BUCKET_NAME}}
BUCKET_REGION: ${{secrets.BUCKET_REGION}}
IMG_BASE_URL: ${{secrets.BASE_URL}}
S3_ACCESS_KEY: ${{secrets.S3_ACCESS_KEY}}
S3_SECRET_KEY: ${{secrets.S3_SECRET_KEY}}


steps:
Expand Down
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

// AWS
implementation(platform("software.amazon.awssdk:bom:2.29.20"))
implementation("software.amazon.awssdk:s3")

// Web Layer
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -61,7 +64,6 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.6.0'


//test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
45 changes: 45 additions & 0 deletions src/main/java/com/somemore/global/configure/S3Config.java
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
Expand Up @@ -14,6 +14,11 @@ public enum ExceptionMessage {
NOT_EXISTS_LOCATION("존재하지 않는 위치 ID 입니다."),
NOT_EXISTS_RECRUIT_BOARD("존재하지 않는 봉사 모집글 ID 입니다."),
UNAUTHORIZED_RECRUIT_BOARD("자신이 작성한 봉사 모집글이 아닙니다."),
UPLOAD_FAILED("파일 업로드에 실패했습니다."),
INVALID_FILE_TYPE("지원하지 않는 파일 형식입니다."),
FILE_SIZE_EXCEEDED("파일 크기가 허용된 한도를 초과했습니다."),
EMPTY_FILE("파일이 존재하지 않습니다."),
INSTANTIATION_NOT_ALLOWED("인스턴스화 할 수 없는 클래스 입니다.")
;

private final String message;
Expand Down
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
Expand Up @@ -2,10 +2,13 @@


import com.somemore.global.exception.BadRequestException;
import com.somemore.global.exception.ImageUploadException;
import org.springframework.data.crossstore.ChangeSetPersister;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;


Expand All @@ -25,4 +28,15 @@ ProblemDetail handleBadRequestException(final BadRequestException e) {
return problemDetail;
}

@ExceptionHandler(ImageUploadException.class)
ProblemDetail handleImageUploadException(final ImageUploadException e) {

ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());

problemDetail.setTitle("이미지 업로드 실패");
problemDetail.setDetail("업로드 중 문제가 발생했습니다. 파일 크기나 형식이 올바른지 확인해 주세요.");

return problemDetail;
}

}
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,63 @@
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 org.springframework.web.multipart.MultipartFile;
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());

try {
return uploadToS3(requestDto.imageFile());
} catch (IOException e) {
throw new ImageUploadException(UPLOAD_FAILED.getMessage());
}
}

private String uploadToS3(MultipartFile file) throws IOException {
String fileName = ImageUploadUtils.generateUniqueFileName(file.getOriginalFilename());

PutObjectRequest request = createPutObjectRequest(file, fileName);

s3Client.putObject(request, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));

return ImageUploadUtils.generateS3Url(baseUrl, fileName);
}

private PutObjectRequest createPutObjectRequest(MultipartFile file, String fileName) {
return PutObjectRequest.builder()
.bucket(bucket)
.key(fileName)
.contentType(file.getContentType())
.build();
}

}
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);
}
27 changes: 27 additions & 0 deletions src/main/java/com/somemore/imageupload/util/ImageUploadUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.somemore.imageupload.util;

import java.util.UUID;

import static com.somemore.global.exception.ExceptionMessage.INSTANTIATION_NOT_ALLOWED;

public class ImageUploadUtils {

private ImageUploadUtils() {
throw new UnsupportedOperationException(INSTANTIATION_NOT_ALLOWED.getMessage());
}

public static String generateUniqueFileName(String originalFileName) {
String uuid = UUID.randomUUID().toString();
String fileExtension = extractFileExtension(originalFileName);
return uuid + fileExtension;
}

private 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);
}
20 changes: 20 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ app:
front-url: ${FRONT_URL}
back-url: ${BACK_URL}

# AWS S3
cloud:
aws:
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
region:
static: ${BUCKET_REGION}
s3:
bucket: ${BUCKET_NAME}
base-url: ${IMG_BASE_URL}
stack:
auto: false

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
Expand Down Expand Up @@ -47,6 +61,12 @@ spring:
locale: ko_KR
locale-resolver: fixed

servlet:
multipart:
max-file-size: 8MB
max-request-size: 8MB


#swagger
springdoc:
swagger-ui:
Expand Down
Loading