Skip to content

Commit

Permalink
Merge pull request #27 from mash-up-kr/yaeoni/presigned-url-image
Browse files Browse the repository at this point in the history
feat; 이미지 업로드 정보 조회 API 작성해요 (presigned url 활용)
  • Loading branch information
yaeoni authored Jun 29, 2024
2 parents b646789 + 8d1814b commit 0b54fea
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 5 deletions.
5 changes: 5 additions & 0 deletions _endpoint_test/http-client.env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"local": {
"host": "localhost:8080"
}
}
2 changes: 2 additions & 0 deletions _endpoint_test/image.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### 이미지 업로드 url 조회하기
GET {{host}}/image-upload-url?contentType=PNG
48 changes: 48 additions & 0 deletions api/src/main/kotlin/com/mashup/dojo/ImageController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.mashup.dojo

import com.mashup.dojo.common.DojoApiResponse
import com.mashup.dojo.external.aws.ImageContentType
import com.mashup.dojo.external.aws.ImageUploadUrlProvider
import com.mashup.dojo.usecase.ImageUseCase
import io.github.oshai.kotlinlogging.KotlinLogging
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

private val logger = KotlinLogging.logger { }

@Tag(name = "Image", description = "이미지 API")
@RestController
class ImageController(
private val imageUploadUrlProvider: ImageUploadUrlProvider,
private val imageUseCase: ImageUseCase,
) {
@GetMapping("image-upload-url")
@Operation(
summary = "단일 이미지 업로드 정보 조회 (URL, UUID)",
description = "이미지 파일을 업로드하기 위한 presigned url 과 uuid 를 반환합니다. PUT API를 통해 호출해주세요",
responses = [
ApiResponse(responseCode = "200", description = "업로드된 이미지 id")
]
)
fun uploadInfo(contentType: ImageContentType): DojoApiResponse<ImageUploadUrlResponse> {
logger.info { "read image upload info, contentType: ${contentType.value}" }

val uploadUrlInfo = imageUploadUrlProvider.createUploadUrl(contentType)
imageUseCase.uploadImage(uploadUrlInfo.uuid, uploadUrlInfo.imageUrl)

return DojoApiResponse.success(
ImageUploadUrlResponse(
uuid = uploadUrlInfo.uuid,
uploadUrl = uploadUrlInfo.uploadUrl.toString()
)
)
}

data class ImageUploadUrlResponse(
val uuid: String,
val uploadUrl: String,
)
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,6 @@ project(":entity") {

project(":common") {
dependencies {
implementation("com.amazonaws:aws-java-sdk-s3:1.12.752")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.mashup.dojo.external

import com.amazonaws.auth.AWSStaticCredentialsProvider
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.regions.Regions
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.AmazonS3ClientBuilder
import com.mashup.dojo.external.aws.ImageUploadUrlProvider
import com.mashup.dojo.external.aws.S3ImageUploadUrlProvider
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment

@Configuration
class ExternalConfiguration {
@Bean
fun s3Client(environment: Environment): AmazonS3Client {
val accessKey = environment.getProperty("aws.access-key")
val secretKey = environment.getProperty("aws.secret-key")
val credential = BasicAWSCredentials(accessKey, secretKey)
return AmazonS3ClientBuilder.standard()
.withCredentials(
AWSStaticCredentialsProvider(credential)
)
.withRegion(Regions.AP_NORTHEAST_2)
.build() as AmazonS3Client
}

@Bean
fun imageUploader(s3Client: AmazonS3Client): ImageUploadUrlProvider {
return S3ImageUploadUrlProvider(s3Client)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.mashup.dojo.external.aws

import com.amazonaws.HttpMethod
import com.amazonaws.services.s3.AmazonS3Client
import java.net.URL
import java.time.Duration
import java.util.Date
import java.util.UUID

interface ImageUploadUrlProvider {
fun createUploadUrl(contentType: ImageContentType): ImageUploadUrl
}

data class ImageUploadUrl(
val uuid: String,
val imageUrl: String,
val uploadUrl: URL,
)

enum class ImageContentType(val value: String) {
PNG("png"),
JPEG("jpeg"),
}

class S3ImageUploadUrlProvider(
private val client: AmazonS3Client,
) : ImageUploadUrlProvider {
override fun createUploadUrl(contentType: ImageContentType): ImageUploadUrl {
val uuid = UUID.randomUUID().toString()
val path = "images/$uuid.${contentType.value}"

val uploadUrl =
client.generatePresignedUrl(
IMAGE_BUCKET_NAME,
path,
Date(System.currentTimeMillis() + Duration.ofMinutes(5).toMillis()),
HttpMethod.PUT
)

return ImageUploadUrl(
uuid = uuid,
imageUrl = "https://$IMAGE_BUCKET_NAME.s3.ap-northeast-2.amazonaws.com/$path",
uploadUrl = uploadUrl
)
}

companion object {
private const val IMAGE_BUCKET_NAME = "dojo-backend-source-bundle"
}
}
3 changes: 3 additions & 0 deletions common/src/main/resources/application-common.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
aws:
access-key: ${AWS_ACCESS_KEY}
secret-key: ${AWS_SECRET_KEY}
15 changes: 15 additions & 0 deletions entity/src/main/kotlin/com/mashup/dojo/ImageRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mashup.dojo

import com.mashup.dojo.base.BaseEntity
import jakarta.persistence.Entity
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface ImageRepository : JpaRepository<ImageEntity, Long>

@Entity
class ImageEntity(
val uuid: String,
val url: String,
) : BaseEntity()
4 changes: 2 additions & 2 deletions service/src/main/kotlin/com/mashup/dojo/domain/Image.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.mashup.dojo.domain

@JvmInline
value class ImageId(val value: Long)
value class ImageId(val value: String)

data class Image(
val id: ImageId,
Expand All @@ -10,7 +10,7 @@ data class Image(
companion object {
val MOCK_USER_IMAGE =
Image(
id = ImageId(1),
id = ImageId("12345678"),
url = "https://example.com/image/1"
)
}
Expand Down
25 changes: 23 additions & 2 deletions service/src/main/kotlin/com/mashup/dojo/service/ImageService.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
package com.mashup.dojo.service

import com.mashup.dojo.ImageEntity
import com.mashup.dojo.ImageRepository
import com.mashup.dojo.domain.Image
import com.mashup.dojo.domain.ImageId
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

interface ImageService {
fun load(imageId: ImageId): Image

fun save(
uuid: String,
imageUrl: String,
): ImageId
}

@Transactional(readOnly = true)
@Service
class MockImageService() : ImageService {
class DefaultImageService(
private val imageRepository: ImageRepository,
) : ImageService {
@Transactional
override fun save(
uuid: String,
imageUrl: String,
): ImageId {
val entity = ImageEntity(uuid = uuid, url = imageUrl)
val saved = imageRepository.save(entity)
return ImageId(saved.uuid)
}

override fun load(imageId: ImageId): Image {
return Image.MOCK_USER_IMAGE
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class DefaultQuestionService : QuestionService {
content = "세상에서 제일 멋쟁이인 사람",
type = QuestionType.FRIEND,
category = QuestionCategory.ROMANCE,
emojiImageId = ImageId(1),
emojiImageId = ImageId("345678"),
createdAt = LocalDateTime.now(),
deletedAt = null
)
Expand Down
12 changes: 12 additions & 0 deletions service/src/main/kotlin/com/mashup/dojo/usecase/ImageUseCase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import org.springframework.stereotype.Component

interface ImageUseCase {
fun loadImage(imageId: ImageId): Image

fun uploadImage(
uuid: String,
imageUrl: String,
): ImageId
}

@Component
Expand All @@ -16,4 +21,11 @@ class DefaultImageUseCase(
override fun loadImage(imageId: ImageId): Image {
return imageService.load(imageId)
}

override fun uploadImage(
uuid: String,
imageUrl: String,
): ImageId {
return imageService.save(uuid = uuid, imageUrl = imageUrl)
}
}

0 comments on commit 0b54fea

Please sign in to comment.