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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.dobby.usecase.experiment.CreateExperimentPostUseCase
import com.dobby.usecase.experiment.DeleteExperimentPostUseCase
import com.dobby.usecase.experiment.ExtractExperimentPostKeywordsUseCase
import com.dobby.usecase.experiment.GenerateExperimentPostPreSignedUrlUseCase
import com.dobby.usecase.experiment.GetDailyLimitForExtractUseCase
import com.dobby.usecase.experiment.GetExperimentPostApplyMethodUseCase
import com.dobby.usecase.experiment.GetExperimentPostCountsByAreaUseCase
import com.dobby.usecase.experiment.GetExperimentPostCountsByRegionUseCase
Expand Down Expand Up @@ -43,6 +44,7 @@ class ExperimentPostService(
private val getMyExperimentPostsUseCase: GetMyExperimentPostsUseCase,
private val getMyExperimentPostTotalCountUseCase: GetMyExperimentPostTotalCountUseCase,
private val extractExperimentPostKeywordsUseCase: ExtractExperimentPostKeywordsUseCase,
private val getDailyLimitForExtractUseCase: GetDailyLimitForExtractUseCase,
private val cacheGateway: CacheGateway
) {
@Transactional
Expand Down Expand Up @@ -154,6 +156,11 @@ class ExperimentPostService(
return getMyExperimentPostTotalCountUseCase.execute(GetMyExperimentPostTotalCountUseCase.Input(input.memberId))
}

@Transactional
fun getMyDailyUsageLimit(input: GetDailyLimitForExtractUseCase.Input): GetDailyLimitForExtractUseCase.Output {
return getDailyLimitForExtractUseCase.execute(input)
}

private fun validateSortOrder(sortOrder: String): String {
return when (sortOrder) {
"ASC", "DESC" -> sortOrder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ExtractExperimentPostKeywordsUseCase(

override fun execute(input: Input): Output {
val member = memberGateway.getById(input.memberId)
// validateDailyUsageLimit(input.memberId)
validateDailyUsageLimit(input.memberId)

val experimentPostKeyword = openAiGateway.extractKeywords(input.text)
val log = ExperimentPostKeywordsLog.newExperimentPostKeywordsLog(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.dobby.usecase.experiment

import com.dobby.gateway.UsageLimitGateway
import com.dobby.usecase.UseCase
import java.time.LocalDateTime

class GetDailyLimitForExtractUseCase(
private val usageLimitGateway: UsageLimitGateway
) : UseCase<GetDailyLimitForExtractUseCase.Input, GetDailyLimitForExtractUseCase.Output> {

companion object {
private const val DAILY_USAGE_LIMIT = 2
}

data class Input(
val memberId: String
)

data class Output(
val count: Long,
val limit: Int,
val remainingCount: Long,
val resetsAt: LocalDateTime
)

override fun execute(input: Input): Output {
val snapshot = usageLimitGateway.getCurrentUsage(
memberId = input.memberId,
dailyLimit = DAILY_USAGE_LIMIT
)

return Output(
count = snapshot.count,
limit = snapshot.limit,
remainingCount = snapshot.remainingCount,
resetsAt = snapshot.resetsAt
)
}
}
6 changes: 6 additions & 0 deletions domain/src/main/kotlin/com/dobby/gateway/AlertChannel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dobby.gateway

enum class AlertChannel {
ERRORS,
NOTIFY
}
1 change: 1 addition & 0 deletions domain/src/main/kotlin/com/dobby/gateway/AlertGateway.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package com.dobby.gateway

interface AlertGateway {
fun sendError(e: Exception, requestUrl: String, clientIp: String?)
fun sendNotify(title: String, description: String, content: String?)
}
3 changes: 3 additions & 0 deletions domain/src/main/kotlin/com/dobby/gateway/UsageLimitGateway.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.dobby.gateway

import com.dobby.model.UsageSnapshot

interface UsageLimitGateway {
fun incrementAndCheckLimit(memberId: String, dailyLimit: Int): Boolean
fun getCurrentUsage(memberId: String, dailyLimit: Int): UsageSnapshot
}
10 changes: 10 additions & 0 deletions domain/src/main/kotlin/com/dobby/model/UsageSnapshot.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dobby.model

import java.time.LocalDateTime

data class UsageSnapshot(
val count: Long,
val limit: Int,
val remainingCount: Long,
val resetsAt: LocalDateTime
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dobby.config.properties

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("discord")
data class DiscordProperties(
val webhooks: Map<String, Webhook> = emptyMap()
) {
data class Webhook(
val id: String,
val token: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,23 @@ package com.dobby.external.feign.discord
import com.dobby.api.dto.request.DiscordMessageRequest
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody

@FeignClient(
name = "discord-alarm-feign-client",
url = "\${discord.webhook-url}"
url = "https://discord.com"
)
interface DiscordFeignClient {

@PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
fun sendMessage(@RequestBody discordMessageRequest: DiscordMessageRequest)
@PostMapping(
value = ["/api/webhooks/{id}/{token}"],
produces = [MediaType.APPLICATION_JSON_VALUE]
)
fun sendMessage(
@PathVariable id: String,
@PathVariable token: String,
@RequestBody discordMessageRequest: DiscordMessageRequest
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.dobby.external.gateway.cache

import com.dobby.gateway.UsageLimitGateway
import com.dobby.model.UsageSnapshot
import org.slf4j.LoggerFactory
import org.springframework.core.env.Environment
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Component
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.concurrent.TimeUnit

@Component
Expand All @@ -17,6 +20,7 @@ class RedisUsageLimitGatewayImpl(
) : UsageLimitGateway {

private val logger = LoggerFactory.getLogger(this::class.java)
private val zone = ZoneId.of("Asia/Seoul")

override fun incrementAndCheckLimit(memberId: String, dailyLimit: Int): Boolean {
val key = getCacheKey(memberId)
Expand All @@ -32,8 +36,34 @@ class RedisUsageLimitGatewayImpl(
return count <= dailyLimit
}

override fun getCurrentUsage(memberId: String, dailyLimit: Int): UsageSnapshot {
val key = getCacheKey(memberId)
val count = (redisTemplate.opsForValue().get(key)?.toLong()) ?: 0L

val ttlSeconds = redisTemplate.getExpire(key, TimeUnit.SECONDS)
val resetsAt: LocalDateTime = when {
ttlSeconds != null && ttlSeconds > 0L ->
ZonedDateTime.now(zone).plusSeconds(ttlSeconds).toLocalDateTime()
else ->
nextMidnightKST().toLocalDateTime()
}

val remainingCount = maxOf(0L, dailyLimit.toLong() - count)
return UsageSnapshot(
count = count,
limit = dailyLimit,
remainingCount = remainingCount,
resetsAt = resetsAt
)
}

private fun getCacheKey(memberId: String): String {
val activeProfile = environment.activeProfiles.firstOrNull() ?: "local"
return "$activeProfile:usage:$memberId:${LocalDate.now()}"
}

private fun nextMidnightKST(): ZonedDateTime {
val today = ZonedDateTime.now(zone).toLocalDate()
return today.plusDays(1).atStartOfDay(zone)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.dobby.external.gateway.discord

import com.dobby.api.dto.request.DiscordMessageRequest
import com.dobby.config.properties.DiscordProperties
import com.dobby.external.feign.discord.DiscordFeignClient
import com.dobby.gateway.AlertChannel
import com.dobby.gateway.AlertGateway
import org.springframework.stereotype.Component
import java.io.PrintWriter
Expand All @@ -10,14 +12,35 @@ import java.time.LocalDateTime

@Component
class AlertGatewayImpl(
private val discordProperties: DiscordProperties,
private val discordFeignClient: DiscordFeignClient
) : AlertGateway {

fun send(channel: AlertChannel, body: DiscordMessageRequest) {
val key = when (channel) {
AlertChannel.ERRORS -> "errors"
AlertChannel.NOTIFY -> "notify"
}
val hook = discordProperties.webhooks[key] ?: return
runCatching {
discordFeignClient.sendMessage(hook.id, hook.token, body)
}.onFailure {
}
}

override fun sendError(e: Exception, requestUrl: String, clientIp: String?) {
sendMessage(createMessage(e, requestUrl, clientIp))
send(AlertChannel.ERRORS, createErrorMessage(e, requestUrl, clientIp))
}

override fun sendNotify(
title: String,
description: String,
content: String?
) {
send(AlertChannel.NOTIFY, createNotifyMessage(title, description, content))
}

private fun createMessage(e: Exception, requestUrl: String, clientIp: String?): DiscordMessageRequest {
private fun createErrorMessage(e: Exception, requestUrl: String, clientIp: String?): DiscordMessageRequest {
return DiscordMessageRequest(
content = "# 🚨 에러 발생 비이이이이사아아아앙",
embeds = listOf(
Expand All @@ -26,13 +49,13 @@ class AlertGatewayImpl(
description = """
### 🕖 발생 시간
${LocalDateTime.now()}

### 🔗 요청 URL
$requestUrl

### 🖥️ 클라이언트 IP
${clientIp ?: "알 수 없음"}

### 📄 Stack Trace
```
${getStackTrace(e).substring(0, 1000)}
Expand All @@ -42,14 +65,26 @@ class AlertGatewayImpl(
)
)
}
fun createNotifyMessage(
title: String,
description: String,
content: String? = null
): DiscordMessageRequest {
val embeds = listOf(
DiscordMessageRequest.Embed(
title = "📣 $title",
description = description
)
)
return DiscordMessageRequest(
content = content,
embeds = embeds
)
}

private fun getStackTrace(e: Exception): String {
val stringWriter = StringWriter()
e.printStackTrace(PrintWriter(stringWriter))
return stringWriter.toString()
}

private fun sendMessage(request: DiscordMessageRequest) {
discordFeignClient.sendMessage(request)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.stereotype.Repository
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.Period
import java.time.format.DateTimeFormatter

@Repository
class ExperimentPostCustomRepositoryImpl(
Expand All @@ -43,6 +45,9 @@ class ExperimentPostCustomRepositoryImpl(
private lateinit var entityManager: EntityManager

private val logger: Logger = LoggerFactory.getLogger(this::class.java)
companion object {
private val LOG_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
}

override fun findExperimentPostsByCustomFilter(
customFilter: CustomFilter,
Expand Down Expand Up @@ -263,7 +268,7 @@ class ExperimentPostCustomRepositoryImpl(
)
.fetch()

logger.info("[쿼리 결과] participants count: {}", participants.size)
logger.info("[쿼리 결과] 알림 수신에 동의한 participants count: {}", participants.size)

participants.take(10).forEachIndexed { index, tuple ->
val participantEntity = tuple.get(participant)
Expand Down Expand Up @@ -307,6 +312,12 @@ class ExperimentPostCustomRepositoryImpl(
}
}.toMap()

MDC.put("match.windowStart", lastProcessedTime.format(LOG_TIME_FORMATTER))
MDC.put("match.windowEnd", currentTime.format(LOG_TIME_FORMATTER))
MDC.put("match.todayPosts", todayPosts.size.toString())
MDC.put("match.consentParticipants", participants.size.toString())
MDC.put("match.matchedRecipients", resultMap.size.toString())

logger.info("[최종 결과] 이메일을 받을 대상자 수: {}", resultMap.size)

return resultMap
Expand Down
Loading