From b18c49707506982838b796f20371fdfd23cb811a Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 02:47:31 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EB=94=94=EB=B2=84=EA=B9=85=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Exception=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt index eda7c22..6a32a02 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt @@ -57,6 +57,12 @@ class ImageService() { expiration: Date ): String { // privateKey와 keyPairId는 GitHub Secrets에서 가져온 값이 자동으로 주입됨 + if (privateKey.isBlank()) { + throw IllegalStateException("Private key is not configured or is blank") + } + if (keyPairId.isBlank()) { + throw IllegalStateException("Key Pair ID is not configured or is blank") + } return createSignedUrl(domain, key, privateKey, keyPairId, expiration) } From 6ea79e60466a751909995d3e53f55596bfd003e4 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 10:19:50 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=EA=B9=83=ED=97=88=EB=B8=8C=20=EC=8B=9C?= =?UTF-8?q?=ED=81=AC=EB=A6=BF=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=EC=9D=B4=20=EB=90=98=EB=8F=84=EB=A1=9D=20cd-?= =?UTF-8?q?aws.yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-aws.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/cd-aws.yml b/.github/workflows/cd-aws.yml index e0ea6e9..91ed9e5 100644 --- a/.github/workflows/cd-aws.yml +++ b/.github/workflows/cd-aws.yml @@ -86,6 +86,9 @@ jobs: username: ${{ env.EC2_SSH_USER }} key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} script: | + # 환경 변수 전달 + export CLF_PRIVATE_KEY="${{ secrets.CLF_PRIVATE_KEY }}" + export CLF_KEY_PAIR_ID="${{ secrets.CLF_KEY_PAIR_ID }}" echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker rm -f $(docker ps -qa) From f551d748e00f527316cf1568eefe94ffbac52741 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 10:34:38 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=EA=B9=83=ED=97=88=EB=B8=8C=20=EC=8B=9C?= =?UTF-8?q?=ED=81=AC=EB=A6=BF=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=EC=9D=B4=20=EB=90=98=EB=8F=84=EB=A1=9D=20cd-?= =?UTF-8?q?aws.yml=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-aws.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd-aws.yml b/.github/workflows/cd-aws.yml index 91ed9e5..84c70fc 100644 --- a/.github/workflows/cd-aws.yml +++ b/.github/workflows/cd-aws.yml @@ -28,6 +28,13 @@ jobs: java-version: '17' distribution: 'temurin' + - name: Set up environment variables + run: | + # GitHub Secrets에서 환경 변수로 주입 + export CLF_PRIVATE_KEY="${{ secrets.CLF_PRIVATE_KEY }}" + export CLF_KEY_PAIR_ID="${{ secrets.CLF_KEY_PAIR_ID }}" + shell: bash + # 민감한 정보(ex. DB url, username, password) 는 따로 github secrets에 저장 - name: Set up application.yaml run: | @@ -86,9 +93,6 @@ jobs: username: ${{ env.EC2_SSH_USER }} key: ${{ secrets.EC2_SSH_PRIVATE_KEY }} script: | - # 환경 변수 전달 - export CLF_PRIVATE_KEY="${{ secrets.CLF_PRIVATE_KEY }}" - export CLF_KEY_PAIR_ID="${{ secrets.CLF_KEY_PAIR_ID }}" echo "${{ secrets.DOCKER_HUB_TOKEN }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin docker rm -f $(docker ps -qa) From 5bd09496b20a1e2a5ad06b308877c86f0df8105c Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 10:49:46 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-aws.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/cd-aws.yml b/.github/workflows/cd-aws.yml index 84c70fc..c50466c 100644 --- a/.github/workflows/cd-aws.yml +++ b/.github/workflows/cd-aws.yml @@ -28,26 +28,12 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Set up environment variables - run: | - # GitHub Secrets에서 환경 변수로 주입 - export CLF_PRIVATE_KEY="${{ secrets.CLF_PRIVATE_KEY }}" - export CLF_KEY_PAIR_ID="${{ secrets.CLF_KEY_PAIR_ID }}" - shell: bash - # 민감한 정보(ex. DB url, username, password) 는 따로 github secrets에 저장 - name: Set up application.yaml run: | echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.yaml echo "${{ secrets.P12_BASE64 }}" | base64 --decode > ./src/main/resources/test.p12 - - name: Set up CloudFront environment variables - env: - CLF_PRIVATE_KEY: ${{ secrets.CLF_PRIVATE_KEY }} - CLF_KEY_PAIR_ID: ${{ secrets.CLF_KEY_PAIR_ID }} - run: echo "Environment variables set" - - - name: Build with Gradle run: ./gradlew build -x test From 1ae066f5da6ba0e38538165a9a0b5df049e92c7d Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 11:12:59 +0900 Subject: [PATCH 05/11] =?UTF-8?q?Swagger=EC=97=90=EC=84=9C=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EC=8B=9C,=20=ED=8C=8C=EC=9D=BC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EB=B3=80=EA=B2=BD=EA=B3=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20DTO=EB=A1=9C=20Request=20Body=20=EA=B5=AC?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../toyTeam6Airbnb/Image/ImageController.kt | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt index 2ddb38f..bdca5bc 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -1,5 +1,9 @@ package com.example.toyTeam6Airbnb.Image +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.parameters.RequestBody import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -17,9 +21,20 @@ import java.util.Date class ImageController( private val imageService: ImageService ) { - + @Operation( + summary = "Upload an image", + description = "Uploads an image to the server and stores it in S3.", + requestBody = RequestBody( + content = [ + Content( + mediaType = "multipart/form-data", + schema = Schema(implementation = UploadImageRequest::class) + ) + ] + ) + ) // 이미지 업로드 엔드포인트 - @PostMapping("/upload") + @PostMapping("/upload", consumes = ["multipart/form-data"]) fun uploadImage( @RequestParam("file") file: MultipartFile, @RequestParam("key") key: String @@ -56,6 +71,15 @@ class ImageController( .body("Failed to generate signed URL: ${e.message}") } } + + // 내부 DTO 클래스 정의 + data class UploadImageRequest( + @Schema(type = "string", format = "binary", description = "The image file to upload") + val file: MultipartFile, + + @Schema(description = "The key to associate with the uploaded image", example = "example-key") + val key: String + ) } // // 이미지 다운로드 엔드포인트 From 66dcf1ef20dc6bffa6c7b55eb9fed3c67642127e Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 12:04:32 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81,=20RequestBody=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=EC=84=9C=20API=20=EC=9D=B4=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/toyTeam6Airbnb/Image/ImageController.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt index bdca5bc..0205123 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile import java.nio.file.Paths @@ -36,10 +35,12 @@ class ImageController( // 이미지 업로드 엔드포인트 @PostMapping("/upload", consumes = ["multipart/form-data"]) fun uploadImage( - @RequestParam("file") file: MultipartFile, - @RequestParam("key") key: String + @RequestBody request: UploadImageRequest ): ResponseEntity { return try { + val file = request.file + val key = request.key + val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile() file.transferTo(tempFile) // MultipartFile을 임시 파일로 저장 From 393fe3a7da3769c44b91440f1a7c28156f032915 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 12:23:56 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EC=8B=9C=20=EA=B0=9D=EC=B2=B4=EC=9D=98=20Key?= =?UTF-8?q?=EC=95=9E=EC=97=90=20=EA=B3=B5=EB=B0=B1=EB=AC=B8=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=94=94=EB=B2=84=EA=B9=85=20=ED=94=84=EB=A6=B0?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt | 2 +- .../kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt index 0205123..0e912ec 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -39,7 +39,7 @@ class ImageController( ): ResponseEntity { return try { val file = request.file - val key = request.key + val key = request.key.trim() val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile() file.transferTo(tempFile) // MultipartFile을 임시 파일로 저장 diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt index 6a32a02..b26daae 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt @@ -29,6 +29,7 @@ class ImageService() { // 파일 업로드 메서드 fun uploadFile(filePath: String, key: String) { + println("Uploading File with key : $key") // 디버깅 s3Client.putObject( PutObjectRequest.builder() .bucket(bucketName) From 5100eb8dc2d26e172f01af3c1f16fe2027f1c8d3 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 16:08:11 +0900 Subject: [PATCH 08/11] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20key/=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=AA=85=20=EC=9D=98=20=EA=B2=BD=EB=A1=9C=EB=A1=9C=20s3?= =?UTF-8?q?=EC=97=90=20=EC=97=85=EB=A1=9C=EB=93=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../toyTeam6Airbnb/Image/ImageController.kt | 30 ++---- .../toyTeam6Airbnb/Image/ImageService.kt | 102 ++++++++++++------ 2 files changed, 76 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt index 0e912ec..a39ffe6 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -12,8 +12,6 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile -import java.nio.file.Paths -import java.util.Date @RestController @RequestMapping("/api/images") @@ -40,32 +38,18 @@ class ImageController( return try { val file = request.file val key = request.key.trim() - - val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile() - file.transferTo(tempFile) // MultipartFile을 임시 파일로 저장 - - imageService.uploadFile(tempFile.absolutePath, key) // S3에 업로드 - tempFile.delete() // 임시 파일 삭제 - - ResponseEntity("Image uploaded successfully with key: $key", HttpStatus.OK) + val fileUrl = imageService.uploadFile(file, key) // S3에 업로드 및 경로 저장 + ResponseEntity.ok("Image uploaded successfully with key: $key, URL: $fileUrl") } catch (e: Exception) { - ResponseEntity("Failed to upload image: ${e.message}", HttpStatus.INTERNAL_SERVER_ERROR) + ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to upload image: ${e.message}") } } - @GetMapping("/{key}") - fun getImageSignedUrl( - @PathVariable("key") key: String - ): ResponseEntity { + @GetMapping("/get-signed-url/{key}") + fun getSignedUrl(@PathVariable key: String): ResponseEntity { return try { - val expirationDate = Date(System.currentTimeMillis() + 3600_000) // 1시간 유효 - - val signedUrl = imageService.generateSignedUrl( - "https://d3m9s5wmwvsq01.cloudfront.net", - key, - expirationDate - ) - + val signedUrl = imageService.generateSignedUrl(key) ResponseEntity.ok(signedUrl) } catch (e: Exception) { ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt index b26daae..e48b94c 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt @@ -4,14 +4,17 @@ import com.amazonaws.services.cloudfront.CloudFrontUrlSigner import com.amazonaws.services.cloudfront.util.SignerUtils 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.regions.Region import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.PutObjectRequest import java.io.File +import java.io.IOException import java.nio.file.Paths import java.security.PrivateKey import java.util.Date +import java.util.UUID @Service class ImageService() { @@ -28,44 +31,77 @@ class ImageService() { private val cloudFrontUrl: String = "https://d3m9s5wmwvsq01.cloudfront.net" // 파일 업로드 메서드 - fun uploadFile(filePath: String, key: String) { - println("Uploading File with key : $key") // 디버깅 - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucketName) - .key(key) - .build(), - RequestBody.fromFile(Paths.get(filePath)) - ) - } + fun uploadFile(file: MultipartFile, key: String): String { + val fileName = "$key/${UUID.randomUUID()}_${file.originalFilename}" // 고유 경로 생성 + try { + val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile() + file.transferTo(tempFile) -// // 파일 다운로드 메서드 -// @Transactional -// fun downloadFile(key: String, destination: String) { -// s3Client.getObject( -// GetObjectRequest.builder() -// .bucket(bucketName) -// .key(key) -// .build(), -// Paths.get(destination) -// ) -// } + s3Client.putObject( + PutObjectRequest.builder() + .bucket(bucketName) + .key(fileName) + .build(), + RequestBody.fromFile(tempFile.toPath()) + ) + tempFile.delete() - // CloudFront signed URL 생성 메서드 - fun generateSignedUrl( - domain: String, - key: String, - expiration: Date - ): String { - // privateKey와 keyPairId는 GitHub Secrets에서 가져온 값이 자동으로 주입됨 - if (privateKey.isBlank()) { - throw IllegalStateException("Private key is not configured or is blank") + // DB에 파일 경로 저장 saveFilePathToDatabase(key, fileName) (추후 Room, User 엔티티에 추가식으로 수정) + // ProfileImageUpload, RoomImageUplaod 형식으로 할것 + } catch (e: IOException) { + throw RuntimeException("S3 업로드 중 에러가 발생했습니다: ${e.message}", e) } - if (keyPairId.isBlank()) { - throw IllegalStateException("Key Pair ID is not configured or is blank") + + return "https://$bucketName.s3.amazonaws.com/$fileName" + } + + // CloudFront signed URL 생성 메서드 + fun generateSignedUrl(key: String): String { + try { + if (privateKey.isBlank()) { + throw IllegalStateException("Private key is not configured or is blank") + } + if (keyPairId.isBlank()) { + throw IllegalStateException("Key Pair ID is not configured or is blank") + } + // S3 객체 경로 생성 (key 폴더 사용) + val filePath = "$key/" + + // 만료 시간 설정 (예: 1시간 후) + val expirationDate = Date(System.currentTimeMillis() + 3600_000) + + // CloudFront 서명된 URL 생성 + // 1. 임시 파일 생성 및 비공개 키 쓰기 + val tempFile = File.createTempFile("privateKey", ".pem") + tempFile.deleteOnExit() // 애플리케이션 종료 시 자동 삭제 + tempFile.writeText(privateKey) + + // 2. Private Key 객체 로드 + val privateKeyObj: PrivateKey = SignerUtils.loadPrivateKey(tempFile) + return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( + "https://d3m9s5wmwvsq01.cloudfront.net/$filePath", + keyPairId, // Key Pair ID + privateKeyObj, + expirationDate + ) + } catch (e: Exception) { + throw RuntimeException("Signed URL 생성 중 에러 발생: ${e.message}", e) } - return createSignedUrl(domain, key, privateKey, keyPairId, expiration) } +// fun generateSignedUrl( +// domain: String, +// key: String, +// expiration: Date +// ): String { +// // privateKey와 keyPairId는 GitHub Secrets에서 가져온 값이 자동으로 주입됨 +// if (privateKey.isBlank()) { +// throw IllegalStateException("Private key is not configured or is blank") +// } +// if (keyPairId.isBlank()) { +// throw IllegalStateException("Key Pair ID is not configured or is blank") +// } +// return createSignedUrl(domain, key, privateKey, keyPairId, expiration) +// } // CloudFront 서명 URL 생성 메서드 // createSignedUrl 메서드 구현 From a0f0b9650bce069805875c48200e96e850f5f90d Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 18:59:47 +0900 Subject: [PATCH 09/11] =?UTF-8?q?Signed=20URl=20->=20Presigned=20URL=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../toyTeam6Airbnb/Image/ImageController.kt | 101 +++----- .../toyTeam6Airbnb/Image/ImageService.kt | 216 ++++++++++-------- 2 files changed, 148 insertions(+), 169 deletions(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt index a39ffe6..39b6dd5 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -1,17 +1,12 @@ package com.example.toyTeam6Airbnb.Image import io.swagger.v3.oas.annotations.Operation -import io.swagger.v3.oas.annotations.media.Content -import io.swagger.v3.oas.annotations.media.Schema -import io.swagger.v3.oas.annotations.parameters.RequestBody import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController -import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/api/images") @@ -19,83 +14,47 @@ class ImageController( private val imageService: ImageService ) { @Operation( - summary = "Upload an image", - description = "Uploads an image to the server and stores it in S3.", - requestBody = RequestBody( - content = [ - Content( - mediaType = "multipart/form-data", - schema = Schema(implementation = UploadImageRequest::class) - ) - ] - ) + summary = "Generate a presigned URL for uploading an image", + description = "Generates a presigned URL for uploading an image directly to S3." ) - // 이미지 업로드 엔드포인트 - @PostMapping("/upload", consumes = ["multipart/form-data"]) - fun uploadImage( - @RequestBody request: UploadImageRequest + @GetMapping("/upload-url") + fun generateUploadUrl( + @RequestParam("key") key: String, + @RequestParam("expirationMinutes", defaultValue = "10") expirationMinutes: Long ): ResponseEntity { return try { - val file = request.file - val key = request.key.trim() - val fileUrl = imageService.uploadFile(file, key) // S3에 업로드 및 경로 저장 - ResponseEntity.ok("Image uploaded successfully with key: $key, URL: $fileUrl") + val uploadUrl = imageService.generateUploadUrl(key, expirationMinutes) + ResponseEntity.ok(uploadUrl) } catch (e: Exception) { ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to upload image: ${e.message}") + .body("Failed to generate upload URL: ${e.message}") } } - @GetMapping("/get-signed-url/{key}") - fun getSignedUrl(@PathVariable key: String): ResponseEntity { + @Operation( + summary = "Generate a presigned URL for downloading an image", + description = "Generates a presigned URL for downloading an image directly from S3." + ) + @GetMapping("/download-url") + fun generateDownloadUrl( + @RequestParam("key") key: String, + @RequestParam("expirationMinutes", defaultValue = "60") expirationMinutes: Long + ): ResponseEntity { return try { - val signedUrl = imageService.generateSignedUrl(key) - ResponseEntity.ok(signedUrl) + val downloadUrl = imageService.generateDownloadUrl(key, expirationMinutes) + ResponseEntity.ok(downloadUrl) } catch (e: Exception) { ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to generate signed URL: ${e.message}") + .body("Failed to generate download URL: ${e.message}") } } - // 내부 DTO 클래스 정의 - data class UploadImageRequest( - @Schema(type = "string", format = "binary", description = "The image file to upload") - val file: MultipartFile, - - @Schema(description = "The key to associate with the uploaded image", example = "example-key") - val key: String - ) -} - -// // 이미지 다운로드 엔드포인트 -// @GetMapping("/download") -// fun downloadImage( -// @RequestParam("key") key: String, -// @RequestParam("destination") destination: String -// ): ResponseEntity { -// return try { -// imageService.downloadFile(key, destination) // S3에서 다운로드 -// ResponseEntity("Image downloaded successfully to: $destination", HttpStatus.OK) -// } catch (e: Exception) { -// ResponseEntity("Failed to download image: ${e.message}", HttpStatus.INTERNAL_SERVER_ERROR) -// } -// } - -// // 이미지를 화면에 표시, 보안 취약 -// @GetMapping("/{key}") -// fun getImage( -// @PathVariable("key") key: String -// ): ResponseEntity { -// return try { -// // CloudFront 배포 URL -// val cloudFrontUrl = "https://d3m9s5wmwvsq01.cloudfront.net/$key" +// // 내부 DTO 클래스 정의 +// data class UploadImageRequest( +// @Schema(type = "string", format = "binary", description = "The image file to upload") +// val file: MultipartFile, // -// // 302 Redirect -// ResponseEntity.status(HttpStatus.FOUND) -// .header("Location", cloudFrontUrl) -// .build() -// } catch (e: Exception) { -// ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) -// .body(null) -// } -// } +// @Schema(description = "The key to associate with the uploaded image", example = "example-key") +// val key: String +// ) +} diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt index e48b94c..c9ff622 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt @@ -1,19 +1,15 @@ package com.example.toyTeam6Airbnb.Image -import com.amazonaws.services.cloudfront.CloudFrontUrlSigner -import com.amazonaws.services.cloudfront.util.SignerUtils 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.regions.Region import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.GetObjectRequest import software.amazon.awssdk.services.s3.model.PutObjectRequest -import java.io.File -import java.io.IOException -import java.nio.file.Paths -import java.security.PrivateKey -import java.util.Date +import software.amazon.awssdk.services.s3.presigner.S3Presigner +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest +import java.time.Duration import java.util.UUID @Service @@ -24,112 +20,136 @@ class ImageService() { @Value("\${cloudfront.key-pair-id}") private lateinit var keyPairId: String + private val s3Client: S3Client = S3Client.builder() .region(Region.AP_NORTHEAST_2) // 원하는 리전 설정 .build() + + private val s3Presigner: S3Presigner = S3Presigner.builder() + .region(Region.AP_NORTHEAST_2) + .build() + private val bucketName: String = "waffle-team6-storage" private val cloudFrontUrl: String = "https://d3m9s5wmwvsq01.cloudfront.net" - // 파일 업로드 메서드 - fun uploadFile(file: MultipartFile, key: String): String { - val fileName = "$key/${UUID.randomUUID()}_${file.originalFilename}" // 고유 경로 생성 - try { - val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile() - file.transferTo(tempFile) + // Presigned URL for Upload + fun generateUploadUrl(key: String, expirationMinutes: Long): String { + val filePath = "$key/${UUID.randomUUID()}_upload.jpg" // 고유 파일 경로 생성 + val putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(filePath) + .build() - s3Client.putObject( - PutObjectRequest.builder() - .bucket(bucketName) - .key(fileName) - .build(), - RequestBody.fromFile(tempFile.toPath()) - ) - tempFile.delete() + val presignRequest = PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(expirationMinutes)) + .putObjectRequest(putObjectRequest) + .build() - // DB에 파일 경로 저장 saveFilePathToDatabase(key, fileName) (추후 Room, User 엔티티에 추가식으로 수정) - // ProfileImageUpload, RoomImageUplaod 형식으로 할것 - } catch (e: IOException) { - throw RuntimeException("S3 업로드 중 에러가 발생했습니다: ${e.message}", e) - } - - return "https://$bucketName.s3.amazonaws.com/$fileName" + return s3Presigner.presignPutObject(presignRequest).url().toString() } - // CloudFront signed URL 생성 메서드 - fun generateSignedUrl(key: String): String { - try { - if (privateKey.isBlank()) { - throw IllegalStateException("Private key is not configured or is blank") - } - if (keyPairId.isBlank()) { - throw IllegalStateException("Key Pair ID is not configured or is blank") - } - // S3 객체 경로 생성 (key 폴더 사용) - val filePath = "$key/" - - // 만료 시간 설정 (예: 1시간 후) - val expirationDate = Date(System.currentTimeMillis() + 3600_000) + // Presigned URL for Download + // 사용자가 Upload시 생성한 Key를 그대로 입력하면 됨. (UUID 없이) + fun generateDownloadUrl(key: String, expirationMinutes: Long): String { + val getObjectRequest = GetObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build() - // CloudFront 서명된 URL 생성 - // 1. 임시 파일 생성 및 비공개 키 쓰기 - val tempFile = File.createTempFile("privateKey", ".pem") - tempFile.deleteOnExit() // 애플리케이션 종료 시 자동 삭제 - tempFile.writeText(privateKey) + val presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(expirationMinutes)) + .getObjectRequest(getObjectRequest) + .build() - // 2. Private Key 객체 로드 - val privateKeyObj: PrivateKey = SignerUtils.loadPrivateKey(tempFile) - return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( - "https://d3m9s5wmwvsq01.cloudfront.net/$filePath", - keyPairId, // Key Pair ID - privateKeyObj, - expirationDate - ) - } catch (e: Exception) { - throw RuntimeException("Signed URL 생성 중 에러 발생: ${e.message}", e) - } + return s3Presigner.presignGetObject(presignRequest).url().toString() } -// fun generateSignedUrl( + +// // 파일 업로드 메서드 +// fun uploadFile(file: MultipartFile, key: String): String { +// val fileName = "$key/${UUID.randomUUID()}_${file.originalFilename}" // 고유 경로 생성 +// try { +// val tempFile = Paths.get(System.getProperty("java.io.tmpdir"), file.originalFilename).toFile() +// file.transferTo(tempFile) +// +// s3Client.putObject( +// PutObjectRequest.builder() +// .bucket(bucketName) +// .key(fileName) +// .build(), +// RequestBody.fromFile(tempFile.toPath()) +// ) +// tempFile.delete() +// +// // DB에 파일 경로 저장 saveFilePathToDatabase(key, fileName) (추후 Room, User 엔티티에 추가식으로 수정) +// // ProfileImageUpload, RoomImageUplaod 형식으로 할것 +// } catch (e: IOException) { +// throw RuntimeException("S3 업로드 중 에러가 발생했습니다: ${e.message}", e) +// } +// +// return "https://$bucketName.s3.amazonaws.com/$fileName" +// } +// +// // CloudFront signed URL 생성 메서드 +// fun generateSignedUrl(key: String): String { +// try { +// if (privateKey.isBlank()) { +// throw IllegalStateException("Private key is not configured or is blank") +// } +// if (keyPairId.isBlank()) { +// throw IllegalStateException("Key Pair ID is not configured or is blank") +// } +// // S3 객체 경로 생성 (key 폴더 사용) +// val filePath = "$key/" +// +// // 만료 시간 설정 (예: 1시간 후) +// val expirationDate = Date(System.currentTimeMillis() + 3600_000) +// +// // CloudFront 서명된 URL 생성 +// // 1. 임시 파일 생성 및 비공개 키 쓰기 +// val tempFile = File.createTempFile("privateKey", ".pem") +// tempFile.deleteOnExit() // 애플리케이션 종료 시 자동 삭제 +// tempFile.writeText(privateKey) +// +// // 2. Private Key 객체 로드 +// val privateKeyObj: PrivateKey = SignerUtils.loadPrivateKey(tempFile) +// return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( +// "https://d3m9s5wmwvsq01.cloudfront.net/$filePath", +// keyPairId, // Key Pair ID +// privateKeyObj, +// expirationDate +// ) +// } catch (e: Exception) { +// throw RuntimeException("Signed URL 생성 중 에러 발생: ${e.message}", e) +// } +// } +// +// // CloudFront 서명 URL 생성 메서드 +// // createSignedUrl 메서드 구현 +// private fun createSignedUrl( // domain: String, // key: String, +// privateKey: String, +// keyPairId: String, // expiration: Date // ): String { -// // privateKey와 keyPairId는 GitHub Secrets에서 가져온 값이 자동으로 주입됨 -// if (privateKey.isBlank()) { -// throw IllegalStateException("Private key is not configured or is blank") -// } -// if (keyPairId.isBlank()) { -// throw IllegalStateException("Key Pair ID is not configured or is blank") +// try { +// // 1. 임시 파일 생성 및 비공개 키 쓰기 +// val tempFile = File.createTempFile("privateKey", ".pem") +// tempFile.deleteOnExit() // 애플리케이션 종료 시 자동 삭제 +// tempFile.writeText(privateKey) +// +// // 2. Private Key 객체 로드 +// val privateKeyObj: PrivateKey = SignerUtils.loadPrivateKey(tempFile) +// +// // 3. CloudFront 서명된 URL 생성 +// return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( +// "$domain/$key", // 리소스 URL +// keyPairId, // Key Pair ID +// privateKeyObj, // PrivateKey 객체 +// expiration // 만료 시간 +// ) +// } catch (e: Exception) { +// throw RuntimeException("Failed to create signed URL: ${e.message}", e) // } -// return createSignedUrl(domain, key, privateKey, keyPairId, expiration) // } - - // CloudFront 서명 URL 생성 메서드 - // createSignedUrl 메서드 구현 - private fun createSignedUrl( - domain: String, - key: String, - privateKey: String, - keyPairId: String, - expiration: Date - ): String { - try { - // 1. 임시 파일 생성 및 비공개 키 쓰기 - val tempFile = File.createTempFile("privateKey", ".pem") - tempFile.deleteOnExit() // 애플리케이션 종료 시 자동 삭제 - tempFile.writeText(privateKey) - - // 2. Private Key 객체 로드 - val privateKeyObj: PrivateKey = SignerUtils.loadPrivateKey(tempFile) - - // 3. CloudFront 서명된 URL 생성 - return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( - "$domain/$key", // 리소스 URL - keyPairId, // Key Pair ID - privateKeyObj, // PrivateKey 객체 - expiration // 만료 시간 - ) - } catch (e: Exception) { - throw RuntimeException("Failed to create signed URL: ${e.message}", e) - } - } } From 09b6b277faa3543eff4e69bb1a1c3f1dfa2fdd53 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 22:40:06 +0900 Subject: [PATCH 10/11] =?UTF-8?q?ResourceType(rooms,=20users),=20ResourceI?= =?UTF-8?q?d(roomId,=20userId)=20=EC=A1=B0=ED=9A=8C=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20URL,=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20URL=20?= =?UTF-8?q?=EC=A0=9C=EA=B3=B5=ED=95=98=EB=8A=94=20Service,=20Controller=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20+=20Entity=EC=97=90=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EB=B0=8F=20=EC=88=98=EC=A0=95=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20URL=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../toyTeam6Airbnb/Image/ImageController.kt | 53 +++++----------- .../toyTeam6Airbnb/Image/ImageService.kt | 62 ++++++++++++++++--- .../room/persistence/RoomEntity.kt | 6 +- .../user/persistence/UserEntity.kt | 4 ++ 4 files changed, 78 insertions(+), 47 deletions(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt index 39b6dd5..1fa227f 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -1,11 +1,10 @@ package com.example.toyTeam6Airbnb.Image import io.swagger.v3.oas.annotations.Operation -import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -17,44 +16,26 @@ class ImageController( summary = "Generate a presigned URL for uploading an image", description = "Generates a presigned URL for uploading an image directly to S3." ) - @GetMapping("/upload-url") - fun generateUploadUrl( - @RequestParam("key") key: String, - @RequestParam("expirationMinutes", defaultValue = "10") expirationMinutes: Long - ): ResponseEntity { - return try { - val uploadUrl = imageService.generateUploadUrl(key, expirationMinutes) - ResponseEntity.ok(uploadUrl) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to generate upload URL: ${e.message}") - } + // 업로드 URL 생성 + @PostMapping("/upload") + fun generateUploadUrl(@RequestBody request: ImageRequest): ResponseEntity> { + val uploadUrl = imageService.generateUploadUrl(request.resourceType, request.resourceId) + return ResponseEntity.ok(mapOf("uploadUrl" to uploadUrl)) } @Operation( summary = "Generate a presigned URL for downloading an image", description = "Generates a presigned URL for downloading an image directly from S3." ) - @GetMapping("/download-url") - fun generateDownloadUrl( - @RequestParam("key") key: String, - @RequestParam("expirationMinutes", defaultValue = "60") expirationMinutes: Long - ): ResponseEntity { - return try { - val downloadUrl = imageService.generateDownloadUrl(key, expirationMinutes) - ResponseEntity.ok(downloadUrl) - } catch (e: Exception) { - ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to generate download URL: ${e.message}") - } + // 다운로드 URL 생성 + @PostMapping("/download") + fun generateDownloadUrl(@RequestBody request: ImageRequest): ResponseEntity> { + val downloadUrl = imageService.generateDownloadUrl(request.resourceType, request.resourceId) + return ResponseEntity.ok(mapOf("downloadUrl" to downloadUrl)) } - -// // 내부 DTO 클래스 정의 -// data class UploadImageRequest( -// @Schema(type = "string", format = "binary", description = "The image file to upload") -// val file: MultipartFile, -// -// @Schema(description = "The key to associate with the uploaded image", example = "example-key") -// val key: String -// ) } + +data class ImageRequest( + val resourceType: String, + val resourceId: String +) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt index c9ff622..5fd8be5 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt @@ -1,5 +1,9 @@ package com.example.toyTeam6Airbnb.Image +import com.example.toyTeam6Airbnb.room.persistence.RoomRepository +import com.example.toyTeam6Airbnb.user.persistence.UserRepository +import org.springdoc.webmvc.ui.SwaggerResourceResolver +import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import software.amazon.awssdk.regions.Region @@ -11,9 +15,14 @@ import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignReques import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest import java.time.Duration import java.util.UUID +import java.util.concurrent.ConcurrentHashMap @Service -class ImageService() { +class ImageService( + @Autowired private val userRepository: UserRepository, + @Autowired private val roomRepository: RoomRepository, + private val swaggerResourceResolver: SwaggerResourceResolver +) { @Value("\${cloudfront.private-key}") private lateinit var privateKey: String @@ -31,37 +40,70 @@ class ImageService() { private val bucketName: String = "waffle-team6-storage" private val cloudFrontUrl: String = "https://d3m9s5wmwvsq01.cloudfront.net" + private val filePathMap: MutableMap = ConcurrentHashMap() // 리소스 타입과 ID로 파일 경로 매핑 // Presigned URL for Upload - fun generateUploadUrl(key: String, expirationMinutes: Long): String { - val filePath = "$key/${UUID.randomUUID()}_upload.jpg" // 고유 파일 경로 생성 + fun generateUploadUrl(resourceType: String, resourceId: String): String { + val filePath = "$resourceType/$resourceId/${UUID.randomUUID()}_image.jpg" + filePathMap["$resourceType:$resourceId"] = filePath // 리소스별 경로 매핑 + val putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) .key(filePath) .build() val presignRequest = PutObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(expirationMinutes)) + .signatureDuration(Duration.ofMinutes(60)) .putObjectRequest(putObjectRequest) .build() - return s3Presigner.presignPutObject(presignRequest).url().toString() + val uploadUrl = s3Presigner.presignPutObject(presignRequest).url().toString() + + if (resourceType == "users") { + val userId = resourceId.toLongOrNull() ?: throw RuntimeException("Invalid user ID") + val user = userRepository.findById(userId).orElseThrow { RuntimeException("User not found") } + user.imageDownloadUrl = uploadUrl + userRepository.save(user) + } else if (resourceType == "rooms") { + val roomId = resourceId.toLongOrNull() ?: throw RuntimeException("Invalid room ID") + val room = roomRepository.findById(roomId).orElseThrow { RuntimeException("Room not found") } + room.imageDownloadUrl = uploadUrl + roomRepository.save(room) + } + + return uploadUrl } // Presigned URL for Download - // 사용자가 Upload시 생성한 Key를 그대로 입력하면 됨. (UUID 없이) - fun generateDownloadUrl(key: String, expirationMinutes: Long): String { + fun generateDownloadUrl(resourceType: String, resourceId: String): String { + val filePath = filePathMap["$resourceType:$resourceId"] + ?: throw IllegalArgumentException("No file found for the given resource: $resourceType with ID: $resourceId") + val getObjectRequest = GetObjectRequest.builder() .bucket(bucketName) - .key(key) + .key(filePath) .build() val presignRequest = GetObjectPresignRequest.builder() - .signatureDuration(Duration.ofMinutes(expirationMinutes)) + .signatureDuration(Duration.ofMinutes(60)) .getObjectRequest(getObjectRequest) .build() - return s3Presigner.presignGetObject(presignRequest).url().toString() + val downloadUrl = s3Presigner.presignGetObject(presignRequest).url().toString() + + if (resourceType == "users") { + val userId = resourceId.toLongOrNull() ?: throw RuntimeException("Invalid user ID") + val user = userRepository.findById(userId).orElseThrow { RuntimeException("User not found") } + user.imageDownloadUrl = downloadUrl + userRepository.save(user) + } else if (resourceType == "rooms") { + val roomId = resourceId.toLongOrNull() ?: throw RuntimeException("Invalid room ID") + val room = roomRepository.findById(roomId).orElseThrow { RuntimeException("Room not found") } + room.imageDownloadUrl = downloadUrl + roomRepository.save(room) + } + + return downloadUrl } // // 파일 업로드 메서드 diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt index 948ad29..f667e9d 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt @@ -56,7 +56,11 @@ class RoomEntity( @Column(nullable = false) var createdAt: Instant = Instant.now(), @Column(nullable = false) - var updatedAt: Instant = Instant.now() + var updatedAt: Instant = Instant.now(), + @Column + var imageDownloadUrl: String? = null, + @Column + var imageUploadUrl: String? = null ) { @PrePersist fun onPrePersist() { diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt index 5663c3c..c4cf335 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt @@ -32,6 +32,10 @@ class UserEntity( val provider: AuthProvider, @Column var oAuthId: String? = null, + @Column + var imageDownloadUrl: String? = null, + @Column + var imageUploadUrl: String? = null, @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true) val reservations: List = mutableListOf(), From 507d1b379962d9ee9e4c4742b4e51723283ae291 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 22:48:55 +0900 Subject: [PATCH 11/11] =?UTF-8?q?URL=20=EC=A0=80=EC=9E=A5=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20LONGTEXT=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt | 4 ++-- .../com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt index f667e9d..6c0a8b0 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/room/persistence/RoomEntity.kt @@ -57,9 +57,9 @@ class RoomEntity( var createdAt: Instant = Instant.now(), @Column(nullable = false) var updatedAt: Instant = Instant.now(), - @Column + @Column(columnDefinition = "LONGTEXT") var imageDownloadUrl: String? = null, - @Column + @Column(columnDefinition = "LONGTEXT") var imageUploadUrl: String? = null ) { @PrePersist diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt index c4cf335..bedb2fb 100644 --- a/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/user/persistence/UserEntity.kt @@ -32,9 +32,9 @@ class UserEntity( val provider: AuthProvider, @Column var oAuthId: String? = null, - @Column + @Column(columnDefinition = "LONGTEXT") var imageDownloadUrl: String? = null, - @Column + @Column(columnDefinition = "LONGTEXT") var imageUploadUrl: String? = null, @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true)