11package com.back.koreaTravelGuide.domain.ai.tour.client
22
3+ import com.back.koreaTravelGuide.common.logging.log
4+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailItem
5+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams
6+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse
37import com.back.koreaTravelGuide.domain.ai.tour.dto.TourItem
8+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams
9+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourParams
410import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
5- import com.back.koreaTravelGuide.domain.ai.tour.dto.TourSearchParams
11+ import com.fasterxml.jackson.databind.JsonNode
612import com.fasterxml.jackson.databind.ObjectMapper
7- import org.slf4j.LoggerFactory
813import org.springframework.beans.factory.annotation.Value
914import org.springframework.stereotype.Component
1015import org.springframework.web.client.RestTemplate
16+ import org.springframework.web.reactive.function.server.RequestPredicates.queryParam
1117import org.springframework.web.util.UriComponentsBuilder
1218import java.net.URI
1319
@@ -19,19 +25,14 @@ class TourApiClient(
1925 @Value(" \$ {tour.api.key}" ) private val serviceKey : String ,
2026 @Value(" \$ {tour.api.base-url}" ) private val apiUrl : String ,
2127) {
22- // println 대신 SLF4J 로거 사용
23- private val logger = LoggerFactory .getLogger(TourApiClient ::class .java)
24-
2528 // 요청 URL 구성
26- private fun buildUrl (params : TourSearchParams ): URI =
29+ private fun buildUrl (params : TourParams ): URI =
2730 UriComponentsBuilder .fromUri(URI .create(apiUrl))
2831 .path(" /areaBasedList2" )
2932 .queryParam(" serviceKey" , serviceKey)
3033 .queryParam(" MobileOS" , " WEB" )
3134 .queryParam(" MobileApp" , " KoreaTravelGuide" )
3235 .queryParam(" _type" , " json" )
33- .queryParam(" numOfRows" , params.numOfRows)
34- .queryParam(" pageNo" , params.pageNo)
3536 .queryParam(" contentTypeId" , params.contentTypeId)
3637 .queryParam(" areaCode" , params.areaCode)
3738 .queryParam(" sigunguCode" , params.sigunguCode)
@@ -40,59 +41,84 @@ class TourApiClient(
4041 .toUri()
4142
4243 // 지역 기반 관광 정보 조회 (areaBasedList2)
43- fun fetchTourInfo (params : TourSearchParams ): TourResponse {
44- logger.info(" 지역 기반 관광 정보 조회 시작" )
45-
44+ fun fetchTourInfo (params : TourParams ): TourResponse {
4645 val url = buildUrl(params)
47- logger.info(" Tour API URL 생성 : $url " )
48-
49- /*
50- * runCatching: 예외를 Result로 감싸 예외를 던지지 않고 처리하는 유틸리티 함수
51- * getOrNull(): 성공 시 응답 문자열을, 실패 시 null 반환
52- * takeUnless { it.isNullOrBlank() }: 공백 응답을 걸러냄
53- * ?.let { parseItems(it) } ?: emptyList(): 유효한 응답은 파싱, 아니면 빈 리스트 반환
54- */
55- val response =
46+
47+ val body =
5648 runCatching { restTemplate.getForObject(url, String ::class .java) }
57- .onFailure { logger .error(" 관광 정보 조회 실패" , it) }
49+ .onFailure { log .error(" 관광 정보 조회 실패" , it) }
5850 .getOrNull()
59- .takeUnless { it.isNullOrBlank() }
60- ?.let { parseItems(it) }
6151
62- return response ? : TourResponse (items = emptyList())
52+ return body
53+ .takeUnless { it.isNullOrBlank() }
54+ ?.let { parseItems(it) }
55+ ? : TourResponse (items = emptyList())
6356 }
6457
65- private fun parseItems (json : String ): TourResponse {
66- val root = objectMapper.readTree(json)
58+ // 위치기반 관광정보 조회 (locationBasedList2)
59+ fun fetchLocationBasedTours (
60+ tourParams : TourParams ,
61+ locationParams : TourLocationBasedParams ,
62+ ): TourResponse {
63+ val url =
64+ UriComponentsBuilder .fromUri(URI .create(apiUrl))
65+ .path(" /locationBasedList2" )
66+ .queryParam(" serviceKey" , serviceKey)
67+ .queryParam(" MobileOS" , " WEB" )
68+ .queryParam(" MobileApp" , " KoreaTravelGuide" )
69+ .queryParam(" _type" , " json" )
70+ .queryParam(" mapX" , locationParams.mapX)
71+ .queryParam(" mapY" , locationParams.mapY)
72+ .queryParam(" radius" , locationParams.radius)
73+ .queryParam(" contentTypeId" , tourParams.contentTypeId)
74+ .queryParam(" areaCode" , tourParams.areaCode)
75+ .queryParam(" sigunguCode" , tourParams.sigunguCode)
76+ .build()
77+ .encode()
78+ .toUri()
6779
68- // header.resultCode 값 추출위한 노스 탐색 과정
69- val resultCode =
70- root
71- .path(" response" )
72- .path(" header" )
73- .path(" resultCode" )
74- .asText()
80+ val body =
81+ runCatching { restTemplate.getForObject(url, String ::class .java) }
82+ .onFailure { log.error(" 위치기반 관광 정보 조회 실패" , it) }
83+ .getOrNull()
7584
76- // resultCode가 "0000"이 아닌 경우 체크
77- if (resultCode != " 0000 " ) {
78- logger.warn( " 관광 정보 API resultCode={} " , resultCode)
79- return TourResponse (items = emptyList())
80- }
85+ return body
86+ . takeUnless { it.isNullOrBlank() }
87+ ?. let { parseItems(it) }
88+ ? : TourResponse (items = emptyList())
89+ }
8190
82- // path("키") 형태로 노드를 탐색, 응답 Json 형태의 순서에 따라 순차적으로 내려감
83- val itemsNode =
84- root
85- .path(" response" )
86- .path(" body" )
87- .path(" items" )
88- .path(" item" )
91+ // 공통정보 조회 (detailCommon2)
92+ fun fetchTourDetail (params : TourDetailParams ): TourDetailResponse {
93+ val url =
94+ UriComponentsBuilder .fromUri(URI .create(apiUrl))
95+ .path(" /detailCommon2" )
96+ .queryParam(" serviceKey" , serviceKey)
97+ .queryParam(" MobileOS" , " WEB" )
98+ .queryParam(" MobileApp" , " KoreaTravelGuide" )
99+ .queryParam(" _type" , " json" )
100+ .queryParam(" contentId" , params.contentId)
101+ .build()
102+ .encode()
103+ .toUri()
89104
90- // 탐색 결과가 비어 있는 경우
91- if (! itemsNode.isArray || itemsNode.isEmpty) return TourResponse (items = emptyList())
105+ val body =
106+ runCatching { restTemplate.getForObject(url, String ::class .java) }
107+ .onFailure { log.error(" 공통정보 조회 실패" , it) }
108+ .getOrNull()
109+
110+ return body
111+ .takeUnless { it.isNullOrBlank() }
112+ ?.let { parseDetailItems(it) }
113+ ? : TourDetailResponse (items = emptyList())
114+ }
115+
116+ private fun parseItems (json : String ): TourResponse {
117+ val itemNodes = extractItemNodes(json, " 관광 정보" )
118+ if (itemNodes.isEmpty()) return TourResponse (items = emptyList())
92119
93- // itemsNode가 배열이므로 map으로 각 노드를 TourItem으로 변환 후 컨테이너로 감싼다.
94120 val items =
95- itemsNode .map { node ->
121+ itemNodes .map { node ->
96122 TourItem (
97123 contentId = node.path(" contentid" ).asText(),
98124 contentTypeId = node.path(" contenttypeid" ).asText(),
@@ -105,6 +131,7 @@ class TourApiClient(
105131 firstimage2 = node.path(" firstimage2" ).takeIf { it.isTextual }?.asText(),
106132 mapX = node.path(" mapx" ).takeIf { it.isTextual }?.asText(),
107133 mapY = node.path(" mapy" ).takeIf { it.isTextual }?.asText(),
134+ distance = node.path(" dist" ).takeIf { it.isTextual }?.asText(),
108135 mlevel = node.path(" mlevel" ).takeIf { it.isTextual }?.asText(),
109136 sigunguCode = node.path(" sigungucode" ).takeIf { it.isTextual }?.asText(),
110137 lDongRegnCd = node.path(" lDongRegnCd" ).takeIf { it.isTextual }?.asText(),
@@ -114,4 +141,55 @@ class TourApiClient(
114141
115142 return TourResponse (items = items)
116143 }
144+
145+ private fun parseDetailItems (json : String ): TourDetailResponse {
146+ val itemNodes = extractItemNodes(json, " 공통정보" )
147+ if (itemNodes.isEmpty()) return TourDetailResponse (items = emptyList())
148+
149+ val items =
150+ itemNodes.map { node ->
151+ TourDetailItem (
152+ contentId = node.path(" contentid" ).asText(),
153+ title = node.path(" title" ).asText(),
154+ overview = node.path(" overview" ).takeIf { it.isTextual }?.asText(),
155+ addr1 = node.path(" addr1" ).takeIf { it.isTextual }?.asText(),
156+ mapX = node.path(" mapx" ).takeIf { it.isTextual }?.asText(),
157+ mapY = node.path(" mapy" ).takeIf { it.isTextual }?.asText(),
158+ firstImage = node.path(" firstimage" ).takeIf { it.isTextual }?.asText(),
159+ tel = node.path(" tel" ).takeIf { it.isTextual }?.asText(),
160+ homepage = node.path(" homepage" ).takeIf { it.isTextual }?.asText(),
161+ )
162+ }
163+
164+ return TourDetailResponse (items = items)
165+ }
166+
167+ private fun extractItemNodes (
168+ json : String ,
169+ apiName : String ,
170+ ): List <JsonNode > {
171+ val root = objectMapper.readTree(json)
172+ val resultCode =
173+ root
174+ .path(" response" )
175+ .path(" header" )
176+ .path(" resultCode" )
177+ .asText()
178+
179+ if (resultCode != " 0000" ) {
180+ log.warn(" {} API resultCode={}" , apiName, resultCode)
181+ return emptyList()
182+ }
183+
184+ val itemsNode =
185+ root
186+ .path(" response" )
187+ .path(" body" )
188+ .path(" items" )
189+ .path(" item" )
190+
191+ if (! itemsNode.isArray || itemsNode.isEmpty) return emptyList()
192+
193+ return itemsNode.map { it }
194+ }
117195}
0 commit comments