Skip to content

Commit 10e7f8f

Browse files
committed
fix(be): TourApiClient 수정, List형태로 결과를 반환
1 parent ed7a5cf commit 10e7f8f

File tree

1 file changed

+62
-35
lines changed

1 file changed

+62
-35
lines changed

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/client/TourApiClient.kt

Lines changed: 62 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ package com.back.koreaTravelGuide.domain.ai.tour.client
33
import com.back.koreaTravelGuide.domain.ai.tour.dto.InternalData
44
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse
55
import com.fasterxml.jackson.databind.ObjectMapper
6+
import org.slf4j.LoggerFactory
67
import org.springframework.beans.factory.annotation.Value
78
import org.springframework.stereotype.Component
89
import org.springframework.web.client.RestTemplate
910
import org.springframework.web.util.UriComponentsBuilder
1011
import java.net.URI
1112

12-
// 09.25 양현준
13+
// 09.26 양현준
1314
@Component
1415
class 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

Comments
 (0)