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
@@ -1,17 +1,63 @@
package com.dobby.usecase.experiment

import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway
import com.dobby.model.experiment.keyword.ExperimentPostKeyword
import com.dobby.exception.ExperimentPostKeywordsDailyLimitExceededException
import com.dobby.gateway.OpenAiGateway
import com.dobby.gateway.experiment.ExperimentPostKeywordsLogGateway
import com.dobby.gateway.member.MemberGateway
import com.dobby.model.experiment.ExperimentPostKeywordsLog
import com.dobby.model.experiment.keyword.ExperimentPostKeywords
import com.dobby.usecase.UseCase
import com.dobby.util.IdGenerator
import com.dobby.util.TimeProvider

class ExtractExperimentPostKeywordsUseCase(
private val experimentKeywordExtractionGateway: ExperimentKeywordExtractionGateway
private val openAiGateway: OpenAiGateway,
private val experimentPostKeywordsGateway: ExperimentPostKeywordsLogGateway,
private val memberGateway: MemberGateway,
private val idGenerator: IdGenerator
) : UseCase<ExtractExperimentPostKeywordsUseCase.Input, ExtractExperimentPostKeywordsUseCase.Output> {
data class Input(val text: String)
data class Output(val experimentPostKeyword: ExperimentPostKeyword)

companion object {
private const val DAILY_USAGE_LIMIT = 2
}

data class Input(
val memberId: String,
val text: String
)

data class Output(
val experimentPostKeywords: ExperimentPostKeywords
)

override fun execute(input: Input): Output {
val experimentPostKeyword = experimentKeywordExtractionGateway.extractKeywords(input.text)
val member = memberGateway.getById(input.memberId)
validateDailyUsageLimit(input.memberId)

val experimentPostKeyword = openAiGateway.extractKeywords(input.text)
val log = ExperimentPostKeywordsLog.newExperimentPostKeywordsLog(
id = idGenerator.generateId(),
member = member,
response = experimentPostKeyword
)

experimentPostKeywordsGateway.save(log)
return Output(experimentPostKeyword)
}

private fun validateDailyUsageLimit(memberId: String) {
val today = TimeProvider.currentDateTime().toLocalDate()
val startOfDay = today.atStartOfDay()
val endOfDay = today.plusDays(1).atStartOfDay()

val todayUsageCount = experimentPostKeywordsGateway.countByMemberIdAndCreatedAtBetween(
memberId = memberId,
start = startOfDay,
end = endOfDay
)

if (todayUsageCount >= DAILY_USAGE_LIMIT) {
throw ExperimentPostKeywordsDailyLimitExceededException
}
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,98 @@
package com.dobby.usecase.experiment

import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway
import com.dobby.model.experiment.keyword.ExperimentPostKeyword
import com.dobby.exception.ExperimentPostKeywordsDailyLimitExceededException
import com.dobby.gateway.OpenAiGateway
import com.dobby.gateway.experiment.ExperimentPostKeywordsLogGateway
import com.dobby.gateway.member.MemberGateway
import com.dobby.model.experiment.ExperimentPostKeywordsLog
import com.dobby.model.experiment.keyword.ExperimentPostKeywords
import com.dobby.model.member.Member
import com.dobby.util.IdGenerator
import com.dobby.util.TimeProvider
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.mockkObject
import io.mockk.unmockkAll
import java.time.LocalDateTime

class ExtractExperimentPostKeywordsUseCaseTest : BehaviorSpec({
val experimentKeywordExtractionGateway = mockk<ExperimentKeywordExtractionGateway>()
val extractExperimentPostKeywordsUseCase = ExtractExperimentPostKeywordsUseCase(experimentKeywordExtractionGateway)
val openAiGateway = mockk<OpenAiGateway>()
val experimentPostKeywordsLogGateway = mockk<ExperimentPostKeywordsLogGateway>()
val memberGateway = mockk<MemberGateway>()
val idGenerator = mockk<IdGenerator>()

given("실험 게시글 텍스트에서 키워드를 추출할 때") {
val extractExperimentPostKeywordsUseCase = ExtractExperimentPostKeywordsUseCase(
openAiGateway,
experimentPostKeywordsLogGateway,
memberGateway,
idGenerator
)

beforeSpec {
mockkObject(TimeProvider)
}

afterSpec {
unmockkAll()
}

given("일일 사용 한도를 초과하지 않은 사용자가") {
val memberId = "test_member_123"
val inputText = "남성 20-30대 대상 설문조사 참여자 모집합니다. 시간은 1시간 소요되며 참가비 10,000원 지급합니다."
val input = ExtractExperimentPostKeywordsUseCase.Input(inputText)
val mockExperimentPostKeyword = mockk<ExperimentPostKeyword>()
val input = ExtractExperimentPostKeywordsUseCase.Input(memberId, inputText)

val mockMember = mockk<Member>()
val mockExperimentPostKeywords = mockk<ExperimentPostKeywords>()
val mockLog = mockk<ExperimentPostKeywordsLog>()
val currentDateTime = LocalDateTime.of(2025, 1, 27, 10, 0, 0)

every { experimentKeywordExtractionGateway.extractKeywords(inputText) } returns mockExperimentPostKeyword
every { TimeProvider.currentDateTime() } returns currentDateTime
every { memberGateway.getById(memberId) } returns mockMember
every { idGenerator.generateId() } returns "test_log_id"
every { openAiGateway.extractKeywords(inputText) } returns mockExperimentPostKeywords
every { experimentPostKeywordsLogGateway.save(any()) } returns mockLog
every {
experimentPostKeywordsLogGateway.countByMemberIdAndCreatedAtBetween(
memberId = memberId,
start = currentDateTime.toLocalDate().atStartOfDay(),
end = currentDateTime.toLocalDate().plusDays(1).atStartOfDay()
)
} returns 1

`when`("키워드 추출을 요청하면") {
val result = extractExperimentPostKeywordsUseCase.execute(input)

then("추출된 키워드 정보를 반환해야 한다") {
result.experimentPostKeyword shouldBe mockExperimentPostKeyword
result.experimentPostKeywords shouldBe mockExperimentPostKeywords
}
}
}

given("일일 사용 한도에 도달한 사용자가") {
val memberId = "exceeded_member_456"
val inputText = "실험 참여자 모집"
val input = ExtractExperimentPostKeywordsUseCase.Input(memberId, inputText)

verify(exactly = 1) {
experimentKeywordExtractionGateway.extractKeywords(inputText)
val mockMember = mockk<Member>()
val currentDateTime = LocalDateTime.of(2025, 1, 27, 15, 30, 0)

every { TimeProvider.currentDateTime() } returns currentDateTime
every { memberGateway.getById(memberId) } returns mockMember
every {
experimentPostKeywordsLogGateway.countByMemberIdAndCreatedAtBetween(
memberId = memberId,
start = currentDateTime.toLocalDate().atStartOfDay(),
end = currentDateTime.toLocalDate().plusDays(1).atStartOfDay()
)
} returns 2

`when`("키워드 추출을 요청하면") {
then("DailyLimitExceededException 예외가 발생해야 한다") {
shouldThrow<ExperimentPostKeywordsDailyLimitExceededException> {
extractExperimentPostKeywordsUseCase.execute(input)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ data object ExperimentPostRewardException : ClientException("EP0010", "Reward ca
data object ExperimentPostContentException : ClientException("EP0011", "Content cannot be null.")
data object ExperimentPostCountException : ClientException("EP0012", "Count could be more than zero.")
data object ExperimentPostLeadResearcherException : ClientException("EP0013", "Lead Researcher cannot be null.")
data object ExperimentPostKeywordsDailyLimitExceededException : ClientException("EP0014", "Daily usage limit for experiment post keywords extraction has been exceeded.")

/**
* ServerException: Exceptions caused by internal server issues
Expand Down
7 changes: 7 additions & 0 deletions domain/src/main/kotlin/com/dobby/gateway/OpenAiGateway.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dobby.gateway

import com.dobby.model.experiment.keyword.ExperimentPostKeywords

interface OpenAiGateway {
fun extractKeywords(text: String): ExperimentPostKeywords
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dobby.gateway.experiment

import com.dobby.model.experiment.ExperimentPostKeywordsLog
import java.time.LocalDateTime

interface ExperimentPostKeywordsLogGateway {
fun save(experimentPostKeywordsLog: ExperimentPostKeywordsLog): ExperimentPostKeywordsLog
fun countByMemberIdAndCreatedAtBetween(memberId: String, start: LocalDateTime, end: LocalDateTime): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.dobby.model.experiment

import com.dobby.model.experiment.keyword.ExperimentPostKeywords
import com.dobby.model.member.Member
import com.dobby.util.TimeProvider
import java.time.LocalDateTime

data class ExperimentPostKeywordsLog(
val id: String,
val member: Member,
val response: ExperimentPostKeywords,
val createdAt: LocalDateTime
) {
companion object {
fun newExperimentPostKeywordsLog(
id: String,
member: Member,
response: ExperimentPostKeywords
) = ExperimentPostKeywordsLog(
id = id,
member = member,
response = response,
createdAt = TimeProvider.currentDateTime()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.dobby.model.experiment.keyword
import com.dobby.enums.MatchType
import com.dobby.enums.experiment.TimeSlot

data class ExperimentPostKeyword(
data class ExperimentPostKeywords(
val targetGroup: TargetGroupKeyword?,
val applyMethod: ApplyMethodKeyword?,
val matchType: MatchType?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dobby.converter

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.springframework.stereotype.Component

@Component
class JsonConverter {
private val objectMapper = jacksonObjectMapper()

fun <T> toJson(obj: T): String = objectMapper.writeValueAsString(obj)

fun <T> fromJson(json: String, clazz: Class<T>): T = objectMapper.readValue(json, clazz)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dobby.external.gateway.experiment

import com.dobby.gateway.experiment.ExperimentPostKeywordsLogGateway
import com.dobby.mapper.ExperimentPostKeywordsLogMapper
import com.dobby.model.experiment.ExperimentPostKeywordsLog
import com.dobby.persistence.repository.ExperimentPostKeywordsLogRepository
import org.springframework.stereotype.Component
import java.time.LocalDateTime

@Component
class ExperimentPostKeywordsLogGatewayImpl(
private val experimentPostKeywordsLogRepository: ExperimentPostKeywordsLogRepository,
private val mapper: ExperimentPostKeywordsLogMapper
) : ExperimentPostKeywordsLogGateway {

override fun save(experimentPostKeywordsLog: ExperimentPostKeywordsLog): ExperimentPostKeywordsLog {
val entity = mapper.fromDomain(experimentPostKeywordsLog)
val savedEntity = experimentPostKeywordsLogRepository.save(entity)
return mapper.toDomain(savedEntity)
}

override fun countByMemberIdAndCreatedAtBetween(
memberId: String,
start: LocalDateTime,
end: LocalDateTime
): Int {
return experimentPostKeywordsLogRepository.countByMemberIdAndCreatedAtBetween(memberId, start, end)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dobby.external.gateway.experiment
package com.dobby.external.openai

import com.dobby.api.dto.request.OpenAiRequest
import com.dobby.exception.CustomOpenAiCallException
Expand All @@ -7,27 +7,27 @@ import com.dobby.external.prompt.ExperimentPostKeywordMapper
import com.dobby.external.prompt.PromptTemplate
import com.dobby.external.prompt.PromptTemplateLoader
import com.dobby.external.prompt.dto.ExperimentPostKeywordDto
import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway
import com.dobby.model.experiment.keyword.ExperimentPostKeyword
import com.dobby.gateway.OpenAiGateway
import com.dobby.model.experiment.keyword.ExperimentPostKeywords
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import feign.FeignException
import org.springframework.stereotype.Component

@Component
class ExperimentKeywordExtractionGatewayImpl(
class OpenAiGatewayImpl(
private val openAiFeignClient: OpenAiFeignClient,
private val promptTemplateLoader: PromptTemplateLoader,
private val mapper: ExperimentPostKeywordMapper
) : ExperimentKeywordExtractionGateway {
) : OpenAiGateway {

private val objectMapper = jacksonObjectMapper()

private val promptTemplate: PromptTemplate by lazy {
promptTemplateLoader.loadPrompt("prompts/keyword_extraction_prompt.json")
}

override fun extractKeywords(text: String): ExperimentPostKeyword {
override fun extractKeywords(text: String): ExperimentPostKeywords {
val promptJson = objectMapper.writeValueAsString(promptTemplate)
val prompt = promptJson.replace("{{text}}", escapeJsonString(text))
val messages = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import com.dobby.external.prompt.dto.ApplyMethodDto
import com.dobby.external.prompt.dto.ExperimentPostKeywordDto
import com.dobby.external.prompt.dto.TargetGroupDto
import com.dobby.model.experiment.keyword.ApplyMethodKeyword
import com.dobby.model.experiment.keyword.ExperimentPostKeyword
import com.dobby.model.experiment.keyword.ExperimentPostKeywords
import com.dobby.model.experiment.keyword.TargetGroupKeyword
import org.springframework.stereotype.Component

@Component
class ExperimentPostKeywordMapper {

fun toDomain(dto: ExperimentPostKeywordDto): ExperimentPostKeyword {
return ExperimentPostKeyword(
fun toDomain(dto: ExperimentPostKeywordDto): ExperimentPostKeywords {
return ExperimentPostKeywords(
targetGroup = dto.targetGroup?.let { targetGroupDto ->
TargetGroupKeyword(
startAge = targetGroupDto.startAge ?: 0,
Expand Down Expand Up @@ -59,7 +59,7 @@ class ExperimentPostKeywordMapper {
)
}

fun toDto(domain: ExperimentPostKeyword): ExperimentPostKeywordDto {
fun toDto(domain: ExperimentPostKeywords): ExperimentPostKeywordDto {
return ExperimentPostKeywordDto(
targetGroup = domain.targetGroup?.let { targetGroupDomain ->
TargetGroupDto(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.dobby.mapper

import com.dobby.converter.JsonConverter
import com.dobby.model.experiment.ExperimentPostKeywordsLog
import com.dobby.model.experiment.keyword.ExperimentPostKeywords
import com.dobby.persistence.entity.experiment.ExperimentPostKeywordsLogEntity
import com.dobby.persistence.entity.member.MemberEntity
import org.springframework.stereotype.Component

@Component
class ExperimentPostKeywordsLogMapper(
private val jsonConverter: JsonConverter
) {

fun toDomain(entity: ExperimentPostKeywordsLogEntity): ExperimentPostKeywordsLog {
return ExperimentPostKeywordsLog(
id = entity.id,
member = entity.member.toDomain(),
response = jsonConverter.fromJson(entity.response, ExperimentPostKeywords::class.java),
createdAt = entity.createdAt
)
}

fun fromDomain(domain: ExperimentPostKeywordsLog): ExperimentPostKeywordsLogEntity {
return ExperimentPostKeywordsLogEntity(
id = domain.id,
member = MemberEntity.fromDomain(domain.member),
response = jsonConverter.toJson(domain.response),
createdAt = domain.createdAt
)
}
}
Loading
Loading