diff --git a/.github/workflows/cd-aws.yml b/.github/workflows/cd-aws.yml index e0ea6e9..c50466c 100644 --- a/.github/workflows/cd-aws.yml +++ b/.github/workflows/cd-aws.yml @@ -34,13 +34,6 @@ jobs: 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 diff --git a/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt new file mode 100644 index 0000000..1fa227f --- /dev/null +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageController.kt @@ -0,0 +1,41 @@ +package com.example.toyTeam6Airbnb.Image + +import io.swagger.v3.oas.annotations.Operation +import org.springframework.http.ResponseEntity +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.RestController + +@RestController +@RequestMapping("/api/images") +class ImageController( + private val imageService: ImageService +) { + @Operation( + summary = "Generate a presigned URL for uploading an image", + description = "Generates a presigned URL for uploading an image directly to S3." + ) + // 업로드 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." + ) + // 다운로드 URL 생성 + @PostMapping("/download") + fun generateDownloadUrl(@RequestBody request: ImageRequest): ResponseEntity> { + val downloadUrl = imageService.generateDownloadUrl(request.resourceType, request.resourceId) + return ResponseEntity.ok(mapOf("downloadUrl" to downloadUrl)) + } +} + +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 new file mode 100644 index 0000000..5fd8be5 --- /dev/null +++ b/src/main/kotlin/com/example/toyTeam6Airbnb/Image/ImageService.kt @@ -0,0 +1,197 @@ +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 +import software.amazon.awssdk.services.s3.S3Client +import software.amazon.awssdk.services.s3.model.GetObjectRequest +import software.amazon.awssdk.services.s3.model.PutObjectRequest +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 +import java.util.concurrent.ConcurrentHashMap + +@Service +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 + + @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" + private val filePathMap: MutableMap = ConcurrentHashMap() // 리소스 타입과 ID로 파일 경로 매핑 + + // Presigned URL for Upload + 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(60)) + .putObjectRequest(putObjectRequest) + .build() + + 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 + 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(filePath) + .build() + + val presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(60)) + .getObjectRequest(getObjectRequest) + .build() + + 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 + } + +// // 파일 업로드 메서드 +// 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 { +// 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) +// } +// } +} 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 8befff9..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,10 +57,10 @@ class RoomEntity( var createdAt: Instant = Instant.now(), @Column(nullable = false) var updatedAt: Instant = Instant.now(), - @Column - var imageUploadUrl: String? = null, - @Column - var imageDownloadUrl: String? = null + @Column(columnDefinition = "LONGTEXT") + var imageDownloadUrl: String? = null, + @Column(columnDefinition = "LONGTEXT") + 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 801a865..97c2f5b 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(columnDefinition = "LONGTEXT") + var imageDownloadUrl: String? = null, + @Column(columnDefinition = "LONGTEXT") + var imageUploadUrl: String? = null, @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true) val reservations: List = mutableListOf(),