From 1a620cd0bb82c5bbc23abdd929b23e2e29e3662a Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 15:23:05 +0900 Subject: [PATCH 01/16] feat: add ExperimentKeyword for prompt --- .../model/experiment/keyword/ApplyMethodKeyword.kt | 9 +++++++++ .../experiment/keyword/ExperimentPostKeyword.kt | 13 +++++++++++++ .../model/experiment/keyword/TargetGroupKeyword.kt | 10 ++++++++++ 3 files changed, 32 insertions(+) create mode 100644 domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt create mode 100644 domain/src/main/kotlin/com/dobby/model/experiment/keyword/ExperimentPostKeyword.kt create mode 100644 domain/src/main/kotlin/com/dobby/model/experiment/keyword/TargetGroupKeyword.kt diff --git a/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt new file mode 100644 index 00000000..5bb8b851 --- /dev/null +++ b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt @@ -0,0 +1,9 @@ +package com.dobby.model.experiment.keyword + +data class ApplyMethodKeyword( + val content: String, + val isFormUrl: Boolean, + val formUrl: String, + val isPhoneNum: Boolean, + val phoneNum: String +) diff --git a/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ExperimentPostKeyword.kt b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ExperimentPostKeyword.kt new file mode 100644 index 00000000..cea8f404 --- /dev/null +++ b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ExperimentPostKeyword.kt @@ -0,0 +1,13 @@ +package com.dobby.model.experiment.keyword + +import com.dobby.enums.MatchType +import com.dobby.enums.experiment.TimeSlot + +data class ExperimentPostKeyword( + val targetGroup: TargetGroupKeyword?, + val applyMethod: ApplyMethodKeyword?, + val matchType: MatchType?, + val reward: String?, + val count: Int?, + val timeRequired: TimeSlot? +) diff --git a/domain/src/main/kotlin/com/dobby/model/experiment/keyword/TargetGroupKeyword.kt b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/TargetGroupKeyword.kt new file mode 100644 index 00000000..04b3ce6c --- /dev/null +++ b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/TargetGroupKeyword.kt @@ -0,0 +1,10 @@ +package com.dobby.model.experiment.keyword + +import com.dobby.enums.member.GenderType + +data class TargetGroupKeyword( + var startAge: Int?, + var endAge: Int?, + var genderType: GenderType?, + var otherCondition: String? +) From 06cefe14e61295bb1365e8f4b9bab38923c8cd2b Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 15:27:49 +0900 Subject: [PATCH 02/16] feat: add ExperimentKeywordExtractionGateway with OpenAI integration --- .../ExperimentKeywordExtractionGateway.kt | 7 ++ .../experiment/keyword/ApplyMethodKeyword.kt | 6 +- .../ExperimentKeywordExtractionGatewayImpl.kt | 52 ++++++++++++++ .../dobby/external/prompt/ApplyMethodDto.kt | 9 +++ .../prompt/ExperimentPostKeywordDto.kt | 16 +++++ .../prompt/ExperimentPostKeywordMapper.kt | 70 +++++++++++++++++++ .../dobby/external/prompt/TargetGroupDto.kt | 8 +++ 7 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 domain/src/main/kotlin/com/dobby/gateway/experiment/ExperimentKeywordExtractionGateway.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt diff --git a/domain/src/main/kotlin/com/dobby/gateway/experiment/ExperimentKeywordExtractionGateway.kt b/domain/src/main/kotlin/com/dobby/gateway/experiment/ExperimentKeywordExtractionGateway.kt new file mode 100644 index 00000000..a189843d --- /dev/null +++ b/domain/src/main/kotlin/com/dobby/gateway/experiment/ExperimentKeywordExtractionGateway.kt @@ -0,0 +1,7 @@ +package com.dobby.gateway.experiment + +import com.dobby.model.experiment.keyword.ExperimentPostKeyword + +interface ExperimentKeywordExtractionGateway { + fun extractKeywords(text: String): ExperimentPostKeyword +} diff --git a/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt index 5bb8b851..83e11696 100644 --- a/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt +++ b/domain/src/main/kotlin/com/dobby/model/experiment/keyword/ApplyMethodKeyword.kt @@ -1,9 +1,9 @@ package com.dobby.model.experiment.keyword data class ApplyMethodKeyword( - val content: String, + val content: String?, val isFormUrl: Boolean, - val formUrl: String, + val formUrl: String?, val isPhoneNum: Boolean, - val phoneNum: String + val phoneNum: String? ) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt new file mode 100644 index 00000000..d1216dae --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -0,0 +1,52 @@ +package com.dobby.external.gateway.experiment + +import com.dobby.api.dto.request.OpenAiRequest +import com.dobby.external.feign.openAi.OpenAiFeignClient +import com.dobby.external.prompt.ExperimentPostKeywordDto +import com.dobby.external.prompt.ExperimentPostKeywordMapper +import com.dobby.external.prompt.PromptTemplate +import com.dobby.external.prompt.PromptTemplateLoader +import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway +import com.dobby.model.experiment.keyword.ExperimentPostKeyword +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.springframework.stereotype.Service + +@Service +class ExperimentKeywordExtractionGatewayImpl( + private val openAiFeignClient: OpenAiFeignClient, + private val promptTemplateLoader: PromptTemplateLoader, + private val mapper: ExperimentPostKeywordMapper +) : ExperimentKeywordExtractionGateway { + + private val promptTemplate: PromptTemplate by lazy { + promptTemplateLoader.loadPrompt("prompts/experiment_info_extraction_prompt.json") + } + + override fun extractKeywords(text: String): ExperimentPostKeyword { + val objectMapper = jacksonObjectMapper() + val promptJson = objectMapper.writeValueAsString(promptTemplate) + val prompt = promptJson.replace("{{text}}", text) + + val messages = listOf( + OpenAiRequest.Message(role = "user", content = prompt) + ) + + val request = OpenAiRequest( + model = "gpt-4o", + temperature = 0.2, + messages = messages + ) + + val response = openAiFeignClient.chatCompletion(request) + val content = response.choices.firstOrNull()?.message?.content + ?: throw IllegalStateException("OpenAI 응답 없음") + + return try { + val dto = objectMapper.readValue(content) + mapper.toDomain(dto) + } catch (e: Exception) { + throw IllegalStateException("응답 파싱 실패: $content", e) + } + } +} diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt new file mode 100644 index 00000000..aa99358d --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt @@ -0,0 +1,9 @@ +package com.dobby.external.prompt + +data class ApplyMethodDto( + val content: String?, + val isFormUrl: Boolean, + val formUrl: String?, + val isPhoneNum: Boolean, + val phoneNum: String? +) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt new file mode 100644 index 00000000..5a27bf75 --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt @@ -0,0 +1,16 @@ +package com.dobby.external.prompt + +import com.dobby.enums.MatchType +import com.dobby.enums.experiment.TimeSlot +import com.fasterxml.jackson.annotation.JsonProperty + +data class ExperimentPostKeywordDto( + @JsonProperty("targetGroupInfo") + val targetGroup: TargetGroupDto?, + @JsonProperty("applyMethodInfo") + val applyMethod: ApplyMethodDto?, + val matchType: MatchType?, + val reward: String?, + val count: Int?, + val timeRequired: TimeSlot? +) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt new file mode 100644 index 00000000..3bbd1bf3 --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt @@ -0,0 +1,70 @@ +package com.dobby.external.prompt + +import com.dobby.enums.member.GenderType +import com.dobby.model.experiment.keyword.ApplyMethodKeyword +import com.dobby.model.experiment.keyword.ExperimentPostKeyword +import com.dobby.model.experiment.keyword.TargetGroupKeyword +import org.springframework.stereotype.Component + +@Component +class ExperimentPostKeywordMapper { + + fun toDomain(dto: ExperimentPostKeywordDto): ExperimentPostKeyword { + return ExperimentPostKeyword( + targetGroup = dto.targetGroup?.let { + TargetGroupKeyword( + startAge = it.startAge, + endAge = it.endAge, + genderType = it.genderType?.let { genderStr -> + when(genderStr) { + "MALE" -> GenderType.MALE + "FEMALE" -> GenderType.FEMALE + "ALL" -> GenderType.ALL + else -> null + } + }, + otherCondition = it.otherCondition + ) + }, + applyMethod = dto.applyMethod?.let { + ApplyMethodKeyword( + content = it.content, + isFormUrl = it.isFormUrl, + formUrl = it.formUrl, + isPhoneNum = it.isPhoneNum, + phoneNum = it.phoneNum + ) + }, + matchType = dto.matchType, + reward = dto.reward, + count = dto.count, + timeRequired = dto.timeRequired + ) + } + + fun toDto(domain: ExperimentPostKeyword): ExperimentPostKeywordDto { + return ExperimentPostKeywordDto( + targetGroup = domain.targetGroup?.let { + TargetGroupDto( + startAge = it.startAge, + endAge = it.endAge, + genderType = it.genderType?.name, + otherCondition = it.otherCondition + ) + }, + applyMethod = domain.applyMethod?.let { + ApplyMethodDto( + content = it.content, + isFormUrl = it.isFormUrl, + formUrl = it.formUrl, + isPhoneNum = it.isPhoneNum, + phoneNum = it.phoneNum + ) + }, + matchType = domain.matchType, + reward = domain.reward, + count = domain.count, + timeRequired = domain.timeRequired + ) + } +} diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt new file mode 100644 index 00000000..cf4a5d6f --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt @@ -0,0 +1,8 @@ +package com.dobby.external.prompt + +data class TargetGroupDto( + val startAge: Int?, + val endAge: Int?, + val genderType: String?, + val otherCondition: String? +) From d0846a3e0b6517dd43dd53f663cc9680be1d178d Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 15:30:15 +0900 Subject: [PATCH 03/16] feat: configure OpenAI API client with Feign --- .../com/dobby/config/OpenAiFeignConfig.kt | 19 +++++++++++++++++++ .../config/properties/OpenAiProperties.kt | 14 ++++++++++++++ .../feign/openAi/OpenAiFeignClient.kt | 18 ++++++++++++++++++ .../dobby/api/dto/request/OpenAiRequest.kt | 12 ++++++++++++ .../dobby/api/dto/response/OpenAiResponse.kt | 14 ++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/config/properties/OpenAiProperties.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/feign/openAi/OpenAiFeignClient.kt create mode 100644 presentation/src/main/kotlin/com/dobby/api/dto/request/OpenAiRequest.kt create mode 100644 presentation/src/main/kotlin/com/dobby/api/dto/response/OpenAiResponse.kt diff --git a/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt b/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt new file mode 100644 index 00000000..7a2523d9 --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt @@ -0,0 +1,19 @@ +package com.dobby.config + +import com.dobby.config.properties.OpenAiProperties +import feign.RequestInterceptor +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class OpenAiFeignConfig( + private val openAiProperties: OpenAiProperties +) { + @Bean + fun openAiRequestInterceptor(): RequestInterceptor { + return RequestInterceptor { template -> + template.header("Authorization", "Bearer ${openAiProperties.api.key}") + template.header("Content-Type", "application/json") + } + } +} diff --git a/infrastructure/src/main/kotlin/com/dobby/config/properties/OpenAiProperties.kt b/infrastructure/src/main/kotlin/com/dobby/config/properties/OpenAiProperties.kt new file mode 100644 index 00000000..b6ae0b5f --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/config/properties/OpenAiProperties.kt @@ -0,0 +1,14 @@ +package com.dobby.config.properties + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.stereotype.Component + +@Component +@ConfigurationProperties(prefix = "openai") +data class OpenAiProperties( + var api: Api = Api() +) { + data class Api( + var key: String = "" + ) +} diff --git a/infrastructure/src/main/kotlin/com/dobby/external/feign/openAi/OpenAiFeignClient.kt b/infrastructure/src/main/kotlin/com/dobby/external/feign/openAi/OpenAiFeignClient.kt new file mode 100644 index 00000000..514c412a --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/feign/openAi/OpenAiFeignClient.kt @@ -0,0 +1,18 @@ +package com.dobby.external.feign.openAi + +import com.dobby.api.dto.request.OpenAiRequest +import com.dobby.api.dto.response.OpenAiResponse +import com.dobby.config.OpenAiFeignConfig +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody + +@FeignClient( + name = "openAiClient", + url = "https://api.openai.com/v1", + configuration = [OpenAiFeignConfig::class] +) +interface OpenAiFeignClient { + @PostMapping("/chat/completions") + fun chatCompletion(@RequestBody request: OpenAiRequest): OpenAiResponse +} diff --git a/presentation/src/main/kotlin/com/dobby/api/dto/request/OpenAiRequest.kt b/presentation/src/main/kotlin/com/dobby/api/dto/request/OpenAiRequest.kt new file mode 100644 index 00000000..058c4b04 --- /dev/null +++ b/presentation/src/main/kotlin/com/dobby/api/dto/request/OpenAiRequest.kt @@ -0,0 +1,12 @@ +package com.dobby.api.dto.request + +data class OpenAiRequest( + val model: String, + val temperature: Double, + val messages: List +) { + data class Message( + val role: String, + val content: String + ) +} diff --git a/presentation/src/main/kotlin/com/dobby/api/dto/response/OpenAiResponse.kt b/presentation/src/main/kotlin/com/dobby/api/dto/response/OpenAiResponse.kt new file mode 100644 index 00000000..510ed079 --- /dev/null +++ b/presentation/src/main/kotlin/com/dobby/api/dto/response/OpenAiResponse.kt @@ -0,0 +1,14 @@ +package com.dobby.api.dto.response + +data class OpenAiResponse( + val choices: List +) { + data class Choice( + val message: Message + ) + + data class Message( + val role: String, + val content: String + ) +} From 2894018f9b14f4a769639a817a4cb0ca7b336098 Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 15:31:24 +0900 Subject: [PATCH 04/16] feat: add prompt json file for keyword extraction --- .../dobby/external/prompt/PromptTemplate.kt | 9 ++++ .../external/prompt/PromptTemplateLoader.kt | 19 +++++++++ .../prompts/keyword_extraction_prompt.json | 41 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplate.kt create mode 100644 infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt create mode 100644 infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplate.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplate.kt new file mode 100644 index 00000000..47db413e --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplate.kt @@ -0,0 +1,9 @@ +package com.dobby.external.prompt + +data class PromptTemplate( + val description: String, + val input: Map, + val extract_items: List, + val output_format: Map, + val conditions: List +) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt new file mode 100644 index 00000000..5016a162 --- /dev/null +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt @@ -0,0 +1,19 @@ +package com.dobby.external.prompt + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.springframework.stereotype.Service + +@Service +class PromptTemplateLoader { + + private val mapper = jacksonObjectMapper() + + fun loadPrompt(resourcePath: String): PromptTemplate { + val resource = this::class.java.classLoader.getResource(resourcePath) + ?: throw IllegalArgumentException("프롬프트 파일을 찾을 수 없습니다: $resourcePath") + + return resource.openStream().use { inputStream -> + mapper.readValue(inputStream, PromptTemplate::class.java) + } + } +} diff --git a/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json b/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json new file mode 100644 index 00000000..5250f86a --- /dev/null +++ b/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json @@ -0,0 +1,41 @@ +{ + "description": "공고 게시글에서 실험 참여 관련 정보를 추출하여 JSON 형태로 반환하세요.", + "input": { + "text": "{{text}}" + }, + "extract_items": [ + "진행 방식 (대면 / 비대면 / 대면+비대면 중 선택)", + "참여 보상 (주관식)", + "실험에 참여하려면 어떻게 하나요? (주관식, URL 및 전화번호 포함 가능)", + "참여 대상 나이 (시작 나이 - 끝 나이)", + "참여 대상 성별 (남성 / 여성 / 무관)", + "참여 대상 기타 조건 (주관식)", + "참여 횟수 (숫자)", + "소요 시간 (예: 30분, 1시간, 1시간 30분, 등)" + ], + "output_format": { + "targetGroupInfo": { + "startAge": "시작 나이 (숫자)", + "endAge": "끝 나이 (숫자)", + "genderType": "MALE/FEMALE/ALL", + "otherCondition": "기타 조건 (문자열)" + }, + "applyMethodInfo": { + "content": "참여 방법 설명 (문자열)", + "isFormUrl": "URL 포함 여부 (boolean)", + "formUrl": "신청 URL (문자열)", + "isPhoneNum": "전화번호 포함 여부 (boolean)", + "phoneNum": "전화번호 (문자열)" + }, + "matchType": "OFFLINE/ONLINE/ALL", + "reward": "참여 보상 (문자열)", + "count": "참여 횟수 (숫자)", + "timeRequired": "LESS_30M/ABOUT_30M/ABOUT_1H/ABOUT_1H30M/ABOUT_2H/ABOUT_2H30M/ABOUT_3H/ABOUT_3H30M/ABOUT_4H" + }, + "conditions": [ + "공고 본문을 정확히 분석하여 각 항목에 맞는 정보를 추출한다.", + "추출된 정보는 반드시 output_format에 따라 JSON 형태로 출력한다.", + "정보가 명시되지 않은 경우 해당 필드는 빈 문자열(\"\") 또는 0으로 설정한다.", + "URL이나 전화번호가 포함된 경우 해당 boolean 값을 true로 설정한다." + ] +} From e810c00f7c3293458a39ffac702d225b0e6735ca Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:05:58 +0900 Subject: [PATCH 05/16] fix: fix typo --- .../ExperimentKeywordExtractionGatewayImpl.kt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt index d1216dae..62fe060f 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -20,7 +20,7 @@ class ExperimentKeywordExtractionGatewayImpl( ) : ExperimentKeywordExtractionGateway { private val promptTemplate: PromptTemplate by lazy { - promptTemplateLoader.loadPrompt("prompts/experiment_info_extraction_prompt.json") + promptTemplateLoader.loadPrompt("prompts/keyword_extraction_prompt.json") } override fun extractKeywords(text: String): ExperimentPostKeyword { @@ -43,10 +43,23 @@ class ExperimentKeywordExtractionGatewayImpl( ?: throw IllegalStateException("OpenAI 응답 없음") return try { - val dto = objectMapper.readValue(content) + // 마크다운 코드 블록 제거 및 JSON 정리 + val cleanedContent = cleanJsonResponse(content) + val dto = objectMapper.readValue(cleanedContent) mapper.toDomain(dto) } catch (e: Exception) { throw IllegalStateException("응답 파싱 실패: $content", e) } } + + private fun cleanJsonResponse(content: String): String { + return content.trim() + // 마크다운 코드 블록 제거 + .replace(Regex("^```json\\s*"), "") + .replace(Regex("```\\s*$"), "") + // 시작/끝 백틱 제거 + .replace(Regex("^`+"), "") + .replace(Regex("`+$"), "") + .trim() + } } From d3c9469c79768ff069ba2565a0ae394f89f48256 Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:08:05 +0900 Subject: [PATCH 06/16] feat: add extractExperimentPostKeywords api --- .../dobby/service/ExperimentPostService.kt | 21 +++------ .../ExtractExperimentPostKeywordsUseCase.kt | 17 ++++++++ .../controller/ExperimentPostController.kt | 23 +++++++--- .../experiment/ExtactKeywordRequest.kt | 10 +++++ .../experiment/ExtractKeywordResponse.kt | 9 ++++ .../dobby/api/mapper/ExperimentPostMapper.kt | 43 +++++++------------ 6 files changed, 74 insertions(+), 49 deletions(-) create mode 100644 application/src/main/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCase.kt create mode 100644 presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtactKeywordRequest.kt create mode 100644 presentation/src/main/kotlin/com/dobby/api/dto/response/experiment/ExtractKeywordResponse.kt diff --git a/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt b/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt index 584b7c60..6282a9ad 100644 --- a/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt +++ b/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt @@ -6,21 +6,7 @@ import com.dobby.exception.ExperimentAreaInCorrectException import com.dobby.exception.ExperimentAreaOverflowException import com.dobby.exception.InvalidRequestValueException import com.dobby.gateway.CacheGateway -import com.dobby.usecase.experiment.CreateExperimentPostUseCase -import com.dobby.usecase.experiment.DeleteExperimentPostUseCase -import com.dobby.usecase.experiment.GenerateExperimentPostPreSignedUrlUseCase -import com.dobby.usecase.experiment.GetExperimentPostApplyMethodUseCase -import com.dobby.usecase.experiment.GetExperimentPostCountsByAreaUseCase -import com.dobby.usecase.experiment.GetExperimentPostCountsByRegionUseCase -import com.dobby.usecase.experiment.GetExperimentPostDetailForUpdateUseCase -import com.dobby.usecase.experiment.GetExperimentPostDetailUseCase -import com.dobby.usecase.experiment.GetExperimentPostTotalCountByCustomFilterUseCase -import com.dobby.usecase.experiment.GetExperimentPostsUseCase -import com.dobby.usecase.experiment.GetMyExperimentPostTotalCountUseCase -import com.dobby.usecase.experiment.GetMyExperimentPostsUseCase -import com.dobby.usecase.experiment.UpdateExperimentPostRecruitStatusUseCase -import com.dobby.usecase.experiment.UpdateExperimentPostUseCase -import com.dobby.usecase.experiment.UpdateExpiredExperimentPostUseCase +import com.dobby.usecase.experiment.* import jakarta.transaction.Transactional import org.springframework.stereotype.Service @@ -41,6 +27,7 @@ class ExperimentPostService( private val getExperimentPostTotalCountByCustomFilterUseCase: GetExperimentPostTotalCountByCustomFilterUseCase, private val getMyExperimentPostsUseCase: GetMyExperimentPostsUseCase, private val getMyExperimentPostTotalCountUseCase: GetMyExperimentPostTotalCountUseCase, + private val extractExperimentPostKeywordsUseCase: ExtractExperimentPostKeywordsUseCase, private val cacheGateway: CacheGateway ) { @Transactional @@ -165,6 +152,10 @@ class ExperimentPostService( evictExperimentPostCountsCaches() } + fun extractExperimentPostKeywords(input: ExtractExperimentPostKeywordsUseCase.Input): ExtractExperimentPostKeywordsUseCase.Output { + return extractExperimentPostKeywordsUseCase.execute(input) + } + private fun evictExperimentPostCountsCaches() { listOf("ALL", "OPEN").forEach { cacheGateway.evict("experimentPostCounts:$it") } } diff --git a/application/src/main/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCase.kt b/application/src/main/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCase.kt new file mode 100644 index 00000000..55ecd569 --- /dev/null +++ b/application/src/main/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCase.kt @@ -0,0 +1,17 @@ +package com.dobby.usecase.experiment + +import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway +import com.dobby.model.experiment.keyword.ExperimentPostKeyword +import com.dobby.usecase.UseCase + +class ExtractExperimentPostKeywordsUseCase( + private val experimentKeywordExtractionGateway: ExperimentKeywordExtractionGateway +) : UseCase { + data class Input(val text: String) + data class Output(val experimentPostKeyword: ExperimentPostKeyword) + + override fun execute(input: Input): Output { + val experimentPostKeyword = experimentKeywordExtractionGateway.extractKeywords(input.text) + return Output(experimentPostKeyword) + } +} diff --git a/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt b/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt index b52da6d5..74e28220 100644 --- a/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt +++ b/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt @@ -2,15 +2,11 @@ package com.dobby.api.controller import com.dobby.api.dto.request.PreSignedUrlRequest import com.dobby.api.dto.request.experiment.CreateExperimentPostRequest +import com.dobby.api.dto.request.experiment.ExtractKeywordRequest 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.ExperimentPostApplyMethodResponse -import com.dobby.api.dto.response.experiment.ExperimentPostCountsResponse -import com.dobby.api.dto.response.experiment.ExperimentPostDetailResponse -import com.dobby.api.dto.response.experiment.ExperimentPostResponse -import com.dobby.api.dto.response.experiment.UpdateExperimentPostResponse +import com.dobby.api.dto.response.experiment.* import com.dobby.api.dto.response.member.DefaultResponse import com.dobby.api.dto.response.member.MyExperimentPostResponse import com.dobby.api.mapper.ExperimentPostMapper @@ -216,4 +212,19 @@ class ExperimentPostController( val isLast = totalCount <= count * page return ExperimentPostMapper.toGetMyExperimentPostsResponse(posts, page, totalCount, isLast) } + + @PreAuthorize("hasRole('RESEARCHER')") + @PostMapping("/extract-keywords") + @Operation( + summary = "실험 공고 키워드 추출 API", + description = "실험 공고 텍스트에서 키워드를 추출합니다." + ) + fun extractExperimentPostKeywords( + @RequestBody @Valid + request: ExtractKeywordRequest + ): ExtractKeywordResponse { + val input = ExperimentPostMapper.toExtractKeywordUseCaseInput(request) + val output = experimentPostService.extractExperimentPostKeywords(input) + return ExperimentPostMapper.toExtractKeywordResponse(output) + } } diff --git a/presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtactKeywordRequest.kt b/presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtactKeywordRequest.kt new file mode 100644 index 00000000..043f3bde --- /dev/null +++ b/presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtactKeywordRequest.kt @@ -0,0 +1,10 @@ +package com.dobby.api.dto.request.experiment + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.NotBlank + +data class ExtractKeywordRequest( + @NotBlank + @Schema(description = "키워드를 추출할 텍스트", example = "심리학 실험 참여자를 모집합니다. 인지 심리학 관련 실험으로 약 30분 소요됩니다.") + val text: String +) diff --git a/presentation/src/main/kotlin/com/dobby/api/dto/response/experiment/ExtractKeywordResponse.kt b/presentation/src/main/kotlin/com/dobby/api/dto/response/experiment/ExtractKeywordResponse.kt new file mode 100644 index 00000000..978b49a1 --- /dev/null +++ b/presentation/src/main/kotlin/com/dobby/api/dto/response/experiment/ExtractKeywordResponse.kt @@ -0,0 +1,9 @@ +package com.dobby.api.dto.response.experiment + +import com.dobby.model.experiment.keyword.ExperimentPostKeyword +import io.swagger.v3.oas.annotations.media.Schema + +data class ExtractKeywordResponse( + @Schema(description = "추출된 키워드 정보") + val experimentPostKeyword: ExperimentPostKeyword +) diff --git a/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt b/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt index 366035d0..620e1d91 100644 --- a/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt +++ b/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt @@ -1,42 +1,17 @@ package com.dobby.api.mapper import com.dobby.api.dto.request.PreSignedUrlRequest -import com.dobby.api.dto.request.experiment.ApplyMethodInfo -import com.dobby.api.dto.request.experiment.CreateExperimentPostRequest -import com.dobby.api.dto.request.experiment.ImageListInfo -import com.dobby.api.dto.request.experiment.TargetGroupInfo -import com.dobby.api.dto.request.experiment.UpdateExperimentPostRequest +import com.dobby.api.dto.request.experiment.* 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.DataCount -import com.dobby.api.dto.response.experiment.DurationInfo -import com.dobby.api.dto.response.experiment.ExperimentPostApplyMethodResponse -import com.dobby.api.dto.response.experiment.ExperimentPostCountsResponse -import com.dobby.api.dto.response.experiment.ExperimentPostDetailResponse -import com.dobby.api.dto.response.experiment.ExperimentPostResponse -import com.dobby.api.dto.response.experiment.PostInfo -import com.dobby.api.dto.response.experiment.UpdateExperimentPostResponse +import com.dobby.api.dto.response.experiment.* import com.dobby.api.dto.response.member.MyExperimentPostResponse import com.dobby.enums.MatchType import com.dobby.enums.areaInfo.Area import com.dobby.enums.areaInfo.Region import com.dobby.enums.experiment.RecruitStatus import com.dobby.enums.member.GenderType -import com.dobby.usecase.experiment.CreateExperimentPostUseCase -import com.dobby.usecase.experiment.DeleteExperimentPostUseCase -import com.dobby.usecase.experiment.GenerateExperimentPostPreSignedUrlUseCase -import com.dobby.usecase.experiment.GetExperimentPostApplyMethodUseCase -import com.dobby.usecase.experiment.GetExperimentPostCountsByAreaUseCase -import com.dobby.usecase.experiment.GetExperimentPostCountsByRegionUseCase -import com.dobby.usecase.experiment.GetExperimentPostDetailForUpdateUseCase -import com.dobby.usecase.experiment.GetExperimentPostDetailUseCase -import com.dobby.usecase.experiment.GetExperimentPostTotalCountByCustomFilterUseCase -import com.dobby.usecase.experiment.GetExperimentPostsUseCase -import com.dobby.usecase.experiment.GetMyExperimentPostTotalCountUseCase -import com.dobby.usecase.experiment.GetMyExperimentPostsUseCase -import com.dobby.usecase.experiment.UpdateExperimentPostRecruitStatusUseCase -import com.dobby.usecase.experiment.UpdateExperimentPostUseCase +import com.dobby.usecase.experiment.* import com.dobby.util.getCurrentMemberId import com.dobby.util.getCurrentMemberIdOrNull @@ -517,4 +492,16 @@ object ExperimentPostMapper { order = order ) } + + fun toExtractKeywordUseCaseInput(request: ExtractKeywordRequest): ExtractExperimentPostKeywordsUseCase.Input { + return ExtractExperimentPostKeywordsUseCase.Input( + text = request.text + ) + } + + fun toExtractKeywordResponse(output: ExtractExperimentPostKeywordsUseCase.Output): ExtractKeywordResponse { + return ExtractKeywordResponse( + experimentPostKeyword = output.experimentPostKeyword + ) + } } From 124279a932a3e53b2b285ac5b6fd9dddbec320b5 Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:08:47 +0900 Subject: [PATCH 07/16] fix: add default values to Dto fields for null safety --- .../dobby/external/prompt/ApplyMethodDto.kt | 10 +-- .../prompt/ExperimentPostKeywordDto.kt | 12 +-- .../prompt/ExperimentPostKeywordMapper.kt | 74 +++++++++++-------- .../dobby/external/prompt/TargetGroupDto.kt | 8 +- 4 files changed, 59 insertions(+), 45 deletions(-) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt index aa99358d..df99b9dd 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt @@ -1,9 +1,9 @@ package com.dobby.external.prompt data class ApplyMethodDto( - val content: String?, - val isFormUrl: Boolean, - val formUrl: String?, - val isPhoneNum: Boolean, - val phoneNum: String? + val content: String? = null, + val isFormUrl: Boolean = false, + val formUrl: String? = null, + val isPhoneNum: Boolean = false, + val phoneNum: String? = null ) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt index 5a27bf75..355cf589 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt @@ -6,11 +6,11 @@ import com.fasterxml.jackson.annotation.JsonProperty data class ExperimentPostKeywordDto( @JsonProperty("targetGroupInfo") - val targetGroup: TargetGroupDto?, + val targetGroup: TargetGroupDto? = null, @JsonProperty("applyMethodInfo") - val applyMethod: ApplyMethodDto?, - val matchType: MatchType?, - val reward: String?, - val count: Int?, - val timeRequired: TimeSlot? + val applyMethod: ApplyMethodDto? = null, + val matchType: String? = null, + val reward: String? = null, + val count: Int? = null, + val timeRequired: String? = null ) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt index 3bbd1bf3..1e18806e 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt @@ -1,6 +1,8 @@ package com.dobby.external.prompt import com.dobby.enums.member.GenderType +import com.dobby.enums.MatchType +import com.dobby.enums.experiment.TimeSlot import com.dobby.model.experiment.keyword.ApplyMethodKeyword import com.dobby.model.experiment.keyword.ExperimentPostKeyword import com.dobby.model.experiment.keyword.TargetGroupKeyword @@ -11,60 +13,72 @@ class ExperimentPostKeywordMapper { fun toDomain(dto: ExperimentPostKeywordDto): ExperimentPostKeyword { return ExperimentPostKeyword( - targetGroup = dto.targetGroup?.let { + targetGroup = dto.targetGroup?.let { targetGroupDto -> TargetGroupKeyword( - startAge = it.startAge, - endAge = it.endAge, - genderType = it.genderType?.let { genderStr -> + startAge = targetGroupDto.startAge ?: 0, + endAge = targetGroupDto.endAge ?: 0, + genderType = targetGroupDto.genderType?.let { genderStr -> when(genderStr) { "MALE" -> GenderType.MALE "FEMALE" -> GenderType.FEMALE "ALL" -> GenderType.ALL - else -> null + else -> GenderType.ALL } - }, - otherCondition = it.otherCondition + } ?: GenderType.ALL, + otherCondition = targetGroupDto.otherCondition ) }, - applyMethod = dto.applyMethod?.let { + applyMethod = dto.applyMethod?.let { applyMethodDto -> ApplyMethodKeyword( - content = it.content, - isFormUrl = it.isFormUrl, - formUrl = it.formUrl, - isPhoneNum = it.isPhoneNum, - phoneNum = it.phoneNum + content = applyMethodDto.content ?: "", + isFormUrl = applyMethodDto.isFormUrl, + formUrl = applyMethodDto.formUrl ?: "", + isPhoneNum = applyMethodDto.isPhoneNum, + phoneNum = applyMethodDto.phoneNum ?: "" ) }, - matchType = dto.matchType, - reward = dto.reward, - count = dto.count, - timeRequired = dto.timeRequired + matchType = dto.matchType?.let { matchTypeStr -> + try { + MatchType.valueOf(matchTypeStr) + } catch (e: IllegalArgumentException) { + MatchType.ALL + } + } ?: MatchType.ALL, + reward = dto.reward ?: "", + count = dto.count ?: 0, + timeRequired = dto.timeRequired?.takeIf { it.isNotBlank() }?.let { timeSlotStr -> + try { + TimeSlot.valueOf(timeSlotStr) + } catch (e: IllegalArgumentException) { + null + } + } ) } fun toDto(domain: ExperimentPostKeyword): ExperimentPostKeywordDto { return ExperimentPostKeywordDto( - targetGroup = domain.targetGroup?.let { + targetGroup = domain.targetGroup?.let { targetGroupDomain -> TargetGroupDto( - startAge = it.startAge, - endAge = it.endAge, - genderType = it.genderType?.name, - otherCondition = it.otherCondition + startAge = targetGroupDomain.startAge, + endAge = targetGroupDomain.endAge, + genderType = targetGroupDomain.genderType?.name, + otherCondition = targetGroupDomain.otherCondition ) }, - applyMethod = domain.applyMethod?.let { + applyMethod = domain.applyMethod?.let { applyMethodDomain -> ApplyMethodDto( - content = it.content, - isFormUrl = it.isFormUrl, - formUrl = it.formUrl, - isPhoneNum = it.isPhoneNum, - phoneNum = it.phoneNum + content = applyMethodDomain.content, + isFormUrl = applyMethodDomain.isFormUrl, + formUrl = applyMethodDomain.formUrl, + isPhoneNum = applyMethodDomain.isPhoneNum, + phoneNum = applyMethodDomain.phoneNum ) }, - matchType = domain.matchType, + matchType = domain.matchType?.name, reward = domain.reward, count = domain.count, - timeRequired = domain.timeRequired + timeRequired = domain.timeRequired?.name ) } } diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt index cf4a5d6f..6a0bf5fb 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt @@ -1,8 +1,8 @@ package com.dobby.external.prompt data class TargetGroupDto( - val startAge: Int?, - val endAge: Int?, - val genderType: String?, - val otherCondition: String? + val startAge: Int? = null, + val endAge: Int? = null, + val genderType: String? = null, + val otherCondition: String? = null ) From 2ed756d967a09770b83ced8bf2dd936068c166d5 Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:17:17 +0900 Subject: [PATCH 08/16] style: apply ktlint formatting --- .../dobby/service/ExperimentPostService.kt | 17 +++++++++- .../prompt/ExperimentPostKeywordDto.kt | 2 -- .../prompt/ExperimentPostKeywordMapper.kt | 4 +-- .../controller/ExperimentPostController.kt | 8 ++++- ...ordRequest.kt => ExtractKeywordRequest.kt} | 0 .../dobby/api/mapper/ExperimentPostMapper.kt | 34 +++++++++++++++++-- 6 files changed, 56 insertions(+), 9 deletions(-) rename presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/{ExtactKeywordRequest.kt => ExtractKeywordRequest.kt} (100%) diff --git a/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt b/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt index 6282a9ad..6be99688 100644 --- a/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt +++ b/application/src/main/kotlin/com/dobby/service/ExperimentPostService.kt @@ -6,7 +6,22 @@ import com.dobby.exception.ExperimentAreaInCorrectException import com.dobby.exception.ExperimentAreaOverflowException import com.dobby.exception.InvalidRequestValueException import com.dobby.gateway.CacheGateway -import com.dobby.usecase.experiment.* +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.GetExperimentPostApplyMethodUseCase +import com.dobby.usecase.experiment.GetExperimentPostCountsByAreaUseCase +import com.dobby.usecase.experiment.GetExperimentPostCountsByRegionUseCase +import com.dobby.usecase.experiment.GetExperimentPostDetailForUpdateUseCase +import com.dobby.usecase.experiment.GetExperimentPostDetailUseCase +import com.dobby.usecase.experiment.GetExperimentPostTotalCountByCustomFilterUseCase +import com.dobby.usecase.experiment.GetExperimentPostsUseCase +import com.dobby.usecase.experiment.GetMyExperimentPostTotalCountUseCase +import com.dobby.usecase.experiment.GetMyExperimentPostsUseCase +import com.dobby.usecase.experiment.UpdateExperimentPostRecruitStatusUseCase +import com.dobby.usecase.experiment.UpdateExperimentPostUseCase +import com.dobby.usecase.experiment.UpdateExpiredExperimentPostUseCase import jakarta.transaction.Transactional import org.springframework.stereotype.Service diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt index 355cf589..d23b71e9 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt @@ -1,7 +1,5 @@ package com.dobby.external.prompt -import com.dobby.enums.MatchType -import com.dobby.enums.experiment.TimeSlot import com.fasterxml.jackson.annotation.JsonProperty data class ExperimentPostKeywordDto( diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt index 1e18806e..3bc2be1d 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt @@ -1,8 +1,8 @@ package com.dobby.external.prompt -import com.dobby.enums.member.GenderType import com.dobby.enums.MatchType import com.dobby.enums.experiment.TimeSlot +import com.dobby.enums.member.GenderType import com.dobby.model.experiment.keyword.ApplyMethodKeyword import com.dobby.model.experiment.keyword.ExperimentPostKeyword import com.dobby.model.experiment.keyword.TargetGroupKeyword @@ -18,7 +18,7 @@ class ExperimentPostKeywordMapper { startAge = targetGroupDto.startAge ?: 0, endAge = targetGroupDto.endAge ?: 0, genderType = targetGroupDto.genderType?.let { genderStr -> - when(genderStr) { + when (genderStr) { "MALE" -> GenderType.MALE "FEMALE" -> GenderType.FEMALE "ALL" -> GenderType.ALL diff --git a/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt b/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt index 74e28220..b3cc573f 100644 --- a/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt +++ b/presentation/src/main/kotlin/com/dobby/api/controller/ExperimentPostController.kt @@ -6,7 +6,13 @@ import com.dobby.api.dto.request.experiment.ExtractKeywordRequest 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.* +import com.dobby.api.dto.response.experiment.CreateExperimentPostResponse +import com.dobby.api.dto.response.experiment.ExperimentPostApplyMethodResponse +import com.dobby.api.dto.response.experiment.ExperimentPostCountsResponse +import com.dobby.api.dto.response.experiment.ExperimentPostDetailResponse +import com.dobby.api.dto.response.experiment.ExperimentPostResponse +import com.dobby.api.dto.response.experiment.ExtractKeywordResponse +import com.dobby.api.dto.response.experiment.UpdateExperimentPostResponse import com.dobby.api.dto.response.member.DefaultResponse import com.dobby.api.dto.response.member.MyExperimentPostResponse import com.dobby.api.mapper.ExperimentPostMapper diff --git a/presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtactKeywordRequest.kt b/presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtractKeywordRequest.kt similarity index 100% rename from presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtactKeywordRequest.kt rename to presentation/src/main/kotlin/com/dobby/api/dto/request/experiment/ExtractKeywordRequest.kt diff --git a/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt b/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt index 620e1d91..e07ceaf1 100644 --- a/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt +++ b/presentation/src/main/kotlin/com/dobby/api/mapper/ExperimentPostMapper.kt @@ -1,17 +1,45 @@ package com.dobby.api.mapper import com.dobby.api.dto.request.PreSignedUrlRequest -import com.dobby.api.dto.request.experiment.* +import com.dobby.api.dto.request.experiment.ApplyMethodInfo +import com.dobby.api.dto.request.experiment.CreateExperimentPostRequest +import com.dobby.api.dto.request.experiment.ExtractKeywordRequest +import com.dobby.api.dto.request.experiment.ImageListInfo +import com.dobby.api.dto.request.experiment.TargetGroupInfo +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.* +import com.dobby.api.dto.response.experiment.CreateExperimentPostResponse +import com.dobby.api.dto.response.experiment.DataCount +import com.dobby.api.dto.response.experiment.DurationInfo +import com.dobby.api.dto.response.experiment.ExperimentPostApplyMethodResponse +import com.dobby.api.dto.response.experiment.ExperimentPostCountsResponse +import com.dobby.api.dto.response.experiment.ExperimentPostDetailResponse +import com.dobby.api.dto.response.experiment.ExperimentPostResponse +import com.dobby.api.dto.response.experiment.ExtractKeywordResponse +import com.dobby.api.dto.response.experiment.PostInfo +import com.dobby.api.dto.response.experiment.UpdateExperimentPostResponse import com.dobby.api.dto.response.member.MyExperimentPostResponse import com.dobby.enums.MatchType import com.dobby.enums.areaInfo.Area import com.dobby.enums.areaInfo.Region import com.dobby.enums.experiment.RecruitStatus import com.dobby.enums.member.GenderType -import com.dobby.usecase.experiment.* +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.GetExperimentPostApplyMethodUseCase +import com.dobby.usecase.experiment.GetExperimentPostCountsByAreaUseCase +import com.dobby.usecase.experiment.GetExperimentPostCountsByRegionUseCase +import com.dobby.usecase.experiment.GetExperimentPostDetailForUpdateUseCase +import com.dobby.usecase.experiment.GetExperimentPostDetailUseCase +import com.dobby.usecase.experiment.GetExperimentPostTotalCountByCustomFilterUseCase +import com.dobby.usecase.experiment.GetExperimentPostsUseCase +import com.dobby.usecase.experiment.GetMyExperimentPostTotalCountUseCase +import com.dobby.usecase.experiment.GetMyExperimentPostsUseCase +import com.dobby.usecase.experiment.UpdateExperimentPostRecruitStatusUseCase +import com.dobby.usecase.experiment.UpdateExperimentPostUseCase import com.dobby.util.getCurrentMemberId import com.dobby.util.getCurrentMemberIdOrNull From b79fecf816047ad73a32a13fe90c8c2e7ca6014b Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:23:26 +0900 Subject: [PATCH 09/16] test: add ExtractExperimentPostKeywordsUseCase test code --- ...xtractExperimentPostKeywordsUseCaseTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 application/src/test/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCaseTest.kt diff --git a/application/src/test/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCaseTest.kt b/application/src/test/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCaseTest.kt new file mode 100644 index 00000000..007573ee --- /dev/null +++ b/application/src/test/kotlin/com/dobby/usecase/experiment/ExtractExperimentPostKeywordsUseCaseTest.kt @@ -0,0 +1,34 @@ +package com.dobby.usecase.experiment + +import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway +import com.dobby.model.experiment.keyword.ExperimentPostKeyword +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + +class ExtractExperimentPostKeywordsUseCaseTest : BehaviorSpec({ + val experimentKeywordExtractionGateway = mockk() + val extractExperimentPostKeywordsUseCase = ExtractExperimentPostKeywordsUseCase(experimentKeywordExtractionGateway) + + given("실험 게시글 텍스트에서 키워드를 추출할 때") { + val inputText = "남성 20-30대 대상 설문조사 참여자 모집합니다. 시간은 1시간 소요되며 참가비 10,000원 지급합니다." + val input = ExtractExperimentPostKeywordsUseCase.Input(inputText) + val mockExperimentPostKeyword = mockk() + + every { experimentKeywordExtractionGateway.extractKeywords(inputText) } returns mockExperimentPostKeyword + + `when`("키워드 추출을 요청하면") { + val result = extractExperimentPostKeywordsUseCase.execute(input) + + then("추출된 키워드 정보를 반환해야 한다") { + result.experimentPostKeyword shouldBe mockExperimentPostKeyword + + verify(exactly = 1) { + experimentKeywordExtractionGateway.extractKeywords(inputText) + } + } + } + } +}) From e375f91783e7c2d59ac2088503f55e0211d036c8 Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:42:29 +0900 Subject: [PATCH 10/16] refactor: reuse ObjectMapper instance to improve efficiency --- .../experiment/ExperimentKeywordExtractionGatewayImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt index 62fe060f..0b90cf98 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -19,12 +19,13 @@ class ExperimentKeywordExtractionGatewayImpl( private val mapper: ExperimentPostKeywordMapper ) : ExperimentKeywordExtractionGateway { + private val objectMapper = jacksonObjectMapper() + private val promptTemplate: PromptTemplate by lazy { promptTemplateLoader.loadPrompt("prompts/keyword_extraction_prompt.json") } override fun extractKeywords(text: String): ExperimentPostKeyword { - val objectMapper = jacksonObjectMapper() val promptJson = objectMapper.writeValueAsString(promptTemplate) val prompt = promptJson.replace("{{text}}", text) From 020f6e0a889b32e3f3bd5b4dd2043d44bf94802f Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:53:11 +0900 Subject: [PATCH 11/16] feat: add CustomOpenAiCallException for OpenAI API call errors --- .../com/dobby/exception/DobbyException.kt | 5 ++++ .../ExperimentKeywordExtractionGatewayImpl.kt | 30 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/domain/src/main/kotlin/com/dobby/exception/DobbyException.kt b/domain/src/main/kotlin/com/dobby/exception/DobbyException.kt index e83f0e68..b9d6edb3 100644 --- a/domain/src/main/kotlin/com/dobby/exception/DobbyException.kt +++ b/domain/src/main/kotlin/com/dobby/exception/DobbyException.kt @@ -77,3 +77,8 @@ data object ExperimentPostLeadResearcherException : ClientException("EP0013", "L sealed class ServerException(code: String, message: String, cause: Throwable? = null) : DobbyException(code, message, cause) data object UnknownServerErrorException : ServerException("DB0001", "An unknown error has occurred") + +/** + * OpenAI API call specific exceptions + */ +data class CustomOpenAiCallException(override val message: String, override val cause: Throwable? = null) : ServerException("AI0001", message, cause) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt index 0b90cf98..756fb666 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -1,6 +1,7 @@ package com.dobby.external.gateway.experiment import com.dobby.api.dto.request.OpenAiRequest +import com.dobby.exception.CustomOpenAiCallException import com.dobby.external.feign.openAi.OpenAiFeignClient import com.dobby.external.prompt.ExperimentPostKeywordDto import com.dobby.external.prompt.ExperimentPostKeywordMapper @@ -10,6 +11,7 @@ import com.dobby.gateway.experiment.ExperimentKeywordExtractionGateway import com.dobby.model.experiment.keyword.ExperimentPostKeyword import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import feign.FeignException import org.springframework.stereotype.Service @Service @@ -27,8 +29,7 @@ class ExperimentKeywordExtractionGatewayImpl( override fun extractKeywords(text: String): ExperimentPostKeyword { val promptJson = objectMapper.writeValueAsString(promptTemplate) - val prompt = promptJson.replace("{{text}}", text) - + val prompt = promptJson.replace("{{text}}", escapeJsonString(text)) val messages = listOf( OpenAiRequest.Message(role = "user", content = prompt) ) @@ -39,26 +40,37 @@ class ExperimentKeywordExtractionGatewayImpl( messages = messages ) - val response = openAiFeignClient.chatCompletion(request) - val content = response.choices.firstOrNull()?.message?.content - ?: throw IllegalStateException("OpenAI 응답 없음") + val content = try { + val response = openAiFeignClient.chatCompletion(request) + response.choices.firstOrNull()?.message?.content + ?: throw IllegalStateException("No response received from OpenAI") + } catch (e: FeignException) { + throw CustomOpenAiCallException("OpenAI API call failed (status=${e.status()})", e) + } catch (e: Exception) { + throw IllegalStateException("Unexpected error occurred during OpenAI API call", e) + } return try { - // 마크다운 코드 블록 제거 및 JSON 정리 val cleanedContent = cleanJsonResponse(content) val dto = objectMapper.readValue(cleanedContent) mapper.toDomain(dto) } catch (e: Exception) { - throw IllegalStateException("응답 파싱 실패: $content", e) + throw IllegalStateException("Failed to parse response: $content", e) } } + private fun escapeJsonString(text: String): String { + return text.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + } + private fun cleanJsonResponse(content: String): String { return content.trim() - // 마크다운 코드 블록 제거 .replace(Regex("^```json\\s*"), "") .replace(Regex("```\\s*$"), "") - // 시작/끝 백틱 제거 .replace(Regex("^`+"), "") .replace(Regex("`+$"), "") .trim() From 9e215038128629a6a2863cd2b38624d17494d151 Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:55:34 +0900 Subject: [PATCH 12/16] refactor: move ExperimentPostKeyword DTO to `/prompt/dto` for better clarity --- .../experiment/ExperimentKeywordExtractionGatewayImpl.kt | 2 +- .../com/dobby/external/prompt/ExperimentPostKeywordMapper.kt | 3 +++ .../com/dobby/external/prompt/{ => dto}/ApplyMethodDto.kt | 2 +- .../external/prompt/{ => dto}/ExperimentPostKeywordDto.kt | 2 +- .../com/dobby/external/prompt/{ => dto}/TargetGroupDto.kt | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) rename infrastructure/src/main/kotlin/com/dobby/external/prompt/{ => dto}/ApplyMethodDto.kt (84%) rename infrastructure/src/main/kotlin/com/dobby/external/prompt/{ => dto}/ExperimentPostKeywordDto.kt (91%) rename infrastructure/src/main/kotlin/com/dobby/external/prompt/{ => dto}/TargetGroupDto.kt (81%) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt index 756fb666..e7e8574c 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -3,7 +3,7 @@ package com.dobby.external.gateway.experiment import com.dobby.api.dto.request.OpenAiRequest import com.dobby.exception.CustomOpenAiCallException import com.dobby.external.feign.openAi.OpenAiFeignClient -import com.dobby.external.prompt.ExperimentPostKeywordDto +import com.dobby.external.prompt.dto.ExperimentPostKeywordDto import com.dobby.external.prompt.ExperimentPostKeywordMapper import com.dobby.external.prompt.PromptTemplate import com.dobby.external.prompt.PromptTemplateLoader diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt index 3bc2be1d..b1986c76 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordMapper.kt @@ -3,6 +3,9 @@ package com.dobby.external.prompt import com.dobby.enums.MatchType import com.dobby.enums.experiment.TimeSlot import com.dobby.enums.member.GenderType +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.TargetGroupKeyword diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/ApplyMethodDto.kt similarity index 84% rename from infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt rename to infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/ApplyMethodDto.kt index df99b9dd..ba63abb9 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ApplyMethodDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/ApplyMethodDto.kt @@ -1,4 +1,4 @@ -package com.dobby.external.prompt +package com.dobby.external.prompt.dto data class ApplyMethodDto( val content: String? = null, diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/ExperimentPostKeywordDto.kt similarity index 91% rename from infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt rename to infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/ExperimentPostKeywordDto.kt index d23b71e9..5857c34b 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/ExperimentPostKeywordDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/ExperimentPostKeywordDto.kt @@ -1,4 +1,4 @@ -package com.dobby.external.prompt +package com.dobby.external.prompt.dto import com.fasterxml.jackson.annotation.JsonProperty diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/TargetGroupDto.kt similarity index 81% rename from infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt rename to infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/TargetGroupDto.kt index 6a0bf5fb..a289cd94 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/TargetGroupDto.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/dto/TargetGroupDto.kt @@ -1,4 +1,4 @@ -package com.dobby.external.prompt +package com.dobby.external.prompt.dto data class TargetGroupDto( val startAge: Int? = null, From 045bced07a89ea215936579e418430cfea18dcbb Mon Sep 17 00:00:00 2001 From: jisu Date: Thu, 17 Jul 2025 16:58:25 +0900 Subject: [PATCH 13/16] style: add ktlint formatting --- .../experiment/ExperimentKeywordExtractionGatewayImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt index e7e8574c..8dc49dde 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -3,10 +3,10 @@ package com.dobby.external.gateway.experiment import com.dobby.api.dto.request.OpenAiRequest import com.dobby.exception.CustomOpenAiCallException import com.dobby.external.feign.openAi.OpenAiFeignClient -import com.dobby.external.prompt.dto.ExperimentPostKeywordDto 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.fasterxml.jackson.module.kotlin.jacksonObjectMapper From 1d5b35e430b88293cfc8486c9f46e814e09ba95c Mon Sep 17 00:00:00 2001 From: jisu Date: Fri, 18 Jul 2025 15:09:28 +0900 Subject: [PATCH 14/16] refactor: replace `@Service` with `@Component` --- .../experiment/ExperimentKeywordExtractionGatewayImpl.kt | 4 ++-- .../kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt index 8dc49dde..9ee5c29c 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/gateway/experiment/ExperimentKeywordExtractionGatewayImpl.kt @@ -12,9 +12,9 @@ import com.dobby.model.experiment.keyword.ExperimentPostKeyword import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import feign.FeignException -import org.springframework.stereotype.Service +import org.springframework.stereotype.Component -@Service +@Component class ExperimentKeywordExtractionGatewayImpl( private val openAiFeignClient: OpenAiFeignClient, private val promptTemplateLoader: PromptTemplateLoader, diff --git a/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt b/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt index 5016a162..9d92c24a 100644 --- a/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt +++ b/infrastructure/src/main/kotlin/com/dobby/external/prompt/PromptTemplateLoader.kt @@ -1,9 +1,9 @@ package com.dobby.external.prompt import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import org.springframework.stereotype.Service +import org.springframework.stereotype.Component -@Service +@Component class PromptTemplateLoader { private val mapper = jacksonObjectMapper() From a1ac0509a8af27810fac864d3f8dde77c7f662af Mon Sep 17 00:00:00 2001 From: jisu Date: Fri, 18 Jul 2025 16:52:01 +0900 Subject: [PATCH 15/16] feat: add field length limits to extraction prompt --- .../prompts/keyword_extraction_prompt.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json b/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json index 5250f86a..5313e256 100644 --- a/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json +++ b/infrastructure/src/main/resources/prompts/keyword_extraction_prompt.json @@ -18,17 +18,17 @@ "startAge": "시작 나이 (숫자)", "endAge": "끝 나이 (숫자)", "genderType": "MALE/FEMALE/ALL", - "otherCondition": "기타 조건 (문자열)" + "otherCondition": "기타 조건 (문자열, 최대 300자)" }, "applyMethodInfo": { - "content": "참여 방법 설명 (문자열)", + "content": "참여 방법 설명 (문자열, 최대 200자)", "isFormUrl": "URL 포함 여부 (boolean)", - "formUrl": "신청 URL (문자열)", + "formUrl": "신청 URL (문자열, 최대 100자)", "isPhoneNum": "전화번호 포함 여부 (boolean)", - "phoneNum": "전화번호 (문자열)" + "phoneNum": "전화번호 (문자열, 최대 50자)" }, "matchType": "OFFLINE/ONLINE/ALL", - "reward": "참여 보상 (문자열)", + "reward": "참여 보상 (문자열, 최대 170자)", "count": "참여 횟수 (숫자)", "timeRequired": "LESS_30M/ABOUT_30M/ABOUT_1H/ABOUT_1H30M/ABOUT_2H/ABOUT_2H30M/ABOUT_3H/ABOUT_3H30M/ABOUT_4H" }, @@ -36,6 +36,7 @@ "공고 본문을 정확히 분석하여 각 항목에 맞는 정보를 추출한다.", "추출된 정보는 반드시 output_format에 따라 JSON 형태로 출력한다.", "정보가 명시되지 않은 경우 해당 필드는 빈 문자열(\"\") 또는 0으로 설정한다.", - "URL이나 전화번호가 포함된 경우 해당 boolean 값을 true로 설정한다." + "URL이나 전화번호가 포함된 경우 해당 boolean 값을 true로 설정한다.", + "각 항목은 지정된 최대 글자 수를 초과하지 않도록 한다." ] } From 7ccfe476dc0165816cbc17e06fc51a33c84cfbcd Mon Sep 17 00:00:00 2001 From: jisu Date: Mon, 28 Jul 2025 18:45:53 +0900 Subject: [PATCH 16/16] fix: restrict OpenAI Feign interceptor to avoid affecting global OAuth clients --- .../src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt b/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt index 7a2523d9..7e5008c8 100644 --- a/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt +++ b/infrastructure/src/main/kotlin/com/dobby/config/OpenAiFeignConfig.kt @@ -3,9 +3,7 @@ package com.dobby.config import com.dobby.config.properties.OpenAiProperties import feign.RequestInterceptor import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -@Configuration class OpenAiFeignConfig( private val openAiProperties: OpenAiProperties ) {