Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/develop_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- develop
workflow_dispatch:

# permission can be added at job level or workflow level
permissions:
Expand Down
4 changes: 4 additions & 0 deletions docker/DockerfileDev
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ EXPOSE 8080
CMD java \
-javaagent:/opt/sentry/agent.jar \
-Dsentry.auto.init=false \
-Dotel.service.name=pida-api \
-Dotel.traces.exporter=sentry \
-Dotel.metrics.exporter=none \
-Dotel.logs.exporter=none \
-Xmx2048m \
-jar /service/pida/core-api-0.0.1.jar
4 changes: 4 additions & 0 deletions docker/DockerfileProd
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ EXPOSE 8080
CMD java \
-javaagent:/opt/sentry/agent.jar \
-Dsentry.auto.init=false \
-Dotel.service.name=pida-api \
-Dotel.traces.exporter=sentry \
-Dotel.metrics.exporter=none \
-Dotel.logs.exporter=none \
-Dspring.profiles.active=${PROFILE} \
-jar /service/pida/core-api-0.0.1.jar
1 change: 1 addition & 0 deletions pida-clients/aws-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ dependencies {
compileOnly(libs.spring.boot.starter.web)

implementation(libs.bundles.aws.client)
implementation(libs.kotlinx.coroutine.core)
implementation(project(":pida-core:core-domain"))

testImplementation(libs.spring.boot.starter.web)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.presigner.S3Presigner
import java.net.URI
Expand Down Expand Up @@ -46,6 +47,20 @@ class AwsConfig(
return client.build()
}

@Bean(destroyMethod = "close")
fun s3AsyncClient(): S3AsyncClient {
val client =
S3AsyncClient
.builder()
.credentialsProvider(credentialProvider())
.region(Region.of(awsProperties.region))
awsProperties.endpoint?.let {
client.endpointOverride(URI.create(awsProperties.endpoint))
}

return client.build()
}

@Bean(destroyMethod = "close")
fun s3Presigner(): S3Presigner {
val client =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pida.client.aws.image

import com.pida.client.aws.config.AwsProperties
import com.pida.client.aws.s3.AwsS3AsyncClient
import com.pida.client.aws.s3.AwsS3Client
import com.pida.support.aws.ImageS3Caller
import com.pida.support.aws.PresignedUrlRateLimiter
Expand All @@ -16,6 +17,7 @@ import java.time.ZoneId
@Component
class ImageS3Processor(
private val awsS3Client: AwsS3Client,
private val awsS3AsyncClient: AwsS3AsyncClient,
private val awsProperties: AwsProperties,
private val imageFileConstructor: ImageFileConstructor,
private val rateLimiter: PresignedUrlRateLimiter,
Expand All @@ -42,9 +44,11 @@ class ImageS3Processor(
Duration.ofSeconds(30), // 만료 시간 최소화
)

val s3Key = "$imageFilePath/$imageFileName"
return S3ImageUrl(
presignedUrl,
generateGetUrl(imageFilePath, imageFileName),
s3Key,
)
}

Expand Down Expand Up @@ -91,11 +95,21 @@ class ImageS3Processor(
): S3ImageInfo? {
val imageFilePath = imageFileConstructor.imageFilePath(prefix, prefixId)

return listImageObjects(imageFilePath)
return awsS3AsyncClient
.listObjects(
bucketName = awsProperties.s3.bucket,
filePath = imageFilePath,
).filterNot { it.key().endsWith("/") }
.maxByOrNull(S3Object::lastModified)
?.toImageInfo(imageFilePath, Duration.ofSeconds(30))
}

override fun generatePresignedUrl(s3Key: String): String {
val filePath = s3Key.substringBeforeLast("/")
val fileName = s3Key.substringAfterLast("/")
return generateGetUrl(filePath, fileName)
}

private fun generateGetUrl(
filePath: String,
fileName: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.pida.client.aws.s3

import kotlinx.coroutines.future.await
import org.springframework.stereotype.Component
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request
import software.amazon.awssdk.services.s3.model.S3Object

@Component
class AwsS3AsyncClient(
private val s3AsyncClient: S3AsyncClient,
) {
suspend fun listObjects(
bucketName: String,
filePath: String,
): List<S3Object> {
val allObjects = mutableListOf<S3Object>()
var continuationToken: String? = null

do {
val request =
ListObjectsV2Request
.builder()
.bucket(bucketName)
.prefix(filePath)
.maxKeys(1000)
.continuationToken(continuationToken)
.build()

val response = s3AsyncClient.listObjectsV2(request).await()
allObjects.addAll(response.contents())
continuationToken = response.nextContinuationToken()
} while (response.isTruncated)

return allObjects
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ data class CafeCategoryItemDetailPayloadResponse(
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
)
val mapUrl: String?,
@field:Schema(
description = "연결된 벚꽃길 ID",
example = "15",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
)
val flowerSpotId: Long?,
@field:Schema(
description = "최근 방문 횟수",
example = "12",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ data class MapCategoryItemDetailResponse(
CafeCategoryItemDetailPayloadResponse(
thumbnailUrl = item.thumbnailUrl,
mapUrl = item.mapUrl,
flowerSpotId = item.flowerSpotId,
recentlyVisitedCount = item.recentlyVisitedCount,
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ data class MapCategoryItemResponse(
)
val endDate: LocalDate?,
@field:Schema(
description = "벚꽃길 ID (CAFE인 경우에만 포함)",
description = "벚꽃길 ID (FLOWER_SPOT인 경우에만 포함)",
example = "15",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "꽃 명소 카페 생성 요청")
data class FlowerSpotCafeCreateRequest(
@Schema(description = "꽃 명소 ID", example = "1")
val flowerSpotId: Long,
@Schema(description = "카페 이름", example = "벚꽃 카페")
val name: String,
@Schema(description = "주소", example = "서울특별시 영등포구 여의도동", required = false)
Expand All @@ -33,7 +31,6 @@ data class FlowerSpotCafeCreateRequest(

fun toNewFlowerSpotCafe(): NewFlowerSpotCafe =
NewFlowerSpotCafe(
flowerSpotId = flowerSpotId,
name = name,
address = address,
description = description,
Expand Down
4 changes: 4 additions & 0 deletions pida-core/core-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ spring:
- weather.yml
- map.yml
- airkorea.yml
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
web.resources.add-mappings: false

server:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ data class Blooming(
val userId: Long,
val flowerSpotId: Long?,
val flowerEventId: Long?,
val flowerSpotCafeId: Long?,
val createdAt: LocalDateTime,
) {
init {
require((flowerSpotId == null) != (flowerEventId == null))
val nonNullCount = listOfNotNull(flowerSpotId, flowerEventId, flowerSpotCafeId).size
require(nonNullCount == 1)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pida.blooming

import com.pida.flowerspot.FlowerSpotService
import com.pida.reporter.RecentReporterService
import com.pida.support.aws.ImagePrefix
import com.pida.support.aws.ImageS3Caller
Expand All @@ -20,6 +21,7 @@ class BloomingFacade(
private val userService: UserService,
private val imageS3Caller: ImageS3Caller,
private val eventPublisher: ApplicationEventPublisher,
private val flowerSpotService: FlowerSpotService,
) {
suspend fun readBloomingDetailsBySpotId(flowerSpotId: Long): BloomingDetails =
coroutineScope {
Expand Down Expand Up @@ -74,13 +76,31 @@ class BloomingFacade(
suspend fun readBloomingDetails(
flowerSpotId: Long? = null,
flowerEventId: Long? = null,
flowerSpotCafeId: Long? = null,
): BloomingDetails =
when {
flowerSpotId != null && flowerEventId == null -> readBloomingDetailsBySpotId(flowerSpotId)
flowerSpotId == null && flowerEventId != null -> readBloomingDetailsByEventId(flowerEventId)
flowerSpotId != null && flowerEventId == null && flowerSpotCafeId == null -> readBloomingDetailsBySpotId(flowerSpotId)
flowerSpotId == null && flowerEventId != null && flowerSpotCafeId == null -> readBloomingDetailsByEventId(flowerEventId)
flowerSpotId == null && flowerEventId == null && flowerSpotCafeId != null -> readBloomingDetailsByCafeId(flowerSpotCafeId)
else -> throw ErrorException(ErrorType.INVALID_REQUEST)
}

private suspend fun readBloomingDetailsByCafeId(flowerSpotCafeId: Long): BloomingDetails =
coroutineScope {
val bloomings = bloomingService.recentlyBloomingByCafeId(flowerSpotCafeId)
val latestBlooming = bloomings.maxByOrNull { it.createdAt }
val userProfileDeferred =
async {
latestBlooming?.userId?.let { userService.getProfile(it) }
}

return@coroutineScope buildBloomingDetails(
bloomings = bloomings,
nickname = userProfileDeferred.await()?.nickname,
updatedAt = latestBlooming?.createdAt,
)
}

private suspend fun readBloomingDetailsByEventId(flowerEventId: Long): BloomingDetails =
coroutineScope {
val bloomings = bloomingService.recentlyBloomingByEventId(flowerEventId)
Expand All @@ -105,11 +125,19 @@ class BloomingFacade(
when (newBlooming) {
is NewBlooming.FlowerSpot -> ImagePrefix.FLOWERSPOT.value to newBlooming.flowerSpotId
is NewBlooming.FlowerEvent -> ImagePrefix.FLOWEREVENT.value to newBlooming.flowerEventId
is NewBlooming.FlowerSpotCafe -> ImagePrefix.FLOWERSPOT.value to newBlooming.flowerSpotCafeId
}

return BloomingImageUploadUrl.from(
imageS3Caller.createUploadUrl(newBlooming.userId, prefix, prefixId),
)
val imageUploadUrl = imageS3Caller.createUploadUrl(newBlooming.userId, prefix, prefixId)

if (newBlooming is NewBlooming.FlowerSpot) {
flowerSpotService.updatePreviewImageKey(
newBlooming.flowerSpotId,
imageUploadUrl.s3Key,
)
}

return BloomingImageUploadUrl.from(imageUploadUrl)
}

private fun buildBloomingDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class BloomingFinder(
flowerEventId: Long,
): Blooming? = bloomingRepository.findTopByUserIdAndEventIdDesc(userId, flowerEventId)

suspend fun readTopByUserIdAndFlowerSpotCafeIdDesc(
userId: Long,
flowerSpotCafeId: Long,
): Blooming? = bloomingRepository.findTopByUserIdAndCafeIdDesc(userId, flowerSpotCafeId)

suspend fun readAllByUserId(userId: Long): List<Blooming> = bloomingRepository.findAllByUserId(userId)

suspend fun readAllByFlowerSpotId(flowerSpotId: Long): List<Blooming> = bloomingRepository.findAllByFlowerSpotId(flowerSpotId)
Expand All @@ -24,10 +29,14 @@ class BloomingFinder(

suspend fun readRecentlyBloomingByEventId(eventId: Long): List<Blooming> = bloomingRepository.findRecentlyByEventId(eventId)

suspend fun readRecentlyBloomingByCafeId(cafeId: Long): List<Blooming> = bloomingRepository.findRecentlyByCafeId(cafeId)

fun recentlyBloomingBySpotIds(spotIds: List<Long>): List<Blooming> = bloomingRepository.findRecentBySpotIds(spotIds)

fun recentlyBloomingByEventIds(eventIds: List<Long>): List<Blooming> = bloomingRepository.findRecentByEventIds(eventIds)

fun recentlyBloomingByCafeIds(cafeIds: List<Long>): List<Blooming> = bloomingRepository.findRecentByCafeIds(cafeIds)

fun readTodayBloomingByUserId(
userId: Long,
flowerSpotId: Long,
Expand All @@ -37,4 +46,9 @@ class BloomingFinder(
userId: Long,
flowerEventId: Long,
): Blooming? = bloomingRepository.findTodayEventBloomingByUserId(userId, flowerEventId)

fun readTodayBloomingByUserIdAndFlowerSpotCafeId(
userId: Long,
flowerSpotCafeId: Long,
): Blooming? = bloomingRepository.findTodayCafeBloomingByUserId(userId, flowerSpotCafeId)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ interface BloomingRepository {
flowerEventId: Long,
): Blooming?

suspend fun findTopByUserIdAndCafeIdDesc(
userId: Long,
flowerSpotCafeId: Long,
): Blooming?

suspend fun findAllByUserId(userId: Long): List<Blooming>

suspend fun findAllByFlowerSpotId(flowerSpotId: Long): List<Blooming>
Expand All @@ -24,10 +29,14 @@ interface BloomingRepository {

suspend fun findRecentlyByEventId(eventId: Long): List<Blooming>

suspend fun findRecentlyByCafeId(cafeId: Long): List<Blooming>

fun findRecentBySpotIds(spotIds: List<Long>): List<Blooming>

fun findRecentByEventIds(eventIds: List<Long>): List<Blooming>

fun findRecentByCafeIds(cafeIds: List<Long>): List<Blooming>

fun findTodayBloomingByUserId(
userId: Long,
flowerSpotId: Long,
Expand All @@ -38,6 +47,11 @@ interface BloomingRepository {
flowerEventId: Long,
): Blooming?

fun findTodayCafeBloomingByUserId(
userId: Long,
flowerSpotCafeId: Long,
): Blooming?

fun findBloomedSpotIdsByFlowerSpotIds(spotIds: List<Long>): List<Long>

fun findBloomedEventIdsByFlowerEventIds(eventIds: List<Long>): List<Long>
Expand Down
Loading
Loading