Skip to content
Closed
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
13 changes: 7 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// mysql 사용시 주석 해제
// mysql
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'

Expand All @@ -44,8 +44,11 @@ dependencies {
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testRuntimeOnly 'com.h2database:h2'

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// 스웨거
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'

// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
Expand All @@ -56,11 +59,9 @@ dependencies {
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
implementation(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.1.1"))
implementation("io.awspring.cloud:spring-cloud-aws-starter-s3")

// oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ public enum ErrorStatus implements BaseErrorCode {
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH406", "유효하지 않은 OAuth 제공자입니다."),

AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."),

AWS_METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "AWS405", "해당 method는 허용되지 않습니다."),
MUSIC_NOT_FOUND(HttpStatus.BAD_REQUEST, "MUSIC400", "음원을 찾을 수 없습니다."),

LIKE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LIKE400", "해당 좋아요를 찾을 수 없습니다.");

private final HttpStatus httpStatus;
Expand Down
29 changes: 14 additions & 15 deletions src/main/java/umc/codeplay/config/AWSConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,30 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class AWSConfig {

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

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

@Value("${cloud.aws.region.static}")
@Value("${spring.cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
final BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);

return (AmazonS3Client)
AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
public S3Presigner s3Presigner() {
return S3Presigner.builder()
.region(Region.of(region))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.build();
}
}
42 changes: 42 additions & 0 deletions src/main/java/umc/codeplay/controller/FileController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package umc.codeplay.controller;

import org.springframework.web.bind.annotation.*;

import lombok.RequiredArgsConstructor;

import io.swagger.v3.oas.annotations.Operation;
import software.amazon.awssdk.http.SdkHttpMethod;
import umc.codeplay.apiPayLoad.ApiResponse;
import umc.codeplay.dto.FileResponseDTO;
import umc.codeplay.service.FileService;

import static umc.codeplay.service.FileService.buildFilename;

@RestController
@RequestMapping("/files")
@RequiredArgsConstructor
public class FileController {

private final FileService fileService;

@Operation(summary = "Download용 Presigned URL 생성", description = "다운로드를 위한 Presigned URL 생성")
@GetMapping("/presigned/download")
public ApiResponse<FileResponseDTO.DownloadFile> getUrl(
@RequestParam(value = "filename") String fileName) {
String preSignedUrl = fileService.generatePreSignedUrl(fileName, SdkHttpMethod.GET);
FileResponseDTO.DownloadFile result = new FileResponseDTO.DownloadFile(preSignedUrl);

return ApiResponse.onSuccess(result);
}

@Operation(summary = "Upload용 Presigned URL 생성", description = "업로드를 위한 Presigned URL 생성")
@PostMapping("/presigned/upload")
public ApiResponse<FileResponseDTO.UploadFile> generateUrl(
@RequestParam(value = "filename") String fileName) {
String newFileName = buildFilename(fileName);
String url = fileService.generatePreSignedUrl(newFileName, SdkHttpMethod.PUT);
FileResponseDTO.UploadFile result = new FileResponseDTO.UploadFile(newFileName, url);

return ApiResponse.onSuccess(result);
}
}
15 changes: 3 additions & 12 deletions src/main/java/umc/codeplay/controller/HealthCheck.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
package umc.codeplay.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

import io.swagger.v3.oas.annotations.Hidden;
import umc.codeplay.service.S3Service;

@Hidden
@RestController
@RequestMapping("/health")
@RequiredArgsConstructor
public class HealthCheck {

private final S3Service s3Service;

// 연결 확인 용
@GetMapping("")
public ResponseEntity<String> healthCheck() {
return ResponseEntity.ok("UMC 7th CodePlay Well Connected!");
}

// s3 업로드 테스트 용
@PostMapping("/s3")
public ResponseEntity<String> s3HealthCheck(@RequestPart(value = "file") MultipartFile file) {
final String s3Url = s3Service.uploadFile(file);
return ResponseEntity.ok("S3 FIle is uploaded! : " + s3Url);
}
}
3 changes: 3 additions & 0 deletions src/main/java/umc/codeplay/domain/enums/AccessType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package umc.codeplay.domain.enums;

public enum AccessType {}
20 changes: 20 additions & 0 deletions src/main/java/umc/codeplay/dto/FileResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package umc.codeplay.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

public class FileResponseDTO {

@Getter
@AllArgsConstructor
public static class DownloadFile {
private String s3URL;
}

@Getter
@AllArgsConstructor
public static class UploadFile {
private String newFileName;
private String s3URL;
}
}
83 changes: 83 additions & 0 deletions src/main/java/umc/codeplay/service/FileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package umc.codeplay.service;

import java.text.Normalizer;
import java.time.Duration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
import umc.codeplay.apiPayLoad.code.status.ErrorStatus;
import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler;

@Service
@RequiredArgsConstructor
public class FileService {

@Value("${spring.cloud.aws.s3.bucket}")
private String bucketName;

private final S3Presigner s3Presigner;

// 타임스탬프_파일명 형식으로 파일 이름 저장
public static String buildFilename(String filename) {
return String.format("%s_%s", System.currentTimeMillis(), sanitizeFileName(filename));
}

// 특수 문자나 공백 등을 정리
private static String sanitizeFileName(String fileName) {
String normalizedFileName = Normalizer.normalize(fileName, Normalizer.Form.NFC);
return normalizedFileName.replaceAll("\\s+", "_").replaceAll("[^a-zA-Z0-9.\\-_]", "");
}

// 파일 업로드(HTTP PUT) 또는 다운로드(HTTP GET)를 위한 Presigned URL 생성
public String generatePreSignedUrl(String fileName, SdkHttpMethod method) {

return switch (method) {
case GET -> generateGetPresignedUrl(fileName);
case PUT -> generatePutPresignedUrl(fileName);
default -> throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE);
};
}

// S3에서 파일을 다운로드할 수 있는 Presigned URL 생성
private String generateGetPresignedUrl(String fileName) {
GetObjectRequest getObjectRequest =
GetObjectRequest.builder().bucket(bucketName).key(fileName).build();

GetObjectPresignRequest presignRequest =
GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(60))
.getObjectRequest(getObjectRequest)
.build();

PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest);
return presignedRequest.url().toString();
}

// S3에 파일을 업로드할 수 있는 Presigned URL 생성
private String generatePutPresignedUrl(String fileName) {
PutObjectRequest putObjectRequest =
PutObjectRequest.builder().bucket(bucketName).key(fileName).build();

PutObjectPresignRequest presignRequest =
PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(60))
.putObjectRequest(putObjectRequest)
.build();

PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
return presignedRequest.url().toString();
}

// TODO: 필요시 직접 업로드 방법 구현 필요
}
87 changes: 0 additions & 87 deletions src/main/java/umc/codeplay/service/S3Service.java

This file was deleted.

18 changes: 9 additions & 9 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ spring:
use_sql_comments: true
default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화)

cloud:
aws:
s3:
bucket: ${S3_BUCKET}
stack.auto: false
region.static: ${AWS_DEFAULT_REGION}
credentials:
accessKey: ${AWS_ACCESS_KEY_ID}
secretKey: ${AWS_SECRET_ACCESS_KEY}
cloud:
aws:
s3:
bucket: ${S3_BUCKET}
credentials:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
region:
static: ${AWS_DEFAULT_REGION}

jwt:
secret: ${JWT_SECRET}
Expand Down
Loading
Loading