Skip to content

Commit

Permalink
Merge pull request #30 from Stumeet/dev ✨ [STMT-146] S3를 활용한 사용자 프로필 …
Browse files Browse the repository at this point in the history
…이미지 업로드 기능 구현 (#29)

✨ [STMT-146] S3를 활용한 사용자 프로필 이미지 업로드 기능 구현 (#29)
  • Loading branch information
05AM authored Feb 23, 2024
2 parents bc13c09 + 0c835f5 commit c9b49cc
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 4 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ dependencies {
implementation 'org.flywaydb:flyway-mysql'
implementation 'org.flywaydb:flyway-core'

implementation(platform("software.amazon.awssdk:bom:2.21.1"))
implementation 'software.amazon.awssdk:s3'

annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.stumeet.server.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface StorageAdapter {

@AliasFor(annotation = Component.class)
String value() default "";
}
42 changes: 42 additions & 0 deletions src/main/java/com/stumeet/server/common/config/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.stumeet.server.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import lombok.RequiredArgsConstructor;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.regions.Region;

@Configuration
@RequiredArgsConstructor
public class AwsS3Config {

@Value("${spring.cloud.config.server.awss3.region}")
private String region;

@Value("${spring.cloud.config.server.awss3.credentials.access-key}")
private String accessKey;

@Value("${spring.cloud.config.server.awss3.credentials.secret-key}")
private String secretKey;

private AwsBasicCredentials awsBasicCredentials() {
return AwsBasicCredentials.create(accessKey, secretKey);
}

@Bean
public StaticCredentialsProvider staticCredentialsProvider() {
return StaticCredentialsProvider.create(awsBasicCredentials());
}

@Bean
public S3Client s3Client() {
return S3Client.builder()
.credentialsProvider(staticCredentialsProvider())
.region(Region.of(region))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ public enum ErrorCode {
METHOD_ARGUMENT_NOT_VALID_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 검증되지 않은 값 입니다."),
METHOD_ARGUMENT_TYPE_MISMATCH_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값의 타입이 잘못되었습니다."),
INVALID_FORMAT_EXCEPTION(HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않은 데이터입니다."),
INVALID_IMAGE_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 이미지 파일입니다."),
INVALID_FILE_EXTENSION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 파일 확장자입니다."),


/*
500 - INTERNAL SERVER ERROR
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부에서 에러가 발생하였습니다.")
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부에서 에러가 발생하였습니다."),
UPLOAD_FILE_FAIL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패하였습니다.")
;

private final HttpStatus httpStatus;
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/com/stumeet/server/common/util/FileUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.stumeet.server.common.util;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;

import com.stumeet.server.common.exception.model.BusinessException;
import com.stumeet.server.common.response.ErrorCode;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FileUtil {

private static final Set<String> VALID_CONTENT_TYPES = new HashSet<>();

static {
VALID_CONTENT_TYPES.add("jpg");
VALID_CONTENT_TYPES.add("jpeg");
VALID_CONTENT_TYPES.add("png");
}

public static String getContentType(String fileName) {
if (fileName.isEmpty()) {
throw new BusinessException(ErrorCode.INVALID_IMAGE_EXCEPTION);
}

String contentType = fileName
.substring(fileName.lastIndexOf(".") + 1)
.toLowerCase(Locale.ROOT);

if (!VALID_CONTENT_TYPES.contains(contentType)) {
throw new BusinessException(ErrorCode.INVALID_FILE_EXTENSION_EXCEPTION);
}

return contentType;
}

public static String createFileName(String directoryPath, String fileName) {
String dateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

return String.format("%s/%s%s-%s", directoryPath, dateTime, UUID.randomUUID(), fileName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.stumeet.server.file.adapter.out;

import java.io.IOException;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.multipart.MultipartFile;

import com.stumeet.server.common.annotation.StorageAdapter;
import com.stumeet.server.common.exception.model.BusinessException;
import com.stumeet.server.common.response.ErrorCode;
import com.stumeet.server.common.util.FileUtil;
import com.stumeet.server.file.application.port.out.FileCommandPort;
import com.stumeet.server.file.application.port.out.FileUrl;

import lombok.RequiredArgsConstructor;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@StorageAdapter
@RequiredArgsConstructor
public class S3ImageStorageAdapter implements FileCommandPort {

@Value("${spring.cloud.config.server.awss3.bucket}")
private String bucket;

@Value("${spring.cloud.config.server.awss3.endpoint}")
private String endpoint;

private final S3Client s3Client;

@Override
public FileUrl uploadImageFile(MultipartFile multipartFile, String directoryPath) {
String originalFileName = multipartFile.getOriginalFilename();
String key = FileUtil.createFileName(directoryPath, originalFileName);

PutObjectRequest objectRequest = PutObjectRequest.builder()
.contentType(FileUtil.getContentType(originalFileName))
.bucket(bucket)
.key(key)
.build();

try {
s3Client.putObject(
objectRequest,
RequestBody
.fromInputStream(
multipartFile.getInputStream(),
multipartFile.getSize()));
} catch (IOException e) {
throw new BusinessException(ErrorCode.UPLOAD_FILE_FAIL_ERROR);
}

return getS3FileUrl(key);
}

@Override
public FileUrl uploadImageFiles(List<MultipartFile> multipartFileList, String directoryPath) {
return null;
}

private FileUrl getS3FileUrl(String key) {
return new FileUrl(String.format("%s/%s", endpoint, key));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
@RequiredArgsConstructor
public class FileUploadService implements FileUploadUseCase {

private final static String USER_PROFILE_IMAGE_DIRECTORY_PATH = "/user/%d/profile";
private final static String STUDY_ACTIVITY_IMAGE_DIRECTORY_PATH = "/study/%d/activity";
private final static String USER_PROFILE_IMAGE_DIRECTORY_PATH = "user/%d/profile";
private final static String STUDY_ACTIVITY_IMAGE_DIRECTORY_PATH = "study/%d/activity";

private final FileCommandPort fileCommandPort;

@Override
Expand Down
12 changes: 11 additions & 1 deletion src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,21 @@ spring:
port: 6379
jpa:
hibernate:
ddl-auto: create
ddl-auto: update
properties:
hibernate:
show_sql: true
format_sql: true
cloud:
config:
server:
awss3:
region: ${S3_REGION}
bucket: ${S3_BUCKET_NAME}
endpoint: ${S3_ENDPOINT}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}

jwt:
issuer: ${JWT_ISSUER}
Expand Down
10 changes: 10 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ spring:
hibernate:
show_sql: true
dialect: org.hibernate.dialect.MySQLDialect
cloud:
config:
server:
awss3:
region: ${S3_REGION}
bucket: ${S3_BUCKET_NAME}
endpoint: ${S3_ENDPOINT}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}

jwt:
issuer: ${JWT_ISSUER}
Expand Down

0 comments on commit c9b49cc

Please sign in to comment.