-
Notifications
You must be signed in to change notification settings - Fork 1
띱 이벤트 관련 엔드포인트 추가 및 S3 파일 저장로직 구현 #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a055766
904e4ff
4248c23
72c2640
f460515
6b13983
0e507cc
0179b7c
9a89a53
ba66648
8bf1429
8dc4623
98ef7b4
aff67e2
b560288
2dba0ec
0e8d4a7
cd72fd3
64013e4
05d046a
372f5d0
18373a5
fe5ec6d
a7cccaf
b798cf3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.knu.ddip.common.config; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| 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.S3Client; | ||
|
|
||
| @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 S3Client s3Client() { | ||
| AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(accessKey, secretKey); | ||
|
|
||
| return S3Client.builder() | ||
| .region(Region.of(region)) | ||
| .credentialsProvider(StaticCredentialsProvider.create(awsCredentials)) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.knu.ddip.common.dto; | ||
|
|
||
| public record StringTypeResponse( | ||
| String response | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.knu.ddip.common.file; | ||
|
|
||
| public class FileStorageException extends RuntimeException { | ||
|
|
||
| public FileStorageException(String message) { | ||
| super(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.knu.ddip.common.file; | ||
|
|
||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| public interface FileStorageService { | ||
|
|
||
| String uploadFile(MultipartFile file, String directory); | ||
|
|
||
| void deleteFile(String fileUrl); | ||
|
|
||
| boolean exists(String fileUrl); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package com.knu.ddip.common.file.infrastructure; | ||
|
|
||
| import com.knu.ddip.common.file.FileStorageException; | ||
| import com.knu.ddip.common.file.FileStorageService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
| import software.amazon.awssdk.core.sync.RequestBody; | ||
| import software.amazon.awssdk.services.s3.S3Client; | ||
| import software.amazon.awssdk.services.s3.model.*; | ||
|
|
||
| import java.io.IOException; | ||
| import java.time.LocalDateTime; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.util.UUID; | ||
|
|
||
| @Slf4j | ||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class S3FileStorageService implements FileStorageService { | ||
|
|
||
| private final S3Client s3Client; | ||
|
|
||
| @Value("${cloud.aws.s3.bucket}") | ||
| private String bucketName; | ||
|
|
||
| @Override | ||
| public String uploadFile(MultipartFile file, String directory) { | ||
| validateFile(file); | ||
|
|
||
| try { | ||
| String fileName = generateUniqueFileName(file.getOriginalFilename()); | ||
| String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); | ||
| String key = String.format("%s/%s/%s", directory, dateDir, fileName); | ||
|
|
||
| PutObjectRequest putObjectRequest = PutObjectRequest.builder() | ||
| .bucket(bucketName) | ||
| .key(key) | ||
| .contentType(file.getContentType()) | ||
| .contentLength(file.getSize()) | ||
| .build(); | ||
|
|
||
| s3Client.putObject(putObjectRequest, | ||
| RequestBody.fromInputStream(file.getInputStream(), file.getSize())); | ||
|
|
||
| String fileUrl = String.format("https://%s.s3.amazonaws.com/%s", bucketName, key); | ||
|
|
||
| log.info("S3 파일 업로드 완료: {} -> {}", file.getOriginalFilename(), fileUrl); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그는 나중에 프로덕션 환경에서 확인하기 위해 작성하신건가요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 맞습니다. 서드파티를 사용하기 때문에 로그 확인용으로 좀 두고 안정성이 확보되면 추후 제거할 예정입니다! |
||
| return fileUrl; | ||
|
|
||
| } catch (IOException e) { | ||
| log.error("S3 파일 업로드 실패: {}", file.getOriginalFilename(), e); | ||
| throw new FileStorageException("파일 업로드에 실패했습니다: " + e.getMessage()); | ||
| } catch (S3Exception e) { | ||
| log.error("S3 서비스 오류: {}", e.awsErrorDetails().errorMessage()); | ||
| throw new FileStorageException("S3 업로드 중 오류가 발생했습니다: " + e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void deleteFile(String fileUrl) { | ||
| try { | ||
| String key = extractS3Key(fileUrl); | ||
|
|
||
| DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() | ||
| .bucket(bucketName) | ||
| .key(key) | ||
| .build(); | ||
|
|
||
| s3Client.deleteObject(deleteObjectRequest); | ||
| log.info("S3 파일 삭제 완료: {}", fileUrl); | ||
|
|
||
| } catch (S3Exception e) { | ||
| log.error("S3 파일 삭제 실패: {}", fileUrl, e); | ||
| throw new FileStorageException("파일 삭제에 실패했습니다: " + e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public boolean exists(String fileUrl) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 exists 메서드는 용도가 뭔가요? 아직 사용하시진 않은 것으로 보이네요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 포트를 만들 때 단순 upload만 만드는 것이 아니라 delete와 exists도 함께 만들었습니다. 현재는 upload기능만 사용하지만, 추후 용량 관리 등을 위해 delete를 사용할 경우에 exists도 아마 사용하지 않을까 싶어 확장성으로 고려해 미리 만들어 두었습니다. |
||
| try { | ||
| String key = extractS3Key(fileUrl); | ||
|
|
||
| HeadObjectRequest headObjectRequest = HeadObjectRequest.builder() | ||
| .bucket(bucketName) | ||
| .key(key) | ||
| .build(); | ||
|
|
||
| s3Client.headObject(headObjectRequest); | ||
| return true; | ||
|
|
||
| } catch (NoSuchKeyException e) { | ||
| return false; | ||
| } catch (S3Exception e) { | ||
| log.error("S3 파일 존재 여부 확인 실패: {}", fileUrl, e); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| private String generateUniqueFileName(String originalFilename) { | ||
| String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HHmmss")); | ||
| String uuid = UUID.randomUUID().toString().substring(0, 8); | ||
| String extension = getFileExtension(originalFilename); | ||
| return String.format("%s_%s%s", timestamp, uuid, extension); | ||
| } | ||
|
|
||
| private String getFileExtension(String filename) { | ||
| if (filename == null || filename.isEmpty()) { | ||
| return ""; | ||
| } | ||
| int lastDotIndex = filename.lastIndexOf('.'); | ||
| return (lastDotIndex == -1) ? "" : filename.substring(lastDotIndex); | ||
| } | ||
|
|
||
| private String extractS3Key(String fileUrl) { | ||
| if (fileUrl.contains(".s3.amazonaws.com/")) { | ||
| return fileUrl.substring(fileUrl.indexOf(".s3.amazonaws.com/") + 18); | ||
| } | ||
|
|
||
| throw new IllegalArgumentException("올바르지 않은 S3 URL 형식입니다: " + fileUrl); | ||
| } | ||
|
|
||
| private void validateFile(MultipartFile file) { | ||
| if (file.isEmpty()) { | ||
| throw new FileStorageException("빈 파일은 업로드할 수 없습니다."); | ||
| } | ||
|
|
||
| // 파일 크기 제한 (50MB) | ||
| long maxSize = 50 * 1024 * 1024; | ||
| if (file.getSize() > maxSize) { | ||
| throw new FileStorageException("파일 크기는 50MB를 초과할 수 없습니다."); | ||
| } | ||
|
|
||
| // 이미지 파일만 허용 | ||
| String contentType = file.getContentType(); | ||
| if (contentType == null || !contentType.startsWith("image/")) { | ||
| throw new FileStorageException("이미지 파일만 업로드 가능합니다."); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.knu.ddip.ddipevent.application.dto; | ||
|
|
||
| import com.knu.ddip.ddipevent.domain.PhotoStatus; | ||
|
|
||
| public record PhotoFeedbackRequest( | ||
| PhotoStatus status, | ||
| String feedback | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.knu.ddip.ddipevent.application.dto; | ||
|
|
||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| public record PhotoUploadRequest( | ||
| MultipartFile photo, | ||
| double latitude, | ||
| double longitude, | ||
| String responderComment | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package com.knu.ddip.ddipevent.application.dto; | ||
|
|
||
| import java.util.UUID; | ||
|
|
||
| public record SelectApplicantRequest( | ||
| UUID applicantId | ||
| ) { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
파일 저장 방식 변경을 염두에 두고 인터페이스 기반으로 개발하신것 좋네요 👍👍