diff --git a/build.gradle.kts b/build.gradle.kts index e108434..8e498bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 -> @@ -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\"\"\"") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/GuideFinderTool.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/GuideFinderTool.kt index c35c65c..422a776 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/GuideFinderTool.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/GuideFinderTool.kt @@ -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 @@ -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") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt index 18844c0..94adf99 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt @@ -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) } @@ -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, ) @@ -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) } @@ -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) } @@ -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" (영어) */ } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt index 657f2e9..4e016f7 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt @@ -27,10 +27,10 @@ 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) @@ -38,7 +38,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 @@ -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) @@ -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 @@ -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 @@ -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") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourParamsParser.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourParamsParser.kt index 3f16d7a..27196b5 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourParamsParser.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourParamsParser.kt @@ -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) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt index 36e32aa..67b9136 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt @@ -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) } } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourAreaBasedServiceCore.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourAreaBasedServiceCore.kt index 43be6a0..130c23d 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourAreaBasedServiceCore.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourAreaBasedServiceCore.kt @@ -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" @@ -34,7 +34,7 @@ class TourAreaBasedServiceCore( // 부산 사하구 관광지 목록(콘텐츠타입 12, KorService2)에 대한 프리셋 응답 if ( - serviceSegment == KOREAN_SERVICE_SEGMENT && + languageCode == KOREAN_SERVICE_SEGMENT && tourParams.contentTypeId == "12" && tourParams.areaCode == "6" && tourParams.sigunguCode == "10" @@ -42,7 +42,7 @@ class TourAreaBasedServiceCore( return PRESET_AREA_TOUR_RESPONSE } - return tourApiClient.fetchTourInfo(tourParams, serviceSegment) + return tourApiClient.fetchTourInfo(tourParams, languageCode) } private companion object { diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourDetailServiceCore.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourDetailServiceCore.kt index 8aa19c8..64cde27 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourDetailServiceCore.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourDetailServiceCore.kt @@ -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 { diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourLocationBasedServiceCore.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourLocationBasedServiceCore.kt index cc8841c..ac9f05d 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourLocationBasedServiceCore.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/core/TourLocationBasedServiceCore.kt @@ -18,13 +18,13 @@ class TourLocationBasedServiceCore( key = "#tourParams.contentTypeId + '_' + #tourParams.areaCode + '_' + #tourParams.sigunguCode + '_' + " + "#locationParams.mapX + '_' + #locationParams.mapY + '_' + #locationParams.radius + '_' + " + - "#serviceSegment", + "#languageCode", unless = "#result == null", ) override fun fetchLocationBasedTours( tourParams: TourParams, locationParams: TourLocationBasedParams, - serviceSegment: String, + languageCode: String, ): TourResponse { if ( tourParams.contentTypeId == "39" && @@ -37,7 +37,7 @@ class TourLocationBasedServiceCore( return PRESET_LOCATION_BASED_RESPONSE } - return tourApiClient.fetchLocationBasedTours(tourParams, locationParams, serviceSegment) + return tourApiClient.fetchLocationBasedTours(tourParams, locationParams, languageCode) } private companion object { diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourAreaBasedUseCase.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourAreaBasedUseCase.kt index 8f5adca..66773ff 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourAreaBasedUseCase.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourAreaBasedUseCase.kt @@ -6,6 +6,6 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse interface TourAreaBasedUseCase { fun fetchAreaBasedTours( tourParams: TourParams, - serviceSegment: String, + languageCode: String, ): TourResponse } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourDetailUseCase.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourDetailUseCase.kt index 00e5344..c4a2a35 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourDetailUseCase.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourDetailUseCase.kt @@ -6,6 +6,6 @@ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse interface TourDetailUseCase { fun fetchTourDetail( detailParams: TourDetailParams, - serviceSegment: String, + languageCode: String, ): TourDetailResponse } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourLocationBasedUseCase.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourLocationBasedUseCase.kt index 6c042dd..8697576 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourLocationBasedUseCase.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/usecase/TourLocationBasedUseCase.kt @@ -8,6 +8,6 @@ interface TourLocationBasedUseCase { fun fetchLocationBasedTours( tourParams: TourParams, locationParams: TourLocationBasedParams, - serviceSegment: String, + languageCode: String, ): TourResponse } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/Region.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/Region.kt index 6ab3ba4..bf3aa69 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/Region.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/Region.kt @@ -1,21 +1,244 @@ package com.back.koreaTravelGuide.domain.user.enums enum class Region(val displayName: String) { + // 서울특별시 SEOUL("서울"), + + // 부산광역시 BUSAN("부산"), + + // 대구광역시 DAEGU("대구"), + + // 인천광역시 INCHEON("인천"), + BAENGNYEONG("백령도"), + GANGHWA("강화"), + + // 광주광역시 GWANGJU("광주"), + + // 대전광역시 DAEJEON("대전"), + + // 울산광역시 ULSAN("울산"), + + // 세종특별자치시 SEJONG("세종"), - GYEONGGI("경기"), - GANGWON("강원"), - CHUNGCHEONGBUK("충북"), - CHUNGCHEONGNAM("충남"), - JEOLLABUK("전북"), - JEOLLANAM("전남"), - GYEONGSANGBUK("경북"), - GYEONGSANGNAM("경남"), + + // 경기도 + GWACHEON("과천"), + GWANGMYEONG("광명"), + GIMPO("김포"), + SIHEUNG("시흥"), + ANSAN("안산"), + BUCHEON("부천"), + UIJEONGBU("의정부"), + GOYANG("고양"), + YANGJU("양주"), + PAJU("파주"), + DONGDUCHEON("동두천"), + YEONCHEON("연천"), + POCHEON("포천"), + GAPYEONG("가평"), + GURI("구리"), + NAMYANGJU("남양주"), + YANGPYEONG("양평"), + HANAM("하남"), + SUWON("수원"), + ANYANG("안양"), + OSAN("오산"), + HWASEONG("화성"), + SEONGNAM("성남"), + PYEONGTAEK("평택"), + UIWANG("의왕"), + GUNPO("군포"), + ANSEONG("안성"), + YONGIN("용인"), + ICHEON("이천"), + YEOJU("여주"), + + // 강원특별자치도 + CHEORWON("철원"), + HWACHEON("화천"), + INJE("인제"), + YANGGU("양구"), + CHUNCHEON("춘천"), + HONGCHEON("홍천"), + WONJU("원주"), + HOENGSEONG("횡성"), + YEONGWOL("영월"), + JEONGSEON("정선"), + PYEONGCHANG("평창"), + DAEGWALLYEONG("대관령"), + TAEBAEK("태백"), + SOKCHO("속초"), + YANGYANG("양양"), + GANGNEUNG("강릉"), + DONGHAE("동해"), + SAMCHEOK("삼척"), + + // 충청북도 + CHUNGJU("충주"), + JINCHEON("진천"), + EUMSEONG("음성"), + JECHEON("제천"), + DANYANG("단양"), + CHEONGJU("청주"), + BOEUN("보은"), + GOESAN("괴산"), + JEUNGPYEONG("증평"), + CHUPUNGNYEONG("추풍령"), + YEONGDONG("영동"), + OKCHEON("옥천"), + + // 충청남도 + SEOSAN("서산"), + TAEAN("태안"), + DANGJIN("당진"), + HONGSEONG("홍성"), + BORYEONG("보령"), + SEOCHEON("서천"), + CHEONAN("천안"), + ASAN("아산"), + YESAN("예산"), + GONGJU("공주"), + GYERYONG("계룡"), + BUYEO("부여"), + CHEONGYANG("청양"), + GEUMSAN("금산"), + NONSAN("논산"), + + // 전북특별자치도 + JEONJU("전주"), + IKSAN("익산"), + JEONGEUP("정읍"), + WANJU("완주"), + JANGSU("장수"), + MUJU("무주"), + JINAN("진안"), + NAMWON("남원"), + IMSIL("임실"), + SUNCHANG("순창"), + GUNSAN("군산"), + GIMJE("김제"), + GOCHANG("고창"), + BUAN("부안"), + + // 전라남도 + HAMPYEONG("함평"), + YEONGGWANG("영광"), + JINDO("진도"), + WANDO("완도"), + HAENAM("해남"), + GANGJIN("강진"), + JANGHEUNG("장흥"), + YEOSU("여수"), + GWANGYANG("광양"), + GOHEUNG("고흥"), + BOSEONG("보성"), + SUNCHEON("순천"), + JANGSEONG("장성"), + NAJU("나주"), + DAMYANG("담양"), + HWASUN("화순"), + GURYE("구례"), + GOKSEONG("곡성"), + HEUKSANDO("흑산도"), + MOKPO("목포"), + YEONGAM("영암"), + SINAN("신안"), + MUAN("무안"), + + // 경상북도 + ULLEUNGDO("울릉도"), + DOKDO("독도"), + ULJIN("울진"), + YEONGDEOK("영덕"), + POHANG("포항"), + GYEONGJU("경주"), + MUNGYEONG("문경"), + SANGJU("상주"), + YECHEON("예천"), + YEONGJU("영주"), + BONGHWA("봉화"), + YEONGYANG("영양"), + ANDONG("안동"), + UISEONG("의성"), + CHEONGSONG("청송"), + GIMCHEON("김천"), + GUMI("구미"), + GUNWI("군위"), + GORYEONG("고령"), + SEONGJU("성주"), + YEONGCHEON("영천"), + GYEONGSAN("경산"), + CHEONGDO("청도"), + CHILGOK("칠곡"), + + // 경상남도 + CHANGWON("창원"), + GIMHAE("김해"), + TONGYEONG("통영"), + SACHEON("사천"), + GEOJE("거제"), + GOSEONG("고성"), + NAMHAE("남해"), + HAMYANG("함양"), + GEOCHANG("거창"), + HAPCHEON("합천"), + MIRYANG("밀양"), + UIRYEONG("의령"), + HAMAN("함안"), + CHANGNYEONG("창녕"), + JINJU("진주"), + SANCHEONG("산청"), + HADONG("하동"), + YANGSAN("양산"), + + // 제주특별자치도 JEJU("제주"), + SEOGWIPO("서귀포"), + SEONGSAN("성산"), + SEONGPANAK("성판악"), + GOSAN("고산"), + IEODO("이어도"), + CHUJADO("추자도"), + ; + + companion object { + const val ALL_REGIONS_DESCRIPTION = + "SEOUL(서울), BUSAN(부산), DAEGU(대구), INCHEON(인천), BAENGNYEONG(백령도), GANGHWA(강화), " + + "GWANGJU(광주), DAEJEON(대전), ULSAN(울산), SEJONG(세종), " + + "GWACHEON(과천), GWANGMYEONG(광명), GIMPO(김포), SIHEUNG(시흥), ANSAN(안산), BUCHEON(부천), " + + "UIJEONGBU(의정부), GOYANG(고양), YANGJU(양주), PAJU(파주), DONGDUCHEON(동두천), " + + "YEONCHEON(연천), POCHEON(포천), GAPYEONG(가평), GURI(구리), NAMYANGJU(남양주), " + + "YANGPYEONG(양평), HANAM(하남), SUWON(수원), ANYANG(안양), OSAN(오산), HWASEONG(화성), " + + "SEONGNAM(성남), PYEONGTAEK(평택), UIWANG(의왕), GUNPO(군포), ANSEONG(안성), YONGIN(용인), " + + "ICHEON(이천), YEOJU(여주), CHEORWON(철원), HWACHEON(화천), INJE(인제), YANGGU(양구), " + + "CHUNCHEON(춘천), HONGCHEON(홍천), WONJU(원주), HOENGSEONG(횡성), YEONGWOL(영월), " + + "JEONGSEON(정선), PYEONGCHANG(평창), DAEGWALLYEONG(대관령), TAEBAEK(태백), SOKCHO(속초), " + + "YANGYANG(양양), GANGNEUNG(강릉), DONGHAE(동해), SAMCHEOK(삼척), CHUNGJU(충주), " + + "JINCHEON(진천), EUMSEONG(음성), JECHEON(제천), DANYANG(단양), CHEONGJU(청주), " + + "BOEUN(보은), GOESAN(괴산), JEUNGPYEONG(증평), CHUPUNGNYEONG(추풍령), YEONGDONG(영동), " + + "OKCHEON(옥천), SEOSAN(서산), TAEAN(태안), DANGJIN(당진), HONGSEONG(홍성), BORYEONG(보령), " + + "SEOCHEON(서천), CHEONAN(천안), ASAN(아산), YESAN(예산), GONGJU(공주), GYERYONG(계룡), " + + "BUYEO(부여), CHEONGYANG(청양), GEUMSAN(금산), NONSAN(논산), JEONJU(전주), IKSAN(익산), " + + "JEONGEUP(정읍), WANJU(완주), JANGSU(장수), MUJU(무주), JINAN(진안), NAMWON(남원), " + + "IMSIL(임실), SUNCHANG(순창), GUNSAN(군산), GIMJE(김제), GOCHANG(고창), BUAN(부안), " + + "HAMPYEONG(함평), YEONGGWANG(영광), JINDO(진도), WANDO(완도), HAENAM(해남), GANGJIN(강진), " + + "JANGHEUNG(장흥), YEOSU(여수), GWANGYANG(광양), GOHEUNG(고흥), BOSEONG(보성), " + + "SUNCHEON(순천), JANGSEONG(장성), NAJU(나주), DAMYANG(담양), HWASUN(화순), GURYE(구례), " + + "GOKSEONG(곡성), HEUKSANDO(흑산도), MOKPO(목포), YEONGAM(영암), SINAN(신안), MUAN(무안), " + + "ULLEUNGDO(울릉도), DOKDO(독도), ULJIN(울진), YEONGDEOK(영덕), POHANG(포항), GYEONGJU(경주), " + + "MUNGYEONG(문경), SANGJU(상주), YECHEON(예천), YEONGJU(영주), BONGHWA(봉화), " + + "YEONGYANG(영양), ANDONG(안동), UISEONG(의성), CHEONGSONG(청송), GIMCHEON(김천), " + + "GUMI(구미), GUNWI(군위), GORYEONG(고령), SEONGJU(성주), YEONGCHEON(영천), GYEONGSAN(경산), " + + "CHEONGDO(청도), CHILGOK(칠곡), CHANGWON(창원), GIMHAE(김해), TONGYEONG(통영), " + + "SACHEON(사천), GEOJE(거제), GOSEONG(고성), NAMHAE(남해), HAMYANG(함양), GEOCHANG(거창), " + + "HAPCHEON(합천), MIRYANG(밀양), UIRYEONG(의령), HAMAN(함안), CHANGNYEONG(창녕), " + + "JINJU(진주), SANCHEONG(산청), HADONG(하동), YANGSAN(양산), JEJU(제주), SEOGWIPO(서귀포), " + + "SEONGSAN(성산), SEONGPANAK(성판악), GOSAN(고산), IEODO(이어도), CHUJADO(추자도)" + } } diff --git a/src/main/resources/content-type-id.yml b/src/main/resources/content-type-id.yml index 0c7b407..525bf66 100644 --- a/src/main/resources/content-type-id.yml +++ b/src/main/resources/content-type-id.yml @@ -8,4 +8,13 @@ tour: 레포츠: "28" 숙박: "32" 쇼핑: "38" - 음식점: "39" \ No newline at end of file + 음식점: "39" + foreign-codes: + 레포츠: "75" + 관광지: "76" + 교통: "77" + 문화시설: "78" + 쇼핑: "79" + 숙박: "80" + 음식점: "82" + 축제공연행사: "85" \ No newline at end of file diff --git a/src/main/resources/prompts.yml b/src/main/resources/prompts.yml index 28a29c5..1f94479 100644 --- a/src/main/resources/prompts.yml +++ b/src/main/resources/prompts.yml @@ -3,7 +3,7 @@ prompts: korea-travel-guide: | 당신은 한국 여행 전문 AI 챗봇입니다. 사용자에게 날씨 기반 여행지를 추천하고, 구체적인 관광 정보를 제공하는 것이 목표입니다. - 답변은 친근하고 자연스러운 한국어로 작성하세요. + 답변은 사용자 메시지의 언어로 작성하세요. # 기본 추천 플로우 (사용자의 별도 요청이 없는 경우) @@ -29,9 +29,8 @@ prompts: - 예: "관광지, 음식점, 숙박시설 중 어떤 정보가 필요하신가요?" 또는 "축제나 레포츠 정보도 알려드릴 수 있어요!" 4단계: 지역 기반 관광정보 조회 - - 사용자가 타입을 선택하면 getAreaBasedTourInfo(contentTypeId, areaAndSigunguCode)를 사용하세요. - - contentTypeId: 관광지=12, 문화시설=14, 축제공연행사=15, 여행코스=25, 레포츠=28, 숙박=32, 쇼핑=38, 음식점=39 - - areaAndSigunguCode: Tool 파라미터 설명을 참고하여 지역 코드를 정확히 변환하세요 (하이픈→쉼표) + - 사용자가 타입을 선택하면 getAreaBasedTourInfo를 사용하세요. + - 한 번에 하나의 contentTypeId만 조회하세요. 여러 타입을 동시에 조회하지 마세요. - 조회된 관광정보를 사용자에게 친근하게 추천하세요. - 추천할 때는 장소 이름, 주소, 특징을 포함하여 3~5개 정도 제시하세요. - 이미지가 있는 경우(firstimage 필드), 반드시 마크다운 형식으로 포함하세요: ![장소의 title](firstimage URL) @@ -39,31 +38,27 @@ prompts: - 예: "이 지역에서 활동하는 여행 가이드를 찾아드릴까요?", "현지 가이드와 함께하면 더 깊이 있는 여행이 가능해요. 가이드 정보를 알아볼까요?" 4-1단계: 지역 가이드 검색 - - 사용자가 가이드 정보를 요청하면 findGuidesByRegion(region)을 사용하세요. - - region 파라미터에는 사용자가 이전에 조회한 지역명(예: '서울', '부산', '강남구')을 사용하세요. + - 사용자가 가이드 정보를 요청하면 findGuidesByRegion을 사용하세요. - 검색된 가이드 목록을 친근하게 소개하세요. - 각 가이드의 이름, 활동 지역, 전문 분야 등을 포함하여 제시하세요. - 가이드가 없는 경우, "죄송합니다. 해당 지역에서 활동하는 가이드를 찾을 수 없네요. 다른 지역을 추천해드릴까요?" 라고 안내하세요. 5단계: 위치 기반 주변 검색 (특정 장소 주변) - - 사용자가 이전에 조회한 장소 주변의 다른 정보를 요청하면 getLocationBasedTourInfo()를 사용하세요. + - 사용자가 이전에 조회한 장소 주변의 다른 정보를 요청하면 getLocationBasedTourInfo를 사용하세요. - 이전 Tool 호출 결과(getAreaBasedTourInfo)에서 받은 해당 장소의 mapX(경도), mapY(위도) 좌표를 활용하세요. - - radius(검색 반경)는 사용자가 명시하지 않으면 1000(1km)을 기본값으로 사용하세요. - - 예시 플로우: 이전 Tool 결과에서 받은 특정 장소의 mapX, mapY를 사용 → getLocationBasedTourInfo(contentTypeId, areaCode, mapX, mapY, radius=1000) 6단계: 관광지 상세 정보 조회 - - 사용자가 이전에 조회한 장소의 상세 정보를 요청하면 getTourDetailInfo()를 사용하세요. - - 이전 Tool 호출 결과에서 받은 해당 장소의 contentId를 사용하세요. + - 사용자가 이전에 조회한 장소의 상세 정보를 요청하면 getTourDetailInfo를 사용하세요. + - 이전 Tool 호출 결과(getAreaBasedTourInfo)에서 받은 해당 장소의 contentId를 활용하세요. - 상세 정보(overview, homepage, tel 등)를 친근하게 요약해서 전달하세요. # 독립적 Tool 사용 (사용자가 직접 요청한 경우) 각 Tool은 사용자의 직접적인 요청이 있을 경우 위 플로우를 건너뛰고 바로 사용할 수 있습니다. - - 특정 지역 날씨 요청 → getRegionalWeatherDetails(location)를 바로 사용하되, REGION_CODES_DESCRIPTION에서 해당 지역 코드를 찾아 사용 - - 특정 구/군 관광정보 요청 → getAreaBasedTourInfo(contentTypeId, areaAndSigunguCode)를 바로 사용하되, - CONTENT_TYPE_CODES_DESCRIPTION에서 타입 코드를 찾고, AREA_CODES_DESCRIPTION에서 지역 코드를 찾아 하이픈을 쉼표로 변환 - - 특정 지역 가이드 요청 → findGuidesByRegion(region)을 바로 사용하여 해당 지역의 가이드 목록 제공 - - 특정 장소 주변 검색 → 먼저 getAreaBasedTourInfo()로 해당 장소를 찾아 mapX, mapY를 얻은 후 getLocationBasedTourInfo() 사용 - - 특정 장소 상세 정보 요청 → 먼저 getAreaBasedTourInfo()로 검색 후 contentId를 얻어 getTourDetailInfo() 사용 + - 특정 지역 날씨 요청 → getRegionalWeatherDetails를 바로 사용 + - 특정 구/군 관광정보 요청 → getAreaBasedTourInfo를 바로 사용 + - 특정 지역 가이드 요청 → findGuidesByRegion을 바로 사용하여 해당 지역의 가이드 목록 제공 + - 특정 장소 주변 검색 → 먼저 getAreaBasedTourInfo로 해당 장소를 찾아 mapX, mapY를 얻은 후 getLocationBasedTourInfo 사용 + - 특정 장소 상세 정보 요청 → 먼저 getAreaBasedTourInfo로 검색 후 contentId를 얻어 getTourDetailInfo 사용 # 중요 원칙 - 사용자 경험을 자연스럽게 이어가세요. 로봇처럼 딱딱하게 말하지 마세요.