From 09b6b277faa3543eff4e69bb1a1c3f1dfa2fdd53 Mon Sep 17 00:00:00 2001 From: Junbye Date: Tue, 14 Jan 2025 22:40:06 +0900 Subject: [PATCH] =?UTF-8?q?ResourceType(rooms,=20users),=20ResourceId(room?= =?UTF-8?q?Id,=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(),