diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8bf180f1..55e4d6b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -181,16 +181,16 @@ jobs: TASK_DEFINITION_ARN=$(aws ecs describe-task-definition --task-definition rmrt-task --query 'taskDefinition.taskDefinitionArn' --output text) aws ecs update-service \ - --cluster rmrt-cluster \ - --service rmrt-service-1 \ + --cluster rmrt-cluster-ec2version \ + --service rmrt-task-service-o4qq7b02 \ --task-definition $TASK_DEFINITION_ARN \ --force-new-deployment # 배포 완료 대기 echo "⏳ ECS 배포 완료 대기..." aws ecs wait services-stable \ - --cluster rmrt-cluster \ - --services rmrt-service-1 + --cluster rmrt-cluster-ec2version \ + --services rmrt-task-service-o4qq7b02 echo "✅ ECS 배포 완료!" @@ -198,8 +198,8 @@ jobs: run: | # 서비스 상태 확인 aws ecs describe-services \ - --cluster rmrt-cluster \ - --services rmrt-service-1 \ + --cluster rmrt-cluster-ec2version \ + --services rmrt-task-service-o4qq7b02 \ --query 'services[0].{status: status, runningCount: runningCount, desiredCount: desiredCount}' echo "🌐 실제 도메인: https://rmrt.albert-im.com" diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt index d42d1fa3..b96c9cad 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/infrastructure/s3/S3PresignedUrlGenerator.kt @@ -1,7 +1,7 @@ package com.albert.realmoneyrealtaste.adapter.infrastructure.s3 import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse import com.albert.realmoneyrealtaste.application.image.required.PresignedUrlGenerator import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value @@ -24,23 +24,22 @@ class S3PresignedUrlGenerator( private val logger = LoggerFactory.getLogger(S3PresignedUrlGenerator::class.java) - override fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPostResponse { + override fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPutResponse { val expiration = Instant.now().plus(Duration.ofMinutes(s3PutUrlExpirationMinutes)) + val metadata = mapOf( + "original-name" to request.fileName, + "content-type" to request.contentType, + "file-size" to request.fileSize.toString(), + "width" to request.width.toString(), + "height" to request.height.toString() + ) val putObjectRequest = PutObjectRequest.builder() .bucket(s3Config.bucketName) .key(imageKey) .contentType(request.contentType) - .metadata( - mapOf( - "original-name" to request.fileName, - "content-type" to request.contentType, - "file-size" to request.fileSize.toString(), - "width" to request.width.toString(), - "height" to request.height.toString() - ) - ) + .metadata(metadata) .build() val presignRequest = PutObjectPresignRequest.builder() @@ -52,11 +51,11 @@ class S3PresignedUrlGenerator( logger.info("Generated presigned PUT URL for key: $imageKey") - return PresignedPostResponse( + return PresignedPutResponse( uploadUrl = presignedRequest.url().toString(), key = imageKey, - fields = emptyMap(), // PUT 방식에서는 fields가 필요 없음 - expiresAt = expiration + expiresAt = expiration, + metadata = metadata, ) } diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt index d82acca0..ad376bce 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApi.kt @@ -4,7 +4,7 @@ import com.albert.realmoneyrealtaste.adapter.infrastructure.security.MemberPrinc import com.albert.realmoneyrealtaste.application.image.dto.ImageInfo import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadResult -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse import com.albert.realmoneyrealtaste.application.image.provided.ImageDeleter import com.albert.realmoneyrealtaste.application.image.provided.ImageReader import com.albert.realmoneyrealtaste.application.image.provided.ImageUploadRequester @@ -38,7 +38,7 @@ class ImageApi( fun requestImageUpload( @RequestBody @Valid request: ImageUploadRequest, @AuthenticationPrincipal member: MemberPrincipal, - ): ResponseEntity { + ): ResponseEntity { val response = imageUploadRequester.generatePresignedPostUrl(request, member.id) return ResponseEntity.ok(response) diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPostResponse.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPutResponse.kt similarity index 69% rename from src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPostResponse.kt rename to src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPutResponse.kt index 98b60924..d06c7578 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPostResponse.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/dto/PresignedPutResponse.kt @@ -2,9 +2,9 @@ package com.albert.realmoneyrealtaste.application.image.dto import java.time.Instant -data class PresignedPostResponse( +data class PresignedPutResponse( val uploadUrl: String, val key: String, - val fields: Map, val expiresAt: Instant, + val metadata: Map, ) diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt index 4869c295..9e4039f8 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequester.kt @@ -1,8 +1,8 @@ package com.albert.realmoneyrealtaste.application.image.provided import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse fun interface ImageUploadRequester { - fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPostResponse + fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPutResponse } diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt index 0a40b03f..dc112260 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGenerator.kt @@ -1,10 +1,10 @@ package com.albert.realmoneyrealtaste.application.image.required import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse interface PresignedUrlGenerator { - fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPostResponse + fun generatePresignedPutUrl(imageKey: String, request: ImageUploadRequest): PresignedPutResponse fun generatePresignedGetUrl(imageKey: String): String } diff --git a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt index 6f41fe05..45770ad9 100644 --- a/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt +++ b/src/main/kotlin/com/albert/realmoneyrealtaste/application/image/service/ImageUploadService.kt @@ -2,7 +2,7 @@ package com.albert.realmoneyrealtaste.application.image.service import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadRequest import com.albert.realmoneyrealtaste.application.image.dto.ImageUploadResult -import com.albert.realmoneyrealtaste.application.image.dto.PresignedPostResponse +import com.albert.realmoneyrealtaste.application.image.dto.PresignedPutResponse import com.albert.realmoneyrealtaste.application.image.exception.ImageConfirmUploadException import com.albert.realmoneyrealtaste.application.image.exception.ImageGenerateException import com.albert.realmoneyrealtaste.application.image.provided.ImageKeyGenerator @@ -32,7 +32,7 @@ class ImageUploadService( private val logger = LoggerFactory.getLogger(ImageUploadService::class.java) - override fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPostResponse { + override fun generatePresignedPostUrl(request: ImageUploadRequest, userId: Long): PresignedPutResponse { try { // 1. 사용자 검증 val todayUploadCount = imageReader.getTodayUploadCount(userId) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index b8a2cc65..a1dcb7d0 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -24,7 +24,6 @@ spring: properties: hibernate: format_sql: false - dialect: org.hibernate.dialect.MySQLDialect jdbc: time_zone: Asia/Seoul diff --git a/src/main/resources/templates/image/fragments/image-upload.html b/src/main/resources/templates/image/fragments/image-upload.html index 63d905a3..25092d59 100644 --- a/src/main/resources/templates/image/fragments/image-upload.html +++ b/src/main/resources/templates/image/fragments/image-upload.html @@ -247,10 +247,16 @@
// Presigned PUT URL 방식 사용 console.log(uploadRequest) console.log(uploadRequest.uploadUrl) + let metadata = uploadRequest.metadata const response = await fetch(uploadRequest.uploadUrl, { method: 'PUT', headers: { - 'Content-Type': file.type + 'Content-Type': metadata['content-type'], + 'x-amz-meta-content-type': metadata['content-type'], + 'x-amz-meta-file-size': metadata['file-size'], + 'x-amz-meta-height': metadata['height'], + 'x-amz-meta-original-name': metadata['original-name'], + 'x-amz-meta-width': metadata['width'] }, body: file }); diff --git a/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt b/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt index 657de872..fab07d03 100644 --- a/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt +++ b/src/test/kotlin/com/albert/realmoneyrealtaste/adapter/webapi/image/ImageApiTest.kt @@ -67,7 +67,7 @@ class ImageApiTest : IntegrationTestBase() { .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.uploadUrl").isString) .andExpect(jsonPath("$.key").isString) - .andExpect(jsonPath("$.fields").isMap) + .andExpect(jsonPath("$.metadata").isMap) .andExpect(jsonPath("$.expiresAt").isString) } diff --git a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt index f553e626..fdcd4153 100644 --- a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt +++ b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/provided/ImageUploadRequesterTest.kt @@ -44,9 +44,6 @@ class ImageUploadRequesterTest : IntegrationTestBase() { assertTrue(response.key.contains("images/")) assertTrue(response.key.endsWith(".jpg")) - assertNotNull(response.fields) - assertTrue(response.fields.isEmpty()) - // 만료 시간 확인 (기본 15분 후) val expectedMinExpiry = Instant.now().plus(Duration.ofMinutes(14)) val expectedMaxExpiry = Instant.now().plus(Duration.ofMinutes(16)) @@ -106,7 +103,6 @@ class ImageUploadRequesterTest : IntegrationTestBase() { assertNotNull(response.uploadUrl) assertNotNull(response.key) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -266,9 +262,6 @@ class ImageUploadRequesterTest : IntegrationTestBase() { assertNotNull(response.key) assertTrue(response.key.isNotBlank()) - assertNotNull(response.fields) - assertTrue(response.fields.isEmpty()) - // 만료 시간 검증 assertTrue(response.expiresAt.isAfter(Instant.now())) assertTrue(response.expiresAt.isAfter(Instant.now().plus(Duration.ofMinutes(10)))) diff --git a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt index 517fc295..72402e7a 100644 --- a/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt +++ b/src/test/kotlin/com/albert/realmoneyrealtaste/application/image/required/PresignedUrlGeneratorTest.kt @@ -38,7 +38,6 @@ class PresignedUrlGeneratorTest( assertTrue(response.uploadUrl.contains(imageKey)) assertEquals(imageKey, response.key) - assertTrue(response.fields.isEmpty()) // PUT 방식에서는 fields가 비어있음 // 만료 시간 확인 val expectedMinExpiry = Instant.now().plus(Duration.ofMinutes(14)) @@ -72,8 +71,6 @@ class PresignedUrlGeneratorTest( assertEquals(key2, response2.key) assertTrue(response1.uploadUrl.contains(key1)) assertTrue(response2.uploadUrl.contains(key2)) - assertTrue(response1.fields.isEmpty()) - assertTrue(response2.fields.isEmpty()) } @Test @@ -102,7 +99,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.uploadUrl) assertEquals(imageKey, response.key) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -133,7 +129,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.uploadUrl) assertEquals(imageKey, response.key) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -163,7 +158,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.uploadUrl) assertEquals(imageKey, response.key) assertTrue(response.uploadUrl.contains(imageKey)) - assertTrue(response.fields.isEmpty()) assertTrue(response.expiresAt.isAfter(Instant.now())) } } @@ -188,7 +182,6 @@ class PresignedUrlGeneratorTest( // Then assertNotNull(minResponse.uploadUrl) assertEquals(minImageKey, minResponse.key) - assertTrue(minResponse.fields.isEmpty()) // Given - 최대값 val maxRequest = ImageUploadRequest( @@ -208,7 +201,6 @@ class PresignedUrlGeneratorTest( // Then assertNotNull(maxResponse.uploadUrl) assertEquals(maxImageKey, maxResponse.key) - assertTrue(maxResponse.fields.isEmpty()) } @Test @@ -236,8 +228,6 @@ class PresignedUrlGeneratorTest( assertNotNull(response.key) assertEquals(imageKey, response.key) - assertNotNull(response.fields) - assertTrue(response.fields.isEmpty()) // PUT 방식에서는 fields가 비어있음 assertNotNull(response.expiresAt) assertTrue(response.expiresAt.isAfter(Instant.now())) @@ -255,7 +245,6 @@ class PresignedUrlGeneratorTest( height = 600, imageType = ImageType.POST_IMAGE ) - val expirationMinutes = 30L // When val response1 = presignedUrlGenerator.generatePresignedPutUrl(imageKey, request) diff --git a/src/test/resources/application-devdb.yml b/src/test/resources/application-devdb.yml index e654c0ac..34f9eeeb 100644 --- a/src/test/resources/application-devdb.yml +++ b/src/test/resources/application-devdb.yml @@ -1,10 +1,4 @@ spring: jpa: hibernate: - ddl-auto: validate - # Flyway 설정 - flyway: - enabled: true - baseline-on-migrate: true - baseline-version: 0 - validate-on-migrate: true + ddl-auto: create-drop