@@ -3,20 +3,24 @@ package com.back.koreaTravelGuide.domain.ai.tour.client
33import com.back.koreaTravelGuide.domain.ai.tour.dto.InternalData
44import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
55import com.fasterxml.jackson.databind.ObjectMapper
6+ import org.slf4j.LoggerFactory
67import org.springframework.beans.factory.annotation.Value
78import org.springframework.stereotype.Component
89import org.springframework.web.client.RestTemplate
910import org.springframework.web.util.UriComponentsBuilder
1011import java.net.URI
1112
12- // 09.25 양현준
13+ // 09.26 양현준
1314@Component
1415class TourApiClient (
1516 private val restTemplate : RestTemplate ,
1617 private val objectMapper : ObjectMapper ,
1718 @Value(" \$ {tour.api.key}" ) private val serviceKey : String ,
1819 @Value(" \$ {tour.api.base-url}" ) private val apiUrl : String ,
1920) {
21+ // println 대신 SLF4J 로거 사용
22+ private val logger = LoggerFactory .getLogger(TourApiClient ::class .java)
23+
2024 // 요청 URL 구성
2125 private fun buildUrl (params : InternalData ): URI =
2226 UriComponentsBuilder .fromUri(URI .create(apiUrl))
@@ -35,49 +39,72 @@ class TourApiClient(
3539 .toUri()
3640
3741 // 지역 기반 관광 정보 조회 (areaBasedList2)
38- fun fetchTourInfo (params : InternalData ): TourResponse ? {
39- println (" URL 생성" )
42+ fun fetchTourInfo (params : InternalData ): List <TourResponse > {
43+ logger.info(" 지역 기반 관광 정보 조회 시작" )
44+
4045 val url = buildUrl(params)
46+ logger.info(" Tour API URL 생성 : $url " )
4147
42- println (" 관광 정보 조회 API 호출: $url " )
48+ /*
49+ * runCatching: 예외를 Result로 감싸 예외를 던지지 않고 처리하는 유틸리티 함수
50+ * getOrNull(): 성공 시 응답 문자열을, 실패 시 null 반환
51+ * takeUnless { it.isNullOrBlank() }: 공백 응답을 걸러냄
52+ * ?.let { parseItems(it) } ?: emptyList(): 유효한 응답은 파싱, 아니면 빈 리스트 반환
53+ */
54+ return runCatching { restTemplate.getForObject(url, String ::class .java) }
55+ .onFailure { logger.error(" 관광 정보 조회 실패" , it) }
56+ .getOrNull()
57+ .takeUnless { it.isNullOrBlank() }
58+ ?.let { parseItems(it) } ? : emptyList()
59+ }
4360
44- return try {
45- val jsonResponse = restTemplate.getForObject(url, String ::class .java)
46- println (" 관광 정보 응답 길이: ${jsonResponse?.length ? : 0 } " )
61+ private fun parseItems (json : String ): List <TourResponse > {
62+ val root = objectMapper.readTree(json)
4763
48- if (jsonResponse.isNullOrBlank()) return null // HTTP 호출 결과가 null이거나 공백 문자열일 때
64+ // header.resultCode 값 추출위한 노스 탐색 과정
65+ val resultCode =
66+ root
67+ .path(" response" )
68+ .path(" header" )
69+ .path(" resultCode" )
70+ .asText()
71+
72+ // resultCode가 "0000"이 아닌 경우 체크
73+ if (resultCode != " 0000" ) {
74+ logger.warn(" 관광 정보 API resultCode={}" , resultCode)
75+ return emptyList()
76+ }
4977
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" )
78+ // path("키") 형태로 노드를 탐색, 응답 Json 형태의 순서에 따라 순차적으로 내려감
79+ val itemsNode =
80+ root
81+ .path(" response" )
82+ .path(" body" )
83+ .path(" items" )
84+ .path(" item" )
5785
58- if (! itemsNode.isArray || itemsNode.isEmpty) return null // 탐색 결과가 비어 있는 경우
86+ // 탐색 결과가 비어 있는 경우
87+ if (! itemsNode.isArray || itemsNode.isEmpty) return emptyList()
5988
60- val firstItem = itemsNode.first()
89+ // itemsNode가 배열이므로 map으로 각 노드를 TourResponse로 변환
90+ return itemsNode.map { node ->
6191 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(),
92+ contentId = node .path(" contentid" ).asText(),
93+ contentTypeId = node .path(" contenttypeid" ).asText(),
94+ createdTime = node .path(" createdtime" ).asText(),
95+ modifiedTime = node .path(" modifiedtime" ).asText(),
96+ title = node .path(" title" ).asText(),
97+ addr1 = node .path(" addr1" ).takeIf { it.isTextual }?.asText(),
98+ areaCode = node .path(" areacode" ).takeIf { it.isTextual }?.asText(),
99+ firstimage = node .path(" firstimage" ).takeIf { it.isTextual }?.asText(),
100+ firstimage2 = node .path(" firstimage2" ).takeIf { it.isTextual }?.asText(),
101+ mapX = node .path(" mapx" ).takeIf { it.isTextual }?.asText(),
102+ mapY = node .path(" mapy" ).takeIf { it.isTextual }?.asText(),
103+ mlevel = node .path(" mlevel" ).takeIf { it.isTextual }?.asText(),
104+ sigunguCode = node .path(" sigungucode" ).takeIf { it.isTextual }?.asText(),
105+ lDongRegnCd = node .path(" lDongRegnCd" ).takeIf { it.isTextual }?.asText(),
106+ lDongSignguCd = node .path(" lDongSignguCd" ).takeIf { it.isTextual }?.asText(),
77107 )
78- } catch (e: Exception ) {
79- println (" 관광 정보 조회 오류: ${e.message} " )
80- null
81108 }
82109 }
83110}
0 commit comments