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
3 changes: 3 additions & 0 deletions hackathon/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ dependencies {
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//s3 설정
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}

// QueryDSL 설정 (수동 방식)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package nerdinary.hackathon.global.awss3.controller;

import lombok.RequiredArgsConstructor;
import nerdinary.hackathon.global.awss3.service.AwsS3Service;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/file")
public class AmazonS3Controller {

private final AwsS3Service awsS3Service;

// 파일 업로드
@PostMapping
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile multipartFile) {
String fileUrl = awsS3Service.uploadFile(multipartFile);
return ResponseEntity.ok(fileUrl);
}

// 파일 삭제
@DeleteMapping
public ResponseEntity<String> deleteFile(@RequestParam String fileName) {
awsS3Service.deleteFile(fileName);
return ResponseEntity.ok(fileName);
}

// 파일 URL 조회
@GetMapping("/url")
public ResponseEntity<String> getFileUrl(@RequestParam String fileName) {
String fileUrl = awsS3Service.getFileUrl(fileName);
return ResponseEntity.ok(fileUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package nerdinary.hackathon.global.awss3.service;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nerdinary.hackathon.global.exception.CustomException;
import nerdinary.hackathon.global.exception.ErrorCode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import java.net.URL;

@Slf4j
@Service
@RequiredArgsConstructor
public class AwsS3Service {
@Value("${cloud.aws.s3.bucket}")
private String bucket;

private final AmazonS3 amazonS3;

// 파일 업로드 후 URL 반환
public String uploadFile(MultipartFile multipartFile) {
if (multipartFile == null || multipartFile.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "파일이 비어있거나 유효하지 않습니다.");
}

String fileName = createFileName(multipartFile.getOriginalFilename());
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());

try (InputStream inputStream = multipartFile.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
}

// S3 URL 반환
return getFileUrl(fileName);
}

// 파일명 난수화 및 확장자 포함
public String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}

// 파일 확장자 추출
private String getFileExtension(String fileName) {
try {
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일 (" + fileName + ")");
}
}

// 파일 URL 조회 (S3의 파일 URL 반환)
public String getFileUrl(String fileName) {
URL fileUrl = amazonS3.getUrl(bucket, fileName);
return fileUrl.toString();
}

// 파일 삭제
public void deleteFile(String fileName) {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
}

// 파일 삭제 (URL을 기준으로 삭제)
public void deleteFileByUrl(String fileUrl) {
try {
// URL에서 파일 이름 추출
String fileName = extractFileNameFromUrl(fileUrl);
// S3에서 파일 삭제
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
} catch (AmazonServiceException e) {
throw new CustomException(ErrorCode.S3_REMOVE_FAIL);
}catch (Exception e) {
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}

// URL에서 파일명 추출
private String extractFileNameFromUrl(String url) {
if (url == null || url.isEmpty()) {
// 예외 처리 또는 기본값 리턴
throw new IllegalArgumentException("URL cannot be null or empty");
}
try {
// URL에서 파일명 부분만 추출 후, +를 공백으로 되돌리기
String decodedUrl = java.net.URLDecoder.decode(url, "UTF-8"); // URL 디코딩
String fileName = decodedUrl.substring(decodedUrl.lastIndexOf("/") + 1); // 파일명 추출
return fileName;
} catch (Exception e) {
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package nerdinary.hackathon.global.config;


import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class AwsS3Config {
@Value("${cloud.aws.credentials.access-key}") // application.yml 에 명시한 내용
private String accessKey;

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

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

@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public enum ErrorCode {
RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."),

// 500 INTERNAL SERVER ERROR
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다.");
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류입니다."),
S3_REMOVE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S3 파일 삭제에 실패했습니다.");

private final HttpStatus status;
private final String message;
Expand Down