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
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ buildConfig {
file("src/main/resources/content-type-id.yml")
.readText()
.substringAfter("codes:")
.substringBefore("foreign-codes:")
.lines()
.filter { it.contains(":") }
.joinToString(", ") { line ->
val parts = line.split(":")
"${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}"
}

val foreignContentTypeCodes =
file("src/main/resources/content-type-id.yml")
.readText()
.substringAfter("foreign-codes:")
.lines()
.filter { it.contains(":") }
.joinToString(", ") { line ->
Expand Down Expand Up @@ -177,6 +189,7 @@ buildConfig {

buildConfigField("String", "AREA_CODES_DESCRIPTION", "\"\"\"$areaCodes\"\"\"")
buildConfigField("String", "CONTENT_TYPE_CODES_DESCRIPTION", "\"\"\"$contentTypeCodes\"\"\"")
buildConfigField("String", "FOREIGN_CONTENT_TYPE_CODES_DESCRIPTION", "\"\"\"$foreignContentTypeCodes\"\"\"")
buildConfigField("String", "LANGUAGE_CODES_DESCRIPTION", "\"\"\"$languageCodesDescription\"\"\"")
buildConfigField("String", "REGION_CODES_DESCRIPTION", "\"\"\"$regionCodes\"\"\"")
buildConfigField("String", "KOREA_TRAVEL_GUIDE_SYSTEM", "\"\"\"$systemPrompt\"\"\"")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.back.koreaTravelGuide.domain.ai.aiChat.tool

import com.back.koreaTravelGuide.common.logging.log
import com.back.koreaTravelGuide.domain.guide.service.GuideService
import com.back.koreaTravelGuide.domain.user.enums.Region
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.ai.tool.annotation.Tool
import org.springframework.ai.tool.annotation.ToolParam
Expand All @@ -14,7 +15,12 @@ class GuideFinderTool(
) {
@Tool(description = "특정 지역(region)에서 활동하는 여행 가이드 목록을 검색합니다.")
fun findGuidesByRegion(
@ToolParam(description = "검색할 지역 이름. 예: '서울', '부산', '강남구'", required = true)
@ToolParam(
description =
"검색할 지역의 영어 코드 (대문자). " +
"사용 가능한 지역: ${Region.ALL_REGIONS_DESCRIPTION}",
required = true,
)
region: String,
): String {
log.info("🔧 [TOOL CALLED] findGuidesByRegion - region: $region")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,35 @@ class TourTool(
fun getAreaBasedTourInfo(
@ToolParam(
description =
"관광 타입 코드를 사용하세요. 사용자가 타입 이름을 말하면 해당하는 코드를 찾아서 사용해야 합니다. " +
"예: 사용자가 '관광지'라고 하면 '12'를 사용하세요. " +
"사용 가능한 타입 코드: ${BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION}",
"STEP 1: 사용자 메시지의 언어를 파악하여 해당하는 서비스 코드를 선택하세요. " +
"사용 가능한 언어 코드: ${BuildConfig.LANGUAGE_CODES_DESCRIPTION}",
required = true,
)
languageCode: String,
@ToolParam(
description =
"STEP 2: languageCode에 따라 관광 타입 코드를 선택하세요. " +
"IF languageCode == 'KorService2' THEN 한국어 코드: ${BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION}. " +
"ELSE (EngService2, JpnService2, ChsService2, ChtService2) THEN " +
"외국어 코드: ${BuildConfig.FOREIGN_CONTENT_TYPE_CODES_DESCRIPTION}",
required = true,
)
contentTypeId: String,
@ToolParam(
description =
"지역 코드를 쉼표(,)로 구분해서 사용하세요. " +
"예: 사용자가 '서울 강남구'라고 하면 AREA_CODES에서 '서울-강남구: 1-1'을 찾고, " +
"하이픈(-)을 쉼표(,)로 바꿔서 '1,1'을 사용하세요. " +
"광역시(인천, 대전 등)는 단일 코드만 사용: 예: '인천' → '2' (쉼표 없음). " +
"STEP 3: 지역 코드를 전달하세요. 하이픈(-) 또는 쉼표(,) 형식 모두 가능합니다. " +
"사용 가능한 지역 코드: ${BuildConfig.AREA_CODES_DESCRIPTION}",
required = true,
)
areaAndSigunguCode: String,
): String {
log.info("🔧 [TOOL CALLED] getAreaBasedTourInfo - contentTypeId: $contentTypeId, areaAndSigunguCode: $areaAndSigunguCode")
log.info(
"🔧 [TOOL CALLED] getAreaBasedTourInfo - " +
"contentTypeId: $contentTypeId, areaAndSigunguCode: $areaAndSigunguCode, languageCode: $languageCode",
)

val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
val tourInfo = tourService.fetchTours(tourParams)
val tourInfo = tourService.fetchTours(tourParams, languageCode)

return try {
val result = tourInfo.let { objectMapper.writeValueAsString(it) }
Expand All @@ -74,17 +82,23 @@ class TourTool(
fun getLocationBasedTourInfo(
@ToolParam(
description =
"관광 타입 코드를 사용하세요. 사용자가 타입 이름을 말하면 해당하는 코드를 찾아서 사용해야 합니다. " +
"예: 사용자가 '음식점'이라고 하면 '39'를 사용하세요. " +
"사용 가능한 타입 코드: ${BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION}",
"STEP 1: 사용자 메시지의 언어를 파악하여 해당하는 서비스 코드를 선택하세요. " +
"사용 가능한 언어 코드: ${BuildConfig.LANGUAGE_CODES_DESCRIPTION}",
required = true,
)
languageCode: String,
@ToolParam(
description =
"STEP 2: languageCode에 따라 관광 타입 코드를 선택하세요. " +
"IF languageCode == 'KorService2' THEN 한국어 코드: ${BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION}. " +
"ELSE (EngService2, JpnService2, ChsService2, ChtService2) THEN " +
"외국어 코드: ${BuildConfig.FOREIGN_CONTENT_TYPE_CODES_DESCRIPTION}",
required = true,
)
contentTypeId: String,
@ToolParam(
description =
"지역 코드를 쉼표(,)로 구분해서 사용하세요. " +
"예: 사용자가 '서울 중구'라고 하면 AREA_CODES에서 '서울-중구: 1-24'를 찾고, " +
"하이픈(-)을 쉼표(,)로 바꿔서 '1,24'를 사용하세요. " +
"STEP 3: 지역 코드를 전달하세요. 하이픈(-) 또는 쉼표(,) 형식 모두 가능합니다. " +
"사용 가능한 지역 코드: ${BuildConfig.AREA_CODES_DESCRIPTION}",
required = true,
)
Expand All @@ -99,12 +113,12 @@ class TourTool(
log.info(
"🔧 [TOOL CALLED] getLocationBasedTourInfo - " +
"contentTypeId: $contentTypeId, area: $areaAndSigunguCode, " +
"mapX: $mapX, mapY: $mapY, radius: $radius",
"mapX: $mapX, mapY: $mapY, radius: $radius, languageCode: $languageCode",
)

val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
val locationBasedParams = TourLocationBasedParams(mapX, mapY, radius)
val tourLocationBasedInfo = tourService.fetchLocationBasedTours(tourParams, locationBasedParams)
val tourLocationBasedInfo = tourService.fetchLocationBasedTours(tourParams, locationBasedParams, languageCode)

return try {
val result = tourLocationBasedInfo.let { objectMapper.writeValueAsString(it) }
Expand All @@ -126,16 +140,23 @@ class TourTool(
fun getTourDetailInfo(
@ToolParam(
description =
"조회할 관광정보의 콘텐츠 ID. " +
"STEP 1: 사용자 메시지의 언어를 파악하여 해당하는 서비스 코드를 선택하세요. " +
"사용 가능한 언어 코드: ${BuildConfig.LANGUAGE_CODES_DESCRIPTION}",
required = true,
)
languageCode: String,
@ToolParam(
description =
"STEP 2: 조회할 관광정보의 콘텐츠 ID. " +
"이전 Tool 호출 결과(getAreaBasedTourInfo 또는 getLocationBasedTourInfo)에서 받은 contentId를 사용하세요.",
required = true,
)
contentId: String = "127974",
contentId: String,
): String {
log.info("🔧 [TOOL CALLED] getTourDetailInfo - contentId: $contentId")
log.info("🔧 [TOOL CALLED] getTourDetailInfo - contentId: $contentId, languageCode: $languageCode")

val tourDetailParams = TourDetailParams(contentId)
val tourDetailInfo = tourService.fetchTourDetail(tourDetailParams)
val tourDetailInfo = tourService.fetchTourDetail(tourDetailParams, languageCode)

return try {
val result = tourDetailInfo.let { objectMapper.writeValueAsString(it) }
Expand All @@ -154,13 +175,13 @@ class TourTool(
* "areacode": "6" 부산
* "sigungucode": "10" 사하구
* "contenttypeid": "76" 관광지 (해외)
* "serviceSegment" : "EngService2" (영어)
* "languageCode" : "EngService2" (영어)
*
*
* 2
* fetchTourDetail - 상세조회
* 케이스 : 콘텐츠ID가 "264247인 관광정보의 "상베 정보" 조회
* "contentid": "264247,
* "serviceSegment" : "EngService2" (영어)
* "languageCode" : "EngService2" (영어)
*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ class TourApiClient(
// 지역 기반 관광 정보 조회 (areaBasedList2)
fun fetchTourInfo(
params: TourParams,
serviceSegment: String,
languageCode: String,
): TourResponse {
val url =
buildTourUri(serviceSegment, "areaBasedList2") {
buildTourUri(languageCode, "areaBasedList2") {
queryParam("contentTypeId", params.contentTypeId)
queryParam("areaCode", params.areaCode)
queryParam("sigunguCode", params.sigunguCode)
}

val body =
runCatching { restTemplate.getForObject(url, String::class.java) }
.onFailure { log.error("관광 정보 조회 실패 - serviceSegment={}", serviceSegment, it) }
.onFailure { log.error("관광 정보 조회 실패 - languageCode={}", languageCode, it) }
.getOrNull()

return body
Expand All @@ -51,10 +51,10 @@ class TourApiClient(
fun fetchLocationBasedTours(
tourParams: TourParams,
locationParams: TourLocationBasedParams,
serviceSegment: String,
languageCode: String,
): TourResponse {
val url =
buildTourUri(serviceSegment, "locationBasedList2") {
buildTourUri(languageCode, "locationBasedList2") {
queryParam("mapX", locationParams.mapX)
queryParam("mapY", locationParams.mapY)
queryParam("radius", locationParams.radius)
Expand All @@ -65,7 +65,7 @@ class TourApiClient(

val body =
runCatching { restTemplate.getForObject(url, String::class.java) }
.onFailure { log.error("위치기반 관광 정보 조회 실패 - serviceSegment={}", serviceSegment, it) }
.onFailure { log.error("위치기반 관광 정보 조회 실패 - languageCode={}", languageCode, it) }
.getOrNull()

return body
Expand All @@ -77,16 +77,16 @@ class TourApiClient(
// 공통정보 조회 (detailCommon2)
fun fetchTourDetail(
params: TourDetailParams,
serviceSegment: String,
languageCode: String,
): TourDetailResponse {
val url =
buildTourUri(serviceSegment, "detailCommon2") {
buildTourUri(languageCode, "detailCommon2") {
queryParam("contentId", params.contentId)
}

val body =
runCatching { restTemplate.getForObject(url, String::class.java) }
.onFailure { log.error("공통정보 조회 실패 - serviceSegment={}", serviceSegment, it) }
.onFailure { log.error("공통정보 조회 실패 - languageCode={}", languageCode, it) }
.getOrNull()

return body
Expand Down Expand Up @@ -176,12 +176,12 @@ class TourApiClient(
}

private fun buildTourUri(
serviceSegment: String,
languageCode: String,
vararg pathSegments: String,
customize: UriComponentsBuilder.() -> Unit = {},
): URI =
UriComponentsBuilder.fromUri(URI.create(apiUrl))
.pathSegment(serviceSegment, *pathSegments)
.pathSegment(languageCode, *pathSegments)
.apply {
queryParam("serviceKey", serviceKey)
queryParam("MobileOS", "WEB")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class TourParamsParser {
contentTypeId: String,
areaAndSigunguCode: String,
): TourParams {
val codes = areaAndSigunguCode.split(",").map { it.trim() }
// 하이픈(-) 또는 쉼표(,) 둘 다 처리 (AI가 어떤 형식으로 보내도 작동)
val codes = areaAndSigunguCode.split(",", "-").map { it.trim() }

val areaCode = codes.getOrNull(0)
val sigunguCode = codes.getOrNull(1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,35 @@ class TourService(

/**
* 지역 기반 관광 정보를 조회한다.
* 언어 문자열을 설정으로 정규화해 다국어 엔드포인트에 맞춰 전달한다.
* languageCode는 AI가 사용자의 대화 언어를 파악하여 전달한다.
*/
fun fetchTours(
tourParams: TourParams,
languageCode: String? = null,
languageCode: String,
): TourResponse {
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
return tourAreaBasedUseCase.fetchAreaBasedTours(tourParams, serviceSegment)
return tourAreaBasedUseCase.fetchAreaBasedTours(tourParams, languageCode)
}

/**
* 위치 기반 관광 정보를 조회한다.
* 전달받은 언어 값을 설정 기반 서비스 세그먼트로 치환해 API 클라이언트를 호출한다.
* languageCode는 AI가 사용자의 대화 언어를 파악하여 전달한다.
*/
fun fetchLocationBasedTours(
tourParams: TourParams,
locationParams: TourLocationBasedParams,
languageCode: String? = null,
languageCode: String,
): TourResponse {
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
return tourLocationBasedUseCase.fetchLocationBasedTours(tourParams, locationParams, serviceSegment)
return tourLocationBasedUseCase.fetchLocationBasedTours(tourParams, locationParams, languageCode)
}

/**
* 관광지 상세 정보를 조회한다.
* 언어 값을 정규화해 상세 API 호출 시 사용한다.
* languageCode는 AI가 사용자의 대화 언어를 파악하여 전달한다.
*/
fun fetchTourDetail(
detailParams: TourDetailParams,
languageCode: String? = null,
languageCode: String,
): TourDetailResponse {
val serviceSegment = languageCode?.takeIf { it.isNotBlank() } ?: DEFAULT_LANGUAGE_SEGMENT
return tourDetailUseCase.fetchTourDetail(detailParams, serviceSegment)
}

companion object {
private const val DEFAULT_LANGUAGE_SEGMENT = "KorService2"
return tourDetailUseCase.fetchTourDetail(detailParams, languageCode)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ class TourAreaBasedServiceCore(
@Cacheable(
"tourAreaBased",
key =
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + #serviceSegment",
"#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + #languageCode",
unless = "#result == null",
)
override fun fetchAreaBasedTours(
tourParams: TourParams,
serviceSegment: String,
languageCode: String,
): TourResponse {
// 부산 사하구 관광안내소(콘텐츠타입 76, EngService2)에 대한 프리셋 응답
if (
serviceSegment == ENGLISH_SERVICE_SEGMENT &&
languageCode == ENGLISH_SERVICE_SEGMENT &&
tourParams.contentTypeId == "76" &&
tourParams.areaCode == "6" &&
tourParams.sigunguCode == "10"
Expand All @@ -34,15 +34,15 @@ class TourAreaBasedServiceCore(

// 부산 사하구 관광지 목록(콘텐츠타입 12, KorService2)에 대한 프리셋 응답
if (
serviceSegment == KOREAN_SERVICE_SEGMENT &&
languageCode == KOREAN_SERVICE_SEGMENT &&
tourParams.contentTypeId == "12" &&
tourParams.areaCode == "6" &&
tourParams.sigunguCode == "10"
) {
return PRESET_AREA_TOUR_RESPONSE
}

return tourApiClient.fetchTourInfo(tourParams, serviceSegment)
return tourApiClient.fetchTourInfo(tourParams, languageCode)
}

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@ class TourDetailServiceCore(
) : TourDetailUseCase {
@Cacheable(
"tourDetail",
key = "#detailParams.contentId + '_' + #serviceSegment",
key = "#detailParams.contentId + '_' + #languageCode",
unless = "#result == null",
)
override fun fetchTourDetail(
detailParams: TourDetailParams,
serviceSegment: String,
languageCode: String,
): TourDetailResponse {
// 을숙도 철새공원 상세정보(EngService2, contentId 264247) 프리셋
if (serviceSegment == ENGLISH_SERVICE_SEGMENT && detailParams.contentId == "264247") {
if (languageCode == ENGLISH_SERVICE_SEGMENT && detailParams.contentId == "264247") {
return PRESET_DETAIL_RESPONSE_EN
}

// 동촌유원지 상세정보(KorService2, contentId 127974) 프리셋
if (serviceSegment == KOREAN_SERVICE_SEGMENT && detailParams.contentId == "127974") {
if (languageCode == KOREAN_SERVICE_SEGMENT && detailParams.contentId == "127974") {
return PRESET_DETAIL_RESPONSE
}

return tourApiClient.fetchTourDetail(detailParams, serviceSegment)
return tourApiClient.fetchTourDetail(detailParams, languageCode)
}

private companion object {
Expand Down
Loading