Skip to content

Commit

Permalink
[feat] : 파일 업로드 API 구현 (#10)
Browse files Browse the repository at this point in the history
* [chore] : S3 설정 파일 추가

* [feat] : S3 관련 에러 코드 정의

* [feat] : S3 파일(동영상, 이미지) 업로드 비즈니스 로직 작성

* [feat] : S3 컨트롤러 DTO 작성

* [feat] : s3 파일 업로드 컨트롤러 메서드 작성

* [fix] : s3 파일 확장자 집합에 . 포함

* [style] : 코드 리포멧팅

* [refactor] : 불필요한 출력문 삭제

* [feat] : 파일 오류 메시지 수정

* [feat] : jpaAuditing 설정 추가
  • Loading branch information
hyun2371 authored Aug 1, 2024
1 parent 70a0ee4 commit ad20e37
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 0 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

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

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.gongmuin.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
33 changes: 33 additions & 0 deletions src/main/java/com/dnd/gongmuin/common/config/S3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.dnd.gongmuin.common.config;

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

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

@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 AmazonS3 amazonS3Client() {
AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/dnd/gongmuin/s3/controller/S3Controller.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.dnd.gongmuin.s3.controller;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.dnd.gongmuin.s3.dto.ImagesUploadRequest;
import com.dnd.gongmuin.s3.dto.ImagesUploadResponse;
import com.dnd.gongmuin.s3.dto.VideoUploadRequest;
import com.dnd.gongmuin.s3.dto.VideoUploadResponse;
import com.dnd.gongmuin.s3.service.S3Service;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

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

private final S3Service s3Service;

@PostMapping("/images")
public ResponseEntity<ImagesUploadResponse> uploadImages(
@ModelAttribute @Valid ImagesUploadRequest request
) {
List<String> imageUrls = s3Service.uploadImages(request.imageFiles());
return ResponseEntity.ok(ImagesUploadResponse.from(imageUrls));
}

@PostMapping("/videos")
public ResponseEntity<VideoUploadResponse> uploadVideo(
@ModelAttribute @Valid VideoUploadRequest request
) {
String videoUrl = s3Service.uploadVideo(request.videoFile());
return ResponseEntity.ok(VideoUploadResponse.from(videoUrl));
}

}
16 changes: 16 additions & 0 deletions src/main/java/com/dnd/gongmuin/s3/dto/ImagesUploadRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dnd.gongmuin.s3.dto;

import java.util.List;

import org.springframework.web.multipart.MultipartFile;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record ImagesUploadRequest(

@NotNull(message = "이미지 파일은 필수 입력 항목입니다.")
@Size(min = 1, max = 10, message = "이미지는 1장 이상 10장 이하로 선택하세요.")
List<MultipartFile> imageFiles
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/dnd/gongmuin/s3/dto/ImagesUploadResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.gongmuin.s3.dto;

import java.util.List;

public record ImagesUploadResponse(
List<String> imageUrls
) {
public static ImagesUploadResponse from(List<String> imageUrls) {
return new ImagesUploadResponse(imageUrls);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/dnd/gongmuin/s3/dto/VideoUploadRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.gongmuin.s3.dto;

import org.springframework.web.multipart.MultipartFile;

import jakarta.validation.constraints.NotNull;

public record VideoUploadRequest(
@NotNull(message = "비디오 파일은 필수 입력 항목입니다.")
MultipartFile videoFile
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.gongmuin.s3.dto;

public record VideoUploadResponse(
String videoUrl
) {
public static VideoUploadResponse from(String videoUrl) {
return new VideoUploadResponse(videoUrl);
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/dnd/gongmuin/s3/exception/S3ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dnd.gongmuin.s3.exception;

import com.dnd.gongmuin.common.exception.ErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum S3ErrorCode implements ErrorCode {
EMPTY_FILE_NAME("원본 파일명은 필수입니다.", "S3_001"),
INVALID_FILE_EXTENSION("잘못된 되었거나 지원하지 않는 파일 형식입니다.", "S3_002"),

FAILED_TO_UPLOAD("파일을 업로드하는데 실패했습니다.", "S3_003");


private final String message;
private final String code;
}
81 changes: 81 additions & 0 deletions src/main/java/com/dnd/gongmuin/s3/service/S3Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.dnd.gongmuin.s3.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

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

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.s3.exception.S3ErrorCode;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class S3Service {

private static final String IMAGE_FOLDER_NAME = "images";
private static final String VIDEO_FOLDER_NAME = "videos";
private static final Set<String> ALLOWED_FILE_EXTENSIONS = Set.of(".jpg", ".jpeg", ".png", ".mp4", ".avi", ".mov");
private final AmazonS3 amazonS3;

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

public List<String> uploadImages(List<MultipartFile> multipartFileList) {
List<String> fileUrls = new ArrayList<>();

for (MultipartFile multipartFile : multipartFileList) {
fileUrls.add(uploadFile(multipartFile, IMAGE_FOLDER_NAME));
}
return fileUrls;
}

public String uploadVideo(MultipartFile multipartFile) {
return uploadFile(multipartFile, VIDEO_FOLDER_NAME);
}

private String uploadFile(MultipartFile multipartFile, String folderName) {
ObjectMetadata metadata = new ObjectMetadata();
String fileName = createFileName(multipartFile.getOriginalFilename());
metadata.setContentLength(multipartFile.getSize());
metadata.setContentType(multipartFile.getContentType());

String bucketName = bucket + "/" + folderName;

try {
amazonS3.putObject(
new PutObjectRequest(bucketName, fileName, multipartFile.getInputStream(), metadata)
);

} catch (IOException exception) {
throw new ValidationException(S3ErrorCode.FAILED_TO_UPLOAD);
}
return amazonS3.getUrl(bucketName, fileName).toString();
}

private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}

private String getFileExtension(String fileName) {
if (Objects.isNull(fileName) || fileName.isBlank()) {
throw new ValidationException(S3ErrorCode.EMPTY_FILE_NAME);
}
String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (!ALLOWED_FILE_EXTENSIONS.contains(extension)) {
throw new ValidationException(S3ErrorCode.INVALID_FILE_EXTENSION);
}
return extension;
}

}

0 comments on commit ad20e37

Please sign in to comment.