From 24c3c7bc3c196b6dcab0fd16d2cd840811152966 Mon Sep 17 00:00:00 2001 From: YangHJ2415 Date: Tue, 30 Sep 2025 12:40:05 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat(be):=20=EC=9C=84=EC=B9=98=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98,=20=EA=B3=B5=ED=86=B5=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ai/tour/client/TourApiClient.kt | 153 +++++++++++++----- .../ai/tour/dto/LocationBasedSearchParams.kt | 15 ++ .../domain/ai/tour/dto/TourResponse.kt | 2 + .../domain/ai/tour/dto/TourSearchParams.kt | 17 +- .../domain/ai/tour/service/TourService.kt | 44 ++--- 5 files changed, 150 insertions(+), 81 deletions(-) create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedSearchParams.kt 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..e1e198d 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,8 +1,13 @@ package com.back.koreaTravelGuide.domain.ai.tour.client +import com.back.koreaTravelGuide.domain.ai.tour.dto.LocationBasedSearchParams +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.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 @@ -30,8 +35,6 @@ class TourApiClient( .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) @@ -46,53 +49,77 @@ class TourApiClient( 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) } .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(params: LocationBasedSearchParams): TourResponse { + val url = + UriComponentsBuilder.fromUri(URI.create(apiUrl)) + .path("/locationBasedList2") + .queryParam("serviceKey", serviceKey) + .queryParam("MobileOS", "WEB") + .queryParam("MobileApp", "KoreaTravelGuide") + .queryParam("_type", "json") + .queryParam("mapX", params.mapX) + .queryParam("mapY", params.mapY) + .queryParam("radius", params.radius) + .queryParam("contentTypeId", params.contentTypeId) + .queryParam("areaCode", params.areaCode) + .queryParam("sigunguCode", params.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 { logger.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") + fun fetchTourCommonDetail(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() + + val body = + runCatching { restTemplate.getForObject(url, String::class.java) } + .onFailure { logger.error("공통정보 조회 실패", it) } + .getOrNull() - // 탐색 결과가 비어 있는 경우 - if (!itemsNode.isArray || itemsNode.isEmpty) return TourResponse(items = emptyList()) + 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 +132,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 +142,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") { + logger.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/LocationBasedSearchParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedSearchParams.kt new file mode 100644 index 0000000..b53dd2e --- /dev/null +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedSearchParams.kt @@ -0,0 +1,15 @@ +package com.back.koreaTravelGuide.domain.ai.tour.dto + +/** + * 9.29 양현준 + * 위치기반 관광정보 조회 요청 파라미터 (locationBasedList2) + * 필수 좌표 값(mapX, mapY, radius)은 NonNull로 정의해 호출 시점에 무조건 전달되도록 보장한다. + */ +data class LocationBasedSearchParams( + val mapX: Double, + val mapY: Double, + val radius: Int, + 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 index 2ae8604..3145571 100644 --- 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 @@ -2,24 +2,13 @@ package com.back.koreaTravelGuide.domain.ai.tour.dto /** * 9.27 양현준 - * API 요청 파라미터 + * 지역기반 관광정보 조회 요청 파라미터 (areaBasedList2) * 기능상, 생략 가능한 필드는 생략 (arrange : 제목 순, cat : 대,중,소 분류, crpyrhtDivCd: 저작권유형) + * 관광타입 ID(12:관광지, 38 : 쇼핑...), 지역코드(1:서울, 2:인천...), 시군구코드(110:종로구, 140:강남구...), 미 입력시 전체 조회 */ 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..9d286a3 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,6 +1,9 @@ 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.LocationBasedSearchParams +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.TourResponse import com.back.koreaTravelGuide.domain.ai.tour.dto.TourSearchParams import org.slf4j.LoggerFactory @@ -13,38 +16,19 @@ 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 } ?: "", - ) - + // 지역기반 관광정보 조회, areaBasedList2 + fun fetchTours(tourSearchParams: TourSearchParams): TourResponse { // request를 바탕으로 관광 정보 API 호출 - val tours = tourApiClient.fetchTourInfo(request) + val tours = tourApiClient.fetchTourInfo(tourSearchParams) - // 관광 정보 결과 로깅 - if (tours.items.isEmpty()) { - logger.info( - "관광 정보 없음: params={} / {} {}", - request.areaCode, - request.sigunguCode, - request.contentTypeId, - ) - } else { - logger.info("관광 정보 {}건 조회 성공", tours.items.size) - } return tours } + + fun fetchLocationBasedTours(locationBasedSearchParams: LocationBasedSearchParams): TourResponse { + return tourApiClient.fetchLocationBasedTours(locationBasedSearchParams) + } + + fun fetchTourCommonDetail(params: TourDetailParams): TourDetailResponse { + return tourApiClient.fetchTourCommonDetail(params) + } } From 08974f28cb3c6006e6c51aa9ee9c054cc65ed893 Mon Sep 17 00:00:00 2001 From: YangHJ2415 Date: Tue, 30 Sep 2025 12:43:31 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat(be):=20=EA=B3=B5=ED=86=B5=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20Dto=20=EC=B6=94=EA=B0=80=20(#4?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ai/tour/dto/TourDetailParams.kt | 9 +++++++++ .../domain/ai/tour/dto/TourDetailResponse.kt | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailParams.kt create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourDetailResponse.kt 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?, +) From d8c70e4a3fc262860566db4f671fdf0e7ce9a915 Mon Sep 17 00:00:00 2001 From: YangHJ2415 Date: Tue, 30 Sep 2025 12:50:58 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor(be):=20TourParams=EC=99=80=20TourD?= =?UTF-8?q?etailParams=EB=A1=9C=20Dto=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ai/tour/client/TourApiClient.kt | 7 ++++--- .../ai/tour/dto/{TourSearchParams.kt => TourParams.kt} | 2 +- .../koreaTravelGuide/domain/ai/tour/service/TourService.kt | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) rename src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/{TourSearchParams.kt => TourParams.kt} (95%) 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 e1e198d..d3acea4 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 @@ -5,8 +5,8 @@ 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.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 @@ -28,7 +28,7 @@ class TourApiClient( 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) @@ -43,7 +43,7 @@ class TourApiClient( .toUri() // 지역 기반 관광 정보 조회 (areaBasedList2) - fun fetchTourInfo(params: TourSearchParams): TourResponse { + fun fetchTourInfo(params: TourParams): TourResponse { logger.info("지역 기반 관광 정보 조회 시작") val url = buildUrl(params) @@ -90,6 +90,7 @@ class TourApiClient( ?: TourResponse(items = emptyList()) } + // 공통정보 조회 (detailCommon2) fun fetchTourCommonDetail(params: TourDetailParams): TourDetailResponse { val url = UriComponentsBuilder.fromUri(URI.create(apiUrl)) 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/TourParams.kt similarity index 95% rename from src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourSearchParams.kt rename to src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourParams.kt index 3145571..eb2174a 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourSearchParams.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourParams.kt @@ -7,7 +7,7 @@ package com.back.koreaTravelGuide.domain.ai.tour.dto * 관광타입 ID(12:관광지, 38 : 쇼핑...), 지역코드(1:서울, 2:인천...), 시군구코드(110:종로구, 140:강남구...), 미 입력시 전체 조회 */ -data class TourSearchParams( +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/service/TourService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/service/TourService.kt index 9d286a3..0ab0a2c 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 @@ -4,8 +4,8 @@ import com.back.koreaTravelGuide.domain.ai.tour.client.TourApiClient import com.back.koreaTravelGuide.domain.ai.tour.dto.LocationBasedSearchParams 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.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 @@ -17,9 +17,9 @@ class TourService( private val logger = LoggerFactory.getLogger(this::class.java) // 지역기반 관광정보 조회, areaBasedList2 - fun fetchTours(tourSearchParams: TourSearchParams): TourResponse { + fun fetchTours(tourParams: TourParams): TourResponse { // request를 바탕으로 관광 정보 API 호출 - val tours = tourApiClient.fetchTourInfo(tourSearchParams) + val tours = tourApiClient.fetchTourInfo(tourParams) return tours } From a8a8baffbda52098200d433a01bea3e6ec061ffb Mon Sep 17 00:00:00 2001 From: YangHJ2415 Date: Tue, 30 Sep 2025 12:54:45 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor(be):=20=EB=A7=88=EC=B0=AC=EA=B0=80?= =?UTF-8?q?=EC=A7=80=EB=A1=9C=20LocationBasedParams=EB=A1=9C=20Dto=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt | 4 ++-- ...{LocationBasedSearchParams.kt => LocationBasedParams.kt} | 2 +- .../koreaTravelGuide/domain/ai/tour/service/TourService.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/{LocationBasedSearchParams.kt => LocationBasedParams.kt} (92%) 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 d3acea4..8d0ad22 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,6 +1,6 @@ package com.back.koreaTravelGuide.domain.ai.tour.client -import com.back.koreaTravelGuide.domain.ai.tour.dto.LocationBasedSearchParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.LocationBasedParams 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 @@ -61,7 +61,7 @@ class TourApiClient( } // 위치기반 관광정보 조회 (locationBasedList2) - fun fetchLocationBasedTours(params: LocationBasedSearchParams): TourResponse { + fun fetchLocationBasedTours(params: LocationBasedParams): TourResponse { val url = UriComponentsBuilder.fromUri(URI.create(apiUrl)) .path("/locationBasedList2") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedSearchParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedParams.kt similarity index 92% rename from src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedSearchParams.kt rename to src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedParams.kt index b53dd2e..aa4ea9a 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedSearchParams.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedParams.kt @@ -5,7 +5,7 @@ package com.back.koreaTravelGuide.domain.ai.tour.dto * 위치기반 관광정보 조회 요청 파라미터 (locationBasedList2) * 필수 좌표 값(mapX, mapY, radius)은 NonNull로 정의해 호출 시점에 무조건 전달되도록 보장한다. */ -data class LocationBasedSearchParams( +data class LocationBasedParams( val mapX: Double, val mapY: Double, val radius: Int, 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 0ab0a2c..595308b 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,7 +1,7 @@ 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.LocationBasedSearchParams +import com.back.koreaTravelGuide.domain.ai.tour.dto.LocationBasedParams 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.TourParams @@ -24,8 +24,8 @@ class TourService( return tours } - fun fetchLocationBasedTours(locationBasedSearchParams: LocationBasedSearchParams): TourResponse { - return tourApiClient.fetchLocationBasedTours(locationBasedSearchParams) + fun fetchLocationBasedTours(locationBasedParams: LocationBasedParams): TourResponse { + return tourApiClient.fetchLocationBasedTours(locationBasedParams) } fun fetchTourCommonDetail(params: TourDetailParams): TourDetailResponse { From d9a25d75e7888779ec77ca217d72e44cec31d68d Mon Sep 17 00:00:00 2001 From: YangHJ2415 Date: Tue, 30 Sep 2025 15:00:02 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat(be):=20yml,=20build.gradle=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EC=88=98(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 36 ++++++++++++++++++++++++++ src/main/resources/area-codes.yml | 13 ++++++++++ src/main/resources/content-type-id.yml | 11 ++++++++ src/main/resources/sigungu-codes.yml | 13 ++++++++++ 4 files changed, 73 insertions(+) create mode 100644 src/main/resources/area-codes.yml create mode 100644 src/main/resources/content-type-id.yml create mode 100644 src/main/resources/sigungu-codes.yml diff --git a/build.gradle.kts b/build.gradle.kts index 90a56bc..0c0843f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -117,6 +117,39 @@ 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 sigunguCodes = + file("src/main/resources/sigungu-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 +173,9 @@ buildConfig { .substringAfter("ai-fallback: \"") .substringBefore("\"") + buildConfigField("String", "AREA_CODES_DESCRIPTION", "\"\"\"$areaCodes\"\"\"") + buildConfigField("String", "SIGUNGU_CODES_DESCRIPTION", "\"\"\"$sigunguCodes\"\"\"") + 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/resources/area-codes.yml b/src/main/resources/area-codes.yml new file mode 100644 index 0000000..455939e --- /dev/null +++ b/src/main/resources/area-codes.yml @@ -0,0 +1,13 @@ +tour: + area: + codes: + 서울: "1" + 인천: "2" + 대전: "3" + 대구: "4" + 광주: "5" + 부산: "6" + 울산: "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 diff --git a/src/main/resources/sigungu-codes.yml b/src/main/resources/sigungu-codes.yml new file mode 100644 index 0000000..7265edc --- /dev/null +++ b/src/main/resources/sigungu-codes.yml @@ -0,0 +1,13 @@ +tour: + sigungu: + codes: + 강남구: "1" + 강동구: "2" + 강북구: "3" + 강서구: "4" + 관악구: "5" + 광진구: "6" + 구로구: "7" + 금천구: "8" + 노원구: "9" + 도봉구: "10" From 106f74cbc3e4ccf2c113fa603dd48e0e4d806f14 Mon Sep 17 00:00:00 2001 From: YangHJ2415 Date: Tue, 30 Sep 2025 19:00:06 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat(be):=20yml,=20gradle=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20tool=20=EC=97=B0=EA=B3=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20client,=20service=20=EC=88=98=EC=A0=95=20(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 12 -- .../domain/ai/aiChat/tool/TourToolExample.kt | 88 +++++++++++ .../domain/ai/tour/client/TourApiClient.kt | 38 +++-- ...edParams.kt => TourLocationBasedParams.kt} | 11 +- .../domain/ai/tour/service/TourService.kt | 146 +++++++++++++++++- src/main/resources/area-codes.yml | 43 +++++- src/main/resources/sigungu-codes.yml | 13 -- 7 files changed, 290 insertions(+), 61 deletions(-) create mode 100644 src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourToolExample.kt rename src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/{LocationBasedParams.kt => TourLocationBasedParams.kt} (57%) delete mode 100644 src/main/resources/sigungu-codes.yml diff --git a/build.gradle.kts b/build.gradle.kts index 0c0843f..6554833 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -128,17 +128,6 @@ buildConfig { "${parts[0].trim()}: ${parts[1].trim().replace("\"", "")}" } - val sigunguCodes = - file("src/main/resources/sigungu-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() @@ -174,7 +163,6 @@ buildConfig { .substringBefore("\"") buildConfigField("String", "AREA_CODES_DESCRIPTION", "\"\"\"$areaCodes\"\"\"") - buildConfigField("String", "SIGUNGU_CODES_DESCRIPTION", "\"\"\"$sigunguCodes\"\"\"") buildConfigField("String", "CONTENT_TYPE_CODES_DESCRIPTION", "\"\"\"$contentTypeCodes\"\"\"") 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/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 8d0ad22..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,18 +1,19 @@ package com.back.koreaTravelGuide.domain.ai.tour.client -import com.back.koreaTravelGuide.domain.ai.tour.dto.LocationBasedParams +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.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 @@ -24,9 +25,6 @@ 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: TourParams): URI = UriComponentsBuilder.fromUri(URI.create(apiUrl)) @@ -44,14 +42,11 @@ class TourApiClient( // 지역 기반 관광 정보 조회 (areaBasedList2) fun fetchTourInfo(params: TourParams): TourResponse { - logger.info("지역 기반 관광 정보 조회 시작") - val url = buildUrl(params) - logger.info("Tour API URL 생성 : $url") val body = runCatching { restTemplate.getForObject(url, String::class.java) } - .onFailure { logger.error("관광 정보 조회 실패", it) } + .onFailure { log.error("관광 정보 조회 실패", it) } .getOrNull() return body @@ -61,7 +56,10 @@ class TourApiClient( } // 위치기반 관광정보 조회 (locationBasedList2) - fun fetchLocationBasedTours(params: LocationBasedParams): TourResponse { + fun fetchLocationBasedTours( + tourParams: TourParams, + locationParams: TourLocationBasedParams, + ): TourResponse { val url = UriComponentsBuilder.fromUri(URI.create(apiUrl)) .path("/locationBasedList2") @@ -69,19 +67,19 @@ class TourApiClient( .queryParam("MobileOS", "WEB") .queryParam("MobileApp", "KoreaTravelGuide") .queryParam("_type", "json") - .queryParam("mapX", params.mapX) - .queryParam("mapY", params.mapY) - .queryParam("radius", params.radius) - .queryParam("contentTypeId", params.contentTypeId) - .queryParam("areaCode", params.areaCode) - .queryParam("sigunguCode", params.sigunguCode) + .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() val body = runCatching { restTemplate.getForObject(url, String::class.java) } - .onFailure { logger.error("위치기반 관광 정보 조회 실패", it) } + .onFailure { log.error("위치기반 관광 정보 조회 실패", it) } .getOrNull() return body @@ -91,7 +89,7 @@ class TourApiClient( } // 공통정보 조회 (detailCommon2) - fun fetchTourCommonDetail(params: TourDetailParams): TourDetailResponse { + fun fetchTourDetail(params: TourDetailParams): TourDetailResponse { val url = UriComponentsBuilder.fromUri(URI.create(apiUrl)) .path("/detailCommon2") @@ -106,7 +104,7 @@ class TourApiClient( val body = runCatching { restTemplate.getForObject(url, String::class.java) } - .onFailure { logger.error("공통정보 조회 실패", it) } + .onFailure { log.error("공통정보 조회 실패", it) } .getOrNull() return body @@ -179,7 +177,7 @@ class TourApiClient( .asText() if (resultCode != "0000") { - logger.warn("{} API resultCode={}", apiName, resultCode) + log.warn("{} API resultCode={}", apiName, resultCode) return emptyList() } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedParams.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourLocationBasedParams.kt similarity index 57% rename from src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedParams.kt rename to src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourLocationBasedParams.kt index aa4ea9a..405a846 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/LocationBasedParams.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourLocationBasedParams.kt @@ -5,11 +5,8 @@ package com.back.koreaTravelGuide.domain.ai.tour.dto * 위치기반 관광정보 조회 요청 파라미터 (locationBasedList2) * 필수 좌표 값(mapX, mapY, radius)은 NonNull로 정의해 호출 시점에 무조건 전달되도록 보장한다. */ -data class LocationBasedParams( - val mapX: Double, - val mapY: Double, - val radius: Int, - val contentTypeId: String? = null, - val areaCode: String? = null, - val sigunguCode: String? = null, +data class TourLocationBasedParams( + val mapX: String, + val mapY: String, + val radius: String, ) 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 595308b..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,9 +1,11 @@ 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.LocationBasedParams +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 org.slf4j.LoggerFactory @@ -16,19 +18,149 @@ class TourService( ) { private val logger = LoggerFactory.getLogger(this::class.java) - // 지역기반 관광정보 조회, areaBasedList2 + // 파라미터를 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 { - // request를 바탕으로 관광 정보 API 호출 + // 09.30 테스트용 하드코딩 + if ( + tourParams.contentTypeId == "12" && + tourParams.areaCode == "6" && + tourParams.sigunguCode == "10" + ) { + return PRESET_AREA_TOUR_RESPONSE + } + val tours = tourApiClient.fetchTourInfo(tourParams) return tours } - fun fetchLocationBasedTours(locationBasedParams: LocationBasedParams): TourResponse { - return tourApiClient.fetchLocationBasedTours(locationBasedParams) + // 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) } - fun fetchTourCommonDetail(params: TourDetailParams): TourDetailResponse { - return tourApiClient.fetchTourCommonDetail(params) + // 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 index 455939e..9d80a86 100644 --- a/src/main/resources/area-codes.yml +++ b/src/main/resources/area-codes.yml @@ -1,12 +1,51 @@ tour: area: codes: - 서울: "1" + 서울-강남구: "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" + 부산-강서구: "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" diff --git a/src/main/resources/sigungu-codes.yml b/src/main/resources/sigungu-codes.yml deleted file mode 100644 index 7265edc..0000000 --- a/src/main/resources/sigungu-codes.yml +++ /dev/null @@ -1,13 +0,0 @@ -tour: - sigungu: - codes: - 강남구: "1" - 강동구: "2" - 강북구: "3" - 강서구: "4" - 관악구: "5" - 광진구: "6" - 구로구: "7" - 금천구: "8" - 노원구: "9" - 도봉구: "10"