diff --git a/hackathon/build.gradle b/hackathon/build.gradle index 5676d96..3d03b7b 100644 --- a/hackathon/build.gradle +++ b/hackathon/build.gradle @@ -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 설정 (수동 방식) diff --git a/hackathon/src/main/java/nerdinary/hackathon/global/awss3/controller/AmazonS3Controller.java b/hackathon/src/main/java/nerdinary/hackathon/global/awss3/controller/AmazonS3Controller.java new file mode 100644 index 0000000..957b8b1 --- /dev/null +++ b/hackathon/src/main/java/nerdinary/hackathon/global/awss3/controller/AmazonS3Controller.java @@ -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 uploadFile(@RequestParam("file") MultipartFile multipartFile) { + String fileUrl = awsS3Service.uploadFile(multipartFile); + return ResponseEntity.ok(fileUrl); + } + + // 파일 삭제 + @DeleteMapping + public ResponseEntity deleteFile(@RequestParam String fileName) { + awsS3Service.deleteFile(fileName); + return ResponseEntity.ok(fileName); + } + + // 파일 URL 조회 + @GetMapping("/url") + public ResponseEntity getFileUrl(@RequestParam String fileName) { + String fileUrl = awsS3Service.getFileUrl(fileName); + return ResponseEntity.ok(fileUrl); + } +} \ No newline at end of file diff --git a/hackathon/src/main/java/nerdinary/hackathon/global/awss3/service/AwsS3Service.java b/hackathon/src/main/java/nerdinary/hackathon/global/awss3/service/AwsS3Service.java new file mode 100644 index 0000000..281aeb7 --- /dev/null +++ b/hackathon/src/main/java/nerdinary/hackathon/global/awss3/service/AwsS3Service.java @@ -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); + } + } +} \ No newline at end of file diff --git a/hackathon/src/main/java/nerdinary/hackathon/global/config/AwsS3Config.java b/hackathon/src/main/java/nerdinary/hackathon/global/config/AwsS3Config.java new file mode 100644 index 0000000..906b8d2 --- /dev/null +++ b/hackathon/src/main/java/nerdinary/hackathon/global/config/AwsS3Config.java @@ -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(); + } +} diff --git a/hackathon/src/main/java/nerdinary/hackathon/global/exception/ErrorCode.java b/hackathon/src/main/java/nerdinary/hackathon/global/exception/ErrorCode.java index 557a91d..4302ab0 100644 --- a/hackathon/src/main/java/nerdinary/hackathon/global/exception/ErrorCode.java +++ b/hackathon/src/main/java/nerdinary/hackathon/global/exception/ErrorCode.java @@ -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;