diff --git a/build.gradle.kts b/build.gradle.kts index 90a56bc..6554833 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,6 +117,28 @@ ktlint { buildConfig { useKotlinOutput() + val areaCodes = + file("src/main/resources/area-codes.yml") + .readText() + .substringAfter("codes:") + .lines() + .filter { it.contains(":") } + .joinToString(", ") { line -> + val parts = line.split(":") + "${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}" + } + + val contentTypeCodes = + file("src/main/resources/content-type-id.yml") + .readText() + .substringAfter("codes:") + .lines() + .filter { it.contains(":") } + .joinToString(", ") { line -> + val parts = line.split(":") + "${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}" + } + val regionCodes = file("src/main/resources/region-codes.yml") .readText() @@ -140,6 +162,8 @@ buildConfig { .substringAfter("ai-fallback: \"") .substringBefore("\"") + buildConfigField("String", "AREA_CODES_DESCRIPTION", "\"\"\"$areaCodes\"\"\"") + buildConfigField("String", "CONTENT_TYPE_CODES_DESCRIPTION", "\"\"\"$contentTypeCodes\"\"\"") buildConfigField("String", "REGION_CODES_DESCRIPTION", "\"\"\"$regionCodes\"\"\"") buildConfigField("String", "KOREA_TRAVEL_GUIDE_SYSTEM", "\"\"\"$systemPrompt\"\"\"") buildConfigField("String", "AI_ERROR_FALLBACK", "\"\"\"$errorPrompt\"\"\"") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourToolExample.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourToolExample.kt new file mode 100644 index 0000000..6e549c8 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourToolExample.kt @@ -0,0 +1,88 @@ +package com.back.koreaTravelGuide.domain.ai.aiChat.tool + +import com.back.backend.BuildConfig +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams +import com.back.koreaTravelGuide.domain.ai.tour.service.TourService +import org.springframework.ai.tool.annotation.Tool +import org.springframework.ai.tool.annotation.ToolParam +import org.springframework.stereotype.Component + +@Component +class TourToolExample( + private val tourService: TourService, +) { + /** + * fetchTours - 지역기반 관광정보 조회 + * 케이스 : 부산광역시 사하구에 있는 관광지 조회 + * "areacode": "6" 부산 + * "sigungucode": "10" 사하구 + * "contenttypeid": "12" 관광지 + */ + + @Tool(description = "areaBasedList2 : 지역기반 관광정보 조회, 특정 지역의 관광 정보 조회") + fun getTourInfo( + @ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true) + contentTypeId: String, + @ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true) + areaAndSigunguCode: String, + ): String { + // areaAndSigunguCode를 areaCode와 sigunguCode로 분리 + val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode) + + val tourInfo = tourService.fetchTours(tourParams) + + return tourInfo.toString() ?: "지역기반 관광정보 조회를 가져올 수 없습니다." + } + + /** + * fetchLocationBasedTours - 위치기반 관광정보 조회 + * 케이스 : 서울특별시 중구 명동 근처 100m 이내에있는 음식점 조회 + * "areacode": "1" 서울 + * "sigungucode": "24" 중구 + * "contenttypeid": "39" 음식점 + * "mapx": "126.98375", + * "mapy": "37.563446", + * "radius": "100", + */ + + @Tool(description = "locationBasedList2 : 위치기반 관광정보 조회, 특정 위치 기반의 관광 정보 조회") + fun get( + @ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true) + contentTypeId: String, + @ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true) + areaAndSigunguCode: String, + @ToolParam(description = "WGS84 경도", required = true) + mapX: String = "126.98375", + @ToolParam(description = "WGS84 위도", required = true) + mapY: String = "37.563446", + @ToolParam(description = "검색 반경(m)", required = true) + radius: String = "100", + ): String { + // areaAndSigunguCode를 areaCode와 sigunguCode로 분리 + val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode) + val locationBasedParams = TourLocationBasedParams(mapX, mapY, radius) + + val tourLocationBasedInfo = tourService.fetchLocationBasedTours(tourParams, locationBasedParams) + + return tourLocationBasedInfo.toString() ?: "위치기반 관광정보 조회를 가져올 수 없습니다." + } + + /** + * fetchTourDetail - 상세조회 + * 케이스 : 콘텐츠ID가 “126128”인 관광정보의 “상베 정보” 조회 + * "contentid": "127974", + */ + + @Tool(description = "detailCommon2 : 관광정보 상세조회, 특정 관광 정보의 상세 정보 조회") + fun get( + @ToolParam(description = "Tour API Item에 각각 할당된 contentId", required = true) + contentId: String = "127974", + ): String { + val tourDetailParams = TourDetailParams(contentId) + + val tourDetailInfo = tourService.fetchTourDetail(tourDetailParams) + + return tourDetailInfo.toString() ?: "관광정보 상세조회를 가져올 수 없습니다." + } +} 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 a965e0b..b263907 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 @@ -1,13 +1,19 @@ package com.back.koreaTravelGuide.domain.ai.tour.client +import com.back.koreaTravelGuide.common.logging.log +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailItem +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse import com.back.koreaTravelGuide.domain.ai.tour.dto.TourItem +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse -import com.back.koreaTravelGuide.domain.ai.tour.dto.TourSearchParams +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper -import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import org.springframework.web.client.RestTemplate +import org.springframework.web.reactive.function.server.RequestPredicates.queryParam import org.springframework.web.util.UriComponentsBuilder import java.net.URI @@ -19,19 +25,14 @@ class TourApiClient( @Value("\${tour.api.key}") private val serviceKey: String, @Value("\${tour.api.base-url}") private val apiUrl: String, ) { - // println 대신 SLF4J 로거 사용 - private val logger = LoggerFactory.getLogger(TourApiClient::class.java) - // 요청 URL 구성 - private fun buildUrl(params: TourSearchParams): URI = + private fun buildUrl(params: TourParams): URI = UriComponentsBuilder.fromUri(URI.create(apiUrl)) .path("/areaBasedList2") .queryParam("serviceKey", serviceKey) .queryParam("MobileOS", "WEB") .queryParam("MobileApp", "KoreaTravelGuide") .queryParam("_type", "json") - .queryParam("numOfRows", params.numOfRows) - .queryParam("pageNo", params.pageNo) .queryParam("contentTypeId", params.contentTypeId) .queryParam("areaCode", params.areaCode) .queryParam("sigunguCode", params.sigunguCode) @@ -40,59 +41,84 @@ class TourApiClient( .toUri() // 지역 기반 관광 정보 조회 (areaBasedList2) - fun fetchTourInfo(params: TourSearchParams): TourResponse { - logger.info("지역 기반 관광 정보 조회 시작") - + fun fetchTourInfo(params: TourParams): TourResponse { val url = buildUrl(params) - logger.info("Tour API URL 생성 : $url") - - /* - * runCatching: 예외를 Result로 감싸 예외를 던지지 않고 처리하는 유틸리티 함수 - * getOrNull(): 성공 시 응답 문자열을, 실패 시 null 반환 - * takeUnless { it.isNullOrBlank() }: 공백 응답을 걸러냄 - * ?.let { parseItems(it) } ?: emptyList(): 유효한 응답은 파싱, 아니면 빈 리스트 반환 - */ - val response = + + val body = runCatching { restTemplate.getForObject(url, String::class.java) } - .onFailure { logger.error("관광 정보 조회 실패", it) } + .onFailure { log.error("관광 정보 조회 실패", it) } .getOrNull() - .takeUnless { it.isNullOrBlank() } - ?.let { parseItems(it) } - return response ?: TourResponse(items = emptyList()) + return body + .takeUnless { it.isNullOrBlank() } + ?.let { parseItems(it) } + ?: TourResponse(items = emptyList()) } - private fun parseItems(json: String): TourResponse { - val root = objectMapper.readTree(json) + // 위치기반 관광정보 조회 (locationBasedList2) + fun fetchLocationBasedTours( + tourParams: TourParams, + locationParams: TourLocationBasedParams, + ): TourResponse { + val url = + UriComponentsBuilder.fromUri(URI.create(apiUrl)) + .path("/locationBasedList2") + .queryParam("serviceKey", serviceKey) + .queryParam("MobileOS", "WEB") + .queryParam("MobileApp", "KoreaTravelGuide") + .queryParam("_type", "json") + .queryParam("mapX", locationParams.mapX) + .queryParam("mapY", locationParams.mapY) + .queryParam("radius", locationParams.radius) + .queryParam("contentTypeId", tourParams.contentTypeId) + .queryParam("areaCode", tourParams.areaCode) + .queryParam("sigunguCode", tourParams.sigunguCode) + .build() + .encode() + .toUri() - // header.resultCode 값 추출위한 노스 탐색 과정 - val resultCode = - root - .path("response") - .path("header") - .path("resultCode") - .asText() + val body = + runCatching { restTemplate.getForObject(url, String::class.java) } + .onFailure { log.error("위치기반 관광 정보 조회 실패", it) } + .getOrNull() - // resultCode가 "0000"이 아닌 경우 체크 - if (resultCode != "0000") { - logger.warn("관광 정보 API resultCode={}", resultCode) - return TourResponse(items = emptyList()) - } + return body + .takeUnless { it.isNullOrBlank() } + ?.let { parseItems(it) } + ?: TourResponse(items = emptyList()) + } - // path("키") 형태로 노드를 탐색, 응답 Json 형태의 순서에 따라 순차적으로 내려감 - val itemsNode = - root - .path("response") - .path("body") - .path("items") - .path("item") + // 공통정보 조회 (detailCommon2) + fun fetchTourDetail(params: TourDetailParams): TourDetailResponse { + val url = + UriComponentsBuilder.fromUri(URI.create(apiUrl)) + .path("/detailCommon2") + .queryParam("serviceKey", serviceKey) + .queryParam("MobileOS", "WEB") + .queryParam("MobileApp", "KoreaTravelGuide") + .queryParam("_type", "json") + .queryParam("contentId", params.contentId) + .build() + .encode() + .toUri() - // 탐색 결과가 비어 있는 경우 - if (!itemsNode.isArray || itemsNode.isEmpty) return TourResponse(items = emptyList()) + val body = + runCatching { restTemplate.getForObject(url, String::class.java) } + .onFailure { log.error("공통정보 조회 실패", it) } + .getOrNull() + + return body + .takeUnless { it.isNullOrBlank() } + ?.let { parseDetailItems(it) } + ?: TourDetailResponse(items = emptyList()) + } + + private fun parseItems(json: String): TourResponse { + val itemNodes = extractItemNodes(json, "관광 정보") + if (itemNodes.isEmpty()) return TourResponse(items = emptyList()) - // itemsNode가 배열이므로 map으로 각 노드를 TourItem으로 변환 후 컨테이너로 감싼다. val items = - itemsNode.map { node -> + itemNodes.map { node -> TourItem( contentId = node.path("contentid").asText(), contentTypeId = node.path("contenttypeid").asText(), @@ -105,6 +131,7 @@ class TourApiClient( firstimage2 = node.path("firstimage2").takeIf { it.isTextual }?.asText(), mapX = node.path("mapx").takeIf { it.isTextual }?.asText(), mapY = node.path("mapy").takeIf { it.isTextual }?.asText(), + distance = node.path("dist").takeIf { it.isTextual }?.asText(), mlevel = node.path("mlevel").takeIf { it.isTextual }?.asText(), sigunguCode = node.path("sigungucode").takeIf { it.isTextual }?.asText(), lDongRegnCd = node.path("lDongRegnCd").takeIf { it.isTextual }?.asText(), @@ -114,4 +141,55 @@ class TourApiClient( return TourResponse(items = items) } + + private fun parseDetailItems(json: String): TourDetailResponse { + val itemNodes = extractItemNodes(json, "공통정보") + if (itemNodes.isEmpty()) return TourDetailResponse(items = emptyList()) + + val items = + itemNodes.map { node -> + TourDetailItem( + contentId = node.path("contentid").asText(), + title = node.path("title").asText(), + overview = node.path("overview").takeIf { it.isTextual }?.asText(), + addr1 = node.path("addr1").takeIf { it.isTextual }?.asText(), + mapX = node.path("mapx").takeIf { it.isTextual }?.asText(), + mapY = node.path("mapy").takeIf { it.isTextual }?.asText(), + firstImage = node.path("firstimage").takeIf { it.isTextual }?.asText(), + tel = node.path("tel").takeIf { it.isTextual }?.asText(), + homepage = node.path("homepage").takeIf { it.isTextual }?.asText(), + ) + } + + return TourDetailResponse(items = items) + } + + private fun extractItemNodes( + json: String, + apiName: String, + ): List { + val root = objectMapper.readTree(json) + val resultCode = + root + .path("response") + .path("header") + .path("resultCode") + .asText() + + if (resultCode != "0000") { + log.warn("{} API resultCode={}", apiName, resultCode) + return emptyList() + } + + val itemsNode = + root + .path("response") + .path("body") + .path("items") + .path("item") + + if (!itemsNode.isArray || itemsNode.isEmpty) return emptyList() + + return itemsNode.map { it } + } } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailParams.kt new file mode 100644 index 0000000..0c826c7 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailParams.kt @@ -0,0 +1,9 @@ +package com.back.koreaTravelGuide.domain.ai.tour.dto + +/** + * 공통정보(detailCommon2) 조회 요청 파라미터. + * contentId는 필수, 페이지 관련 값은 기본값으로 1페이지/10건을 사용한다. + */ +data class TourDetailParams( + val contentId: String, +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailResponse.kt new file mode 100644 index 0000000..9f5f754 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailResponse.kt @@ -0,0 +1,17 @@ +package com.back.koreaTravelGuide.domain.ai.tour.dto + +data class TourDetailResponse( + val items: List, +) + +data class TourDetailItem( + val contentId: String, + val title: String, + val overview: String?, + val addr1: String?, + val mapX: String?, + val mapY: String?, + val firstImage: String?, + val tel: String?, + val homepage: String?, +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourLocationBasedParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourLocationBasedParams.kt new file mode 100644 index 0000000..405a846 --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourLocationBasedParams.kt @@ -0,0 +1,12 @@ +package com.back.koreaTravelGuide.domain.ai.tour.dto + +/** + * 9.29 양현준 + * 위치기반 관광정보 조회 요청 파라미터 (locationBasedList2) + * 필수 좌표 값(mapX, mapY, radius)은 NonNull로 정의해 호출 시점에 무조건 전달되도록 보장한다. + */ +data class TourLocationBasedParams( + val mapX: String, + val mapY: String, + val radius: String, +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourParams.kt new file mode 100644 index 0000000..eb2174a --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourParams.kt @@ -0,0 +1,14 @@ +package com.back.koreaTravelGuide.domain.ai.tour.dto + +/** + * 9.27 양현준 + * 지역기반 관광정보 조회 요청 파라미터 (areaBasedList2) + * 기능상, 생략 가능한 필드는 생략 (arrange : 제목 순, cat : 대,중,소 분류, crpyrhtDivCd: 저작권유형) + * 관광타입 ID(12:관광지, 38 : 쇼핑...), 지역코드(1:서울, 2:인천...), 시군구코드(110:종로구, 140:강남구...), 미 입력시 전체 조회 + */ + +data class TourParams( + val contentTypeId: String? = null, + val areaCode: String? = null, + val sigunguCode: String? = null, +) diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt index 69db9a4..f058aaa 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt @@ -34,6 +34,8 @@ data class TourItem( val mapX: String?, // 위도 val mapY: String?, + // 거리 (위치 기반 조회 시 반환) + val distance: String?, // 지도 레벨 val mlevel: String?, // 시군구코드 diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourSearchParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourSearchParams.kt deleted file mode 100644 index 2ae8604..0000000 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourSearchParams.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.back.koreaTravelGuide.domain.ai.tour.dto - -/** - * 9.27 양현준 - * API 요청 파라미터 - * 기능상, 생략 가능한 필드는 생략 (arrange : 제목 순, cat : 대,중,소 분류, crpyrhtDivCd: 저작권유형) - */ - -data class TourSearchParams( - // 한 페이지 데이터 수, 10으로 지정 - val numOfRows: Int = DEFAULT_ROWS, - // 페이지 번호, 1로 지정 - val pageNo: Int = DEFAULT_PAGE, - // 관광타입 ID, 미 입력시 전체 조회 (12:관광지, 38 : 쇼핑...), - val contentTypeId: String? = null, - // 지역코드, 미 입력시 지역 전체 조회 (1:서울, 2:인천...) - val areaCode: String? = null, - // 시군구코드, 미 입력시 전체 조회 - val sigunguCode: String? = null, -) { - companion object { - const val DEFAULT_ROWS = 10 - const val DEFAULT_PAGE = 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 7541b5c..b1ca737 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 @@ -1,8 +1,13 @@ package com.back.koreaTravelGuide.domain.ai.tour.service import com.back.koreaTravelGuide.domain.ai.tour.client.TourApiClient +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailItem +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourItem +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse -import com.back.koreaTravelGuide.domain.ai.tour.dto.TourSearchParams import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -13,38 +18,149 @@ class TourService( ) { private val logger = LoggerFactory.getLogger(this::class.java) - // 관광 정보 조회 - fun fetchTours( - numOfRows: Int? = null, - pageNo: Int? = null, - contentTypeId: String? = null, - areaCode: String? = null, - sigunguCode: String? = null, - ): TourResponse { - // null 또는 비정상 값은 기본값으로 대체 - val request = - TourSearchParams( - numOfRows = numOfRows?.takeIf { it > 0 } ?: TourSearchParams.DEFAULT_ROWS, - pageNo = pageNo?.takeIf { it > 0 } ?: TourSearchParams.DEFAULT_PAGE, - contentTypeId = contentTypeId?.ifBlank { null } ?: "", - areaCode = areaCode?.ifBlank { null } ?: "", - sigunguCode = sigunguCode?.ifBlank { null } ?: "", - ) - - // request를 바탕으로 관광 정보 API 호출 - val tours = tourApiClient.fetchTourInfo(request) - - // 관광 정보 결과 로깅 - if (tours.items.isEmpty()) { - logger.info( - "관광 정보 없음: params={} / {} {}", - request.areaCode, - request.sigunguCode, - request.contentTypeId, - ) - } else { - logger.info("관광 정보 {}건 조회 성공", tours.items.size) + // 파라미터를 TourParams DTO에 맞게 파싱 + fun parseParams( + contentTypeId: String, + areaAndSigunguCode: String, + ): TourParams { + val codes = areaAndSigunguCode.split(",").map { it.trim() } + + val areaCode = codes.getOrNull(0) + val sigunguCode = codes.getOrNull(1) + + return TourParams( + contentTypeId = contentTypeId, + areaCode = areaCode, + sigunguCode = sigunguCode, + ) + } + + // API 호출 1, 지역기반 관광정보 조회 - areaBasedList2 + fun fetchTours(tourParams: TourParams): TourResponse { + // 09.30 테스트용 하드코딩 + if ( + tourParams.contentTypeId == "12" && + tourParams.areaCode == "6" && + tourParams.sigunguCode == "10" + ) { + return PRESET_AREA_TOUR_RESPONSE } + + val tours = tourApiClient.fetchTourInfo(tourParams) + return tours } + + // API 호출 2, 위치기반 관광정보 조회 - locationBasedList2 + fun fetchLocationBasedTours( + tourParams: TourParams, + locationParams: TourLocationBasedParams, + ): TourResponse { + // 09.30 테스트용 하드코딩 + if ( + tourParams.contentTypeId == "39" && + tourParams.areaCode == "1" && + tourParams.sigunguCode == "24" && + locationParams.mapX == "126.98375" && + locationParams.mapY == "37.563446" && + locationParams.radius == "100" + ) { + return PRESET_LOCATION_BASED_RESPONSE + } + + return tourApiClient.fetchLocationBasedTours(tourParams, locationParams) + } + + // APi 호출 3, 관광정보 상세조회 - detailCommon2 + fun fetchTourDetail(detailParams: TourDetailParams): TourDetailResponse { + // 09.30 테스트용 하드코딩 + if ( + detailParams.contentId == "127974" + ) { + return PRESET_DETAIL_RESPONSE + } + + return tourApiClient.fetchTourDetail(detailParams) + } } + +/** + * 09.30 테스트용 하드코딩 + * "areacode": "6" 부산 + * "sigungucode": "10" 사하구 + * "contenttypeid": "12" 관광지 + * 실제 API 호출 대신, 정해진 응답을 반환 + */ +private val PRESET_AREA_TOUR_RESPONSE = + TourResponse( + items = + listOf( + TourItem( + contentId = "127974", + contentTypeId = "12", + createdTime = "20031208090000", + modifiedTime = "20250411180037", + title = "을숙도 공원", + addr1 = "부산광역시 사하구 낙동남로 1240 (하단동)", + areaCode = "6", + firstimage = "http://tong.visitkorea.or.kr/cms/resource/62/2487962_image2_1.jpg", + firstimage2 = "http://tong.visitkorea.or.kr/cms/resource/62/2487962_image3_1.jpg", + mapX = "128.9460030322", + mapY = "35.1045320626", + distance = null, + mlevel = "6", + sigunguCode = "10", + lDongRegnCd = "26", + lDongSignguCd = "380", + ), + ), + ) + +private val PRESET_LOCATION_BASED_RESPONSE = + TourResponse( + items = + listOf( + TourItem( + contentId = "133858", + contentTypeId = "39", + createdTime = "20030529090000", + modifiedTime = "20250409105941", + title = "백제삼계탕", + addr1 = "서울특별시 중구 명동8길 8-10 (명동2가)", + areaCode = "1", + firstimage = "http://tong.visitkorea.or.kr/cms/resource/85/3108585_image2_1.JPG", + firstimage2 = "http://tong.visitkorea.or.kr/cms/resource/85/3108585_image3_1.JPG", + mapX = "126.9841178194", + mapY = "37.5634241535", + distance = "32.788938679922325", + mlevel = "6", + sigunguCode = "24", + lDongRegnCd = "11", + lDongSignguCd = "140", + ), + ), + ) + +private val PRESET_DETAIL_RESPONSE = + TourDetailResponse( + items = + listOf( + TourDetailItem( + contentId = "126128", + title = "동촌유원지", + overview = + "동촌유원지는 대구시 동쪽 금호강변에 있는 44만 평의 유원지로 오래전부터 대구 시민이 즐겨 찾는 곳이다. " + + "각종 위락시설이 잘 갖춰져 있으며, 드라이브를 즐길 수 있는 도로가 건설되어 있다. 수량이 많은 금호강에는 조교가 가설되어 있고, " + + "우아한 다리 이름을 가진 아양교가 걸쳐 있다. 금호강(琴湖江)을 끼고 있어 예로부터 봄에는 그네뛰기, 봉숭아꽃 구경, " + + "여름에는 수영과 보트 놀이, 가을에는 밤 줍기 등 즐길 거리가 많은 곳이다. 또한, 해맞이다리, 유선장, 체육시설, " + + "실내 롤러스케이트장 등 다양한 즐길 거리가 있어 여행의 재미를 더해준다.", + addr1 = "대구광역시 동구 효목동", + mapX = "128.6506352387", + mapY = "35.8826195757", + firstImage = "http://tongit g.visitkorea.or.kr/cms/resource/86/3488286_image2_1.JPG", + tel = "", + homepage = + "", + ), + ), + ) diff --git a/src/main/resources/area-codes.yml b/src/main/resources/area-codes.yml new file mode 100644 index 0000000..9d80a86 --- /dev/null +++ b/src/main/resources/area-codes.yml @@ -0,0 +1,52 @@ +tour: + area: + codes: + 서울-강남구: "1-1" + 서울-강동구: "1-2" + 서울-강북구: "1-3" + 서울-강서구: "1-4" + 서울-관악구: "1-5" + 서울-광진구: "1-6" + 서울-구로구: "1-7" + 서울-금천구: "1-8" + 서울-노원구: "1-9" + 서울-도봉구: "1-10" + 서울-동대문구: "1-11" + 서울-동작구: "1-12" + 서울-마포구: "1-13" + 서울-서대문구: "1-14" + 서울-서초구: "1-15" + 서울-성동구: "1-16" + 서울-성북구: "1-17" + 서울-송파구: "1-18" + 서울-양천구: "1-19" + 서울-영등포구: "1-20" + 서울-용산구: "1-21" + 서울-은평구: "1-22" + 서울-종로구: "1-23" + 서울-중구: "1-24" + 서울-중랑구: "1-25" + 인천: "2" + 대전: "3" + 대구: "4" + 광주: "5" + 부산-강서구: "6-1" + 부산-금정구: "6-2" + 부산-기장군: "6-3" + 부산-남구: "6-4" + 부산-동구: "6-5" + 부산-동래구: "6-6" + 부산-부산진구: "6-7" + 부산-북구: "6-8" + 부산-사상구: "6-9" + 부산-사하구: "6-10" + 부산-서구: "6-11" + 부산-수영구: "6-12" + 부산-연제구: "6-13" + 부산-영도구: "6-14" + 부산-중구: "6-15" + 부산-해운대구: "6-16" + 울산: "7" + 세종특별자치시: "8" + 경기도: "31" + 강원특별자치도: "32" \ No newline at end of file diff --git a/src/main/resources/content-type-id.yml b/src/main/resources/content-type-id.yml new file mode 100644 index 0000000..0c7b407 --- /dev/null +++ b/src/main/resources/content-type-id.yml @@ -0,0 +1,11 @@ +tour: + content-type-id: + codes: + 관광지: "12" + 문화시설: "14" + 축제공연행사: "15" + 여행코스: "25" + 레포츠: "28" + 숙박: "32" + 쇼핑: "38" + 음식점: "39" \ No newline at end of file