-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MERGE/#59] 사진 업로드를 위한 Presigned URL 구현
[FEAT] #59 - 사진 업로드를 위한 Presigned URL 구현
- Loading branch information
Showing
7 changed files
with
200 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
src/main/java/org/sopt/seonyakServer/global/common/external/s3/S3Controller.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package org.sopt.seonyakServer.global.common.external.s3; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.sopt.seonyakServer.global.common.external.s3.dto.PreSignedUrlResponse; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
@RequestMapping("/api/v1") | ||
@RequiredArgsConstructor | ||
public class S3Controller { | ||
private final S3Service s3Service; | ||
|
||
@GetMapping("/image") | ||
public ResponseEntity<PreSignedUrlResponse> getPreSignedUrl() { | ||
return ResponseEntity.ok(s3Service.getUploadPreSignedUrl()); | ||
} | ||
|
||
} |
58 changes: 58 additions & 0 deletions
58
src/main/java/org/sopt/seonyakServer/global/common/external/s3/S3Service.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package org.sopt.seonyakServer.global.common.external.s3; | ||
|
||
import java.net.URL; | ||
import java.time.Duration; | ||
import java.util.UUID; | ||
import lombok.RequiredArgsConstructor; | ||
import org.sopt.seonyakServer.global.common.external.s3.dto.PreSignedUrlResponse; | ||
import org.sopt.seonyakServer.global.exception.enums.ErrorType; | ||
import org.sopt.seonyakServer.global.exception.model.CustomException; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
import software.amazon.awssdk.services.s3.S3Client; | ||
import software.amazon.awssdk.services.s3.model.PutObjectRequest; | ||
import software.amazon.awssdk.services.s3.presigner.S3Presigner; | ||
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class S3Service { | ||
|
||
@Value("${aws-property.s3-bucket-name}") | ||
private String bucketName; | ||
|
||
private final S3Client s3Client; | ||
private final S3Presigner s3Presigner; | ||
|
||
// PreSigned URL 만료시간 60분 | ||
private static final Long PRE_SIGNED_URL_EXPIRE_MINUTE = 60L; | ||
private final static String imagePath = "profiles/"; | ||
|
||
public PreSignedUrlResponse getUploadPreSignedUrl() { | ||
try { | ||
// UUID 파일명 생성 | ||
String uuidFileName = UUID.randomUUID().toString() + ".jpg"; | ||
// 경로 + 파일 이름 | ||
String key = imagePath + uuidFileName; | ||
|
||
PutObjectRequest putObjectRequest = PutObjectRequest.builder() | ||
.bucket(bucketName) | ||
.key(key) | ||
.build(); | ||
|
||
// S3에서 업로드는 PUT 요청 | ||
PutObjectPresignRequest preSignedUrlRequest = PutObjectPresignRequest.builder() | ||
.signatureDuration(Duration.ofMinutes(PRE_SIGNED_URL_EXPIRE_MINUTE)) | ||
.putObjectRequest(putObjectRequest) | ||
.build(); | ||
|
||
// Persigned URL 생성 | ||
URL url = s3Presigner.presignPutObject(preSignedUrlRequest).url(); | ||
|
||
return PreSignedUrlResponse.of(uuidFileName, url.toString()); | ||
|
||
} catch (RuntimeException e) { | ||
throw new CustomException(ErrorType.GET_UPLOAD_PRESIGNED_URL_ERROR); | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/java/org/sopt/seonyakServer/global/common/external/s3/dto/PreSignedUrlResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.sopt.seonyakServer.global.common.external.s3.dto; | ||
|
||
public record PreSignedUrlResponse( | ||
String fileName, | ||
String url | ||
) { | ||
public static PreSignedUrlResponse of(String fileName, String url) { | ||
return new PreSignedUrlResponse(fileName, url); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
src/main/java/org/sopt/seonyakServer/global/config/S3Config.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.sopt.seonyakServer.global.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; | ||
import software.amazon.awssdk.services.s3.presigner.S3Presigner; | ||
|
||
@Configuration | ||
public class S3Config { | ||
|
||
@Value("${aws-property.access-key}") | ||
private String accessKeyId; | ||
|
||
@Value("${aws-property.secret-key}") | ||
private String secretAccessKey; | ||
|
||
@Value("${aws-property.aws-region}") | ||
private String awsRegion; | ||
|
||
@Bean | ||
public S3Client s3Client() { | ||
return S3Client.builder() | ||
.region(Region.of(awsRegion)) | ||
.credentialsProvider(StaticCredentialsProvider.create( | ||
AwsBasicCredentials.create(accessKeyId, secretAccessKey) | ||
)) | ||
.build(); | ||
} | ||
|
||
@Bean | ||
public S3Presigner getS3Presigner() { | ||
return S3Presigner.builder() | ||
.region(Region.of(awsRegion)) | ||
.credentialsProvider(StaticCredentialsProvider.create( | ||
AwsBasicCredentials.create(accessKeyId, secretAccessKey) | ||
)) | ||
.build(); | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
src/test/java/org/sopt/seonyakServer/global/common/external/s3/S3ServiceIntegrationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package org.sopt.seonyakServer.global.common.external.s3; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.io.ByteArrayInputStream; | ||
import java.io.File; | ||
import java.io.FileInputStream; | ||
import java.io.InputStream; | ||
import java.net.HttpURLConnection; | ||
import java.net.URL; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.sopt.seonyakServer.global.common.external.s3.dto.PreSignedUrlResponse; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.test.context.ActiveProfiles; | ||
import org.springframework.test.context.junit.jupiter.SpringExtension; | ||
|
||
@ExtendWith(SpringExtension.class) | ||
@SpringBootTest | ||
@ActiveProfiles("test") | ||
class S3ServiceIntegrationTest { | ||
|
||
@Autowired | ||
private S3Service s3Service; | ||
|
||
@Test | ||
void testGetUploadPreSignedUrl() throws Exception { | ||
// Pre-Signed URL 생성 | ||
PreSignedUrlResponse response = s3Service.getUploadPreSignedUrl(); | ||
|
||
// Assertions: Pre-Signed URL이 유효한지 확인 | ||
assertThat(response).isNotNull(); | ||
assertThat(response.fileName()).isNotEmpty(); | ||
assertThat(response.url()).isNotEmpty(); | ||
|
||
// 이미지 파일 읽기 | ||
File file = new File("src/test/resources/경희대학교_재학증명서.pdf"); | ||
byte[] imageData; | ||
try (InputStream inputStream = new FileInputStream(file)) { | ||
imageData = inputStream.readAllBytes(); | ||
} | ||
|
||
// Pre-Signed URL로 PUT 요청 보내기 | ||
URL url = new URL(response.url()); | ||
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | ||
connection.setDoOutput(true); | ||
connection.setRequestMethod("PUT"); | ||
connection.setRequestProperty("Content-Type", "application/pdf"); | ||
connection.setRequestProperty("Content-Length", String.valueOf(imageData.length)); | ||
|
||
// 이미지 데이터 전송 | ||
try (InputStream inputStream = new ByteArrayInputStream(imageData)) { | ||
byte[] buffer = new byte[1024]; | ||
int bytesRead; | ||
while ((bytesRead = inputStream.read(buffer)) != -1) { | ||
connection.getOutputStream().write(buffer, 0, bytesRead); | ||
} | ||
} | ||
|
||
int responseCode = connection.getResponseCode(); | ||
connection.disconnect(); | ||
|
||
// Assertions: HTTP 응답 코드가 200 (성공)인지 확인 | ||
assertThat(responseCode).isEqualTo(200); | ||
} | ||
} |