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
)
}
}
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
@@ -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
Expand Up @@ -7,6 +7,7 @@ import com.dobby.api.dto.request.experiment.UpdateExperimentPostRequest
import com.dobby.api.dto.response.PaginatedResponse
import com.dobby.api.dto.response.PreSignedUrlResponse
import com.dobby.api.dto.response.experiment.CreateExperimentPostResponse
import com.dobby.api.dto.response.experiment.DailyUsageSnapshotResponse
import com.dobby.api.dto.response.experiment.ExperimentPostApplyMethodResponse
import com.dobby.api.dto.response.experiment.ExperimentPostCountsResponse
import com.dobby.api.dto.response.experiment.ExperimentPostDetailResponse
Expand Down Expand Up @@ -233,4 +234,16 @@ class ExperimentPostController(
val output = experimentPostService.extractExperimentPostKeywords(input)
return ExperimentPostMapper.toExtractKeywordResponse(output)
}

@PreAuthorize("hasRole('RESEARCHER')")
@GetMapping("/usage-limit")
@Operation(
summary = "실험 공고 키워드 추출 일일 사용량 조회 API",
description = "실험 공고 키워드 추출의 남은 일일 사용량 및 정보를 조회합니다."
)
fun getDailyUsageForExtract(): DailyUsageSnapshotResponse {
val input = ExperimentPostMapper.toDailyUsageSnapshotInput()
val output = experimentPostService.getMyDailyUsageLimit(input)
return ExperimentPostMapper.toDailyUsageSnapshotResponse(output)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dobby.api.dto.response.experiment

import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

@Schema(description = "AI 공고 등록 일일 사용 횟수 관련 응답")
data class DailyUsageSnapshotResponse(

@Schema(description = "현재 사용한 횟수", example = "1")
val count: Long,

@Schema(description = "현재 사용한 횟수", example = "3")
val limit: Int,

@Schema(description = "현재 남은 사용 횟수", example = "2")
val remainingCount: Long,

@Schema(description = "초기화 시각", example = "2025-11-01T05:59:28.565Z")
val resetsAt: LocalDateTime
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.dobby.api.dto.request.experiment.UpdateExperimentPostRequest
import com.dobby.api.dto.response.PaginatedResponse
import com.dobby.api.dto.response.PreSignedUrlResponse
import com.dobby.api.dto.response.experiment.CreateExperimentPostResponse
import com.dobby.api.dto.response.experiment.DailyUsageSnapshotResponse
import com.dobby.api.dto.response.experiment.DataCount
import com.dobby.api.dto.response.experiment.DurationInfo
import com.dobby.api.dto.response.experiment.ExperimentPostApplyMethodResponse
Expand All @@ -29,6 +30,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 @@ -540,4 +542,19 @@ object ExperimentPostMapper {
experimentPostKeywords = output.experimentPostKeywords
)
}

fun toDailyUsageSnapshotInput(): GetDailyLimitForExtractUseCase.Input {
return GetDailyLimitForExtractUseCase.Input(
memberId = getCurrentMemberId()
)
}

fun toDailyUsageSnapshotResponse(output: GetDailyLimitForExtractUseCase.Output): DailyUsageSnapshotResponse {
return DailyUsageSnapshotResponse(
count = output.count,
limit = output.limit,
remainingCount = output.remainingCount,
resetsAt = output.resetsAt
)
}
}
Loading