11package com.back.koreaTravelGuide.domain.ai.tour.client
22
3- import com.back.koreaTravelGuide.domain.ai.tour.dto.InternalData
3+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourItem
44import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
5+ import com.back.koreaTravelGuide.domain.ai.tour.dto.TourSearchParams
56import com.fasterxml.jackson.databind.ObjectMapper
7+ import org.slf4j.LoggerFactory
68import org.springframework.beans.factory.annotation.Value
79import org.springframework.stereotype.Component
810import org.springframework.web.client.RestTemplate
911import org.springframework.web.util.UriComponentsBuilder
1012import java.net.URI
1113
12- // 09.25 양현준
14+ // 09.26 양현준
1315@Component
1416class TourApiClient (
1517 private val restTemplate : RestTemplate ,
1618 private val objectMapper : ObjectMapper ,
1719 @Value(" \$ {tour.api.key}" ) private val serviceKey : String ,
1820 @Value(" \$ {tour.api.base-url}" ) private val apiUrl : String ,
1921) {
22+ // println 대신 SLF4J 로거 사용
23+ private val logger = LoggerFactory .getLogger(TourApiClient ::class .java)
24+
2025 // 요청 URL 구성
21- private fun buildUrl (params : InternalData ): URI =
26+ private fun buildUrl (params : TourSearchParams ): URI =
2227 UriComponentsBuilder .fromUri(URI .create(apiUrl))
2328 .path(" /areaBasedList2" )
2429 .queryParam(" serviceKey" , serviceKey)
@@ -35,49 +40,78 @@ class TourApiClient(
3540 .toUri()
3641
3742 // 지역 기반 관광 정보 조회 (areaBasedList2)
38- fun fetchTourInfo (params : InternalData ): TourResponse ? {
39- println (" URL 생성" )
40- val url = buildUrl(params)
43+ fun fetchTourInfo (params : TourSearchParams ): TourResponse {
44+ logger.info(" 지역 기반 관광 정보 조회 시작" )
4145
42- println (" 관광 정보 조회 API 호출: $url " )
46+ val url = buildUrl(params)
47+ logger.info(" Tour API URL 생성 : $url " )
4348
44- return try {
45- val jsonResponse = restTemplate.getForObject(url, String ::class .java)
46- println (" 관광 정보 응답 길이: ${jsonResponse?.length ? : 0 } " )
49+ /*
50+ * runCatching: 예외를 Result로 감싸 예외를 던지지 않고 처리하는 유틸리티 함수
51+ * getOrNull(): 성공 시 응답 문자열을, 실패 시 null 반환
52+ * takeUnless { it.isNullOrBlank() }: 공백 응답을 걸러냄
53+ * ?.let { parseItems(it) } ?: emptyList(): 유효한 응답은 파싱, 아니면 빈 리스트 반환
54+ */
55+ val response =
56+ runCatching { restTemplate.getForObject(url, String ::class .java) }
57+ .onFailure { logger.error(" 관광 정보 조회 실패" , it) }
58+ .getOrNull()
59+ .takeUnless { it.isNullOrBlank() }
60+ ?.let { parseItems(it) }
4761
48- if (jsonResponse.isNullOrBlank()) return null // HTTP 호출 결과가 null이거나 공백 문자열일 때
62+ return response ? : TourResponse (items = emptyList())
63+ }
4964
50- val root = objectMapper.readTree(jsonResponse) // 문자열을 Jackson 트리 구조(JsonNode)로 변환
51- val itemsNode =
52- root // path("키") 형태로 노드를 탐색, 응답 Json 형태의 순서에 따라 순차적으로 내려감
53- .path(" response" )
54- .path(" body" )
55- .path(" items" )
56- .path(" item" )
65+ private fun parseItems (json : String ): TourResponse {
66+ val root = objectMapper.readTree(json)
5767
58- if (! itemsNode.isArray || itemsNode.isEmpty) return null // 탐색 결과가 비어 있는 경우
68+ // header.resultCode 값 추출위한 노스 탐색 과정
69+ val resultCode =
70+ root
71+ .path(" response" )
72+ .path(" header" )
73+ .path(" resultCode" )
74+ .asText()
5975
60- val firstItem = itemsNode.first()
61- TourResponse (
62- contentId = firstItem.path(" contentid" ).asText(),
63- contentTypeId = firstItem.path(" contenttypeid" ).asText(),
64- createdTime = firstItem.path(" createdtime" ).asText(),
65- modifiedTime = firstItem.path(" modifiedtime" ).asText(),
66- title = firstItem.path(" title" ).asText(),
67- addr1 = firstItem.path(" addr1" ).takeIf { it.isTextual }?.asText(),
68- areaCode = firstItem.path(" areacode" ).takeIf { it.isTextual }?.asText(),
69- firstimage = firstItem.path(" firstimage" ).takeIf { it.isTextual }?.asText(),
70- firstimage2 = firstItem.path(" firstimage2" ).takeIf { it.isTextual }?.asText(),
71- mapX = firstItem.path(" mapx" ).takeIf { it.isTextual }?.asText(),
72- mapY = firstItem.path(" mapy" ).takeIf { it.isTextual }?.asText(),
73- mlevel = firstItem.path(" mlevel" ).takeIf { it.isTextual }?.asText(),
74- sigunguCode = firstItem.path(" sigungucode" ).takeIf { it.isTextual }?.asText(),
75- lDongRegnCd = firstItem.path(" lDongRegnCd" ).takeIf { it.isTextual }?.asText(),
76- lDongSignguCd = firstItem.path(" lDongSignguCd" ).takeIf { it.isTextual }?.asText(),
77- )
78- } catch (e: Exception ) {
79- println (" 관광 정보 조회 오류: ${e.message} " )
80- null
76+ // resultCode가 "0000"이 아닌 경우 체크
77+ if (resultCode != " 0000" ) {
78+ logger.warn(" 관광 정보 API resultCode={}" , resultCode)
79+ return TourResponse (items = emptyList())
8180 }
81+
82+ // path("키") 형태로 노드를 탐색, 응답 Json 형태의 순서에 따라 순차적으로 내려감
83+ val itemsNode =
84+ root
85+ .path(" response" )
86+ .path(" body" )
87+ .path(" items" )
88+ .path(" item" )
89+
90+ // 탐색 결과가 비어 있는 경우
91+ if (! itemsNode.isArray || itemsNode.isEmpty) return TourResponse (items = emptyList())
92+
93+ // itemsNode가 배열이므로 map으로 각 노드를 TourItem으로 변환 후 컨테이너로 감싼다.
94+ val items =
95+ itemsNode.map { node ->
96+ TourItem (
97+ contentId = node.path(" contentid" ).asText(),
98+ contentTypeId = node.path(" contenttypeid" ).asText(),
99+ createdTime = node.path(" createdtime" ).asText(),
100+ modifiedTime = node.path(" modifiedtime" ).asText(),
101+ title = node.path(" title" ).asText(),
102+ addr1 = node.path(" addr1" ).takeIf { it.isTextual }?.asText(),
103+ areaCode = node.path(" areacode" ).takeIf { it.isTextual }?.asText(),
104+ firstimage = node.path(" firstimage" ).takeIf { it.isTextual }?.asText(),
105+ firstimage2 = node.path(" firstimage2" ).takeIf { it.isTextual }?.asText(),
106+ mapX = node.path(" mapx" ).takeIf { it.isTextual }?.asText(),
107+ mapY = node.path(" mapy" ).takeIf { it.isTextual }?.asText(),
108+ mlevel = node.path(" mlevel" ).takeIf { it.isTextual }?.asText(),
109+ sigunguCode = node.path(" sigungucode" ).takeIf { it.isTextual }?.asText(),
110+ lDongRegnCd = node.path(" lDongRegnCd" ).takeIf { it.isTextual }?.asText(),
111+ lDongSignguCd = node.path(" lDongSignguCd" ).takeIf { it.isTextual }?.asText(),
112+ )
113+ }
114+
115+ return TourResponse (items = items)
82116 }
83117}
0 commit comments