Skip to content

Commit fdcf151

Browse files
committed
기상청 API JSON 응답 처리 및 실제 API 테스트 추가
- WeatherApiClient: XML에서 JSON 응답 처리로 전환 - GlobalExceptionHandler: NoResourceFoundException 핸들러 추가 - WeatherApiRealTest: 실제 기상청 API 호출 테스트 코드 추가 - application-test.properties: 테스트 환경 설정 추가
1 parent 5ea26d6 commit fdcf151

File tree

4 files changed

+350
-45
lines changed

4 files changed

+350
-45
lines changed

src/main/kotlin/com/back/koreaTravelGuide/common/exception/GlobalExceptionHandler.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import org.springframework.validation.FieldError
88
import org.springframework.web.bind.MethodArgumentNotValidException
99
import org.springframework.web.bind.annotation.ControllerAdvice
1010
import org.springframework.web.bind.annotation.ExceptionHandler
11+
import org.springframework.web.servlet.resource.NoResourceFoundException
1112

1213
/**
1314
* 전역 예외 처리기
@@ -78,6 +79,22 @@ class GlobalExceptionHandler {
7879
.body(ApiResponse(ex.message ?: "요청한 데이터를 찾을 수 없습니다"))
7980
}
8081

82+
/**
83+
* 리소스 없음 처리 (404 Not Found) - favicon.ico 등
84+
*/
85+
@ExceptionHandler(NoResourceFoundException::class)
86+
fun handleNoResourceFound(ex: NoResourceFoundException): ResponseEntity<ApiResponse<Void>> {
87+
// favicon.ico 요청은 조용히 무시 (로그 안 남김)
88+
if (ex.message?.contains("favicon.ico") == true) {
89+
return ResponseEntity.status(HttpStatus.NOT_FOUND).build()
90+
}
91+
92+
// 다른 리소스는 로그 남기고 처리
93+
println("⚠️ 리소스를 찾을 수 없음: ${ex.message}")
94+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
95+
.body(ApiResponse("요청한 리소스를 찾을 수 없습니다"))
96+
}
97+
8198
/**
8299
* 모든 예외 처리 (500 Internal Server Error)
83100
* 위에서 처리되지 않은 모든 예외들의 최종 처리

src/main/kotlin/com/back/koreaTravelGuide/domain/weather/client/WeatherApiClient.kt

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.back.koreaTravelGuide.domain.weather.client
22

3-
// TODO: 기상청 API 클라이언트 - HTTP 요청으로 날씨 데이터 조회 및 XML 파싱
3+
// TODO: 기상청 API 클라이언트 - HTTP 요청으로 날씨 데이터 조회 및 JSON 파싱
44
import com.back.koreaTravelGuide.domain.weather.dto.*
55
import org.springframework.beans.factory.annotation.Value
66
import org.springframework.stereotype.Component
@@ -16,119 +16,142 @@ class WeatherApiClient(
1616
// 1. 중기전망조회 (getMidFcst) - 텍스트 기반 전망
1717
fun fetchMidForecast(regionId: String, baseTime: String): String? {
1818
val stnId = getStnIdFromRegionCode(regionId)
19-
val url = "$apiUrl/getMidFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&stnId=$stnId&tmFc=$baseTime&dataType=XML"
19+
val url = "$apiUrl/getMidFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&stnId=$stnId&tmFc=$baseTime&dataType=JSON"
2020

2121
println("🔮 중기전망조회 API 호출: $url")
2222

2323
return try {
24-
val xmlResponse = restTemplate.getForObject(url, String::class.java)
25-
println("📡 중기전망 응답 수신 (길이: ${xmlResponse?.length ?: 0})")
24+
@Suppress("UNCHECKED_CAST")
25+
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
26+
println("📡 중기전망 JSON 응답 수신")
2627

27-
// API 오류 응답 체크
28-
xmlResponse?.let { response ->
29-
if (response.contains("<resultCode>03</resultCode>") || response.contains("NO_DATA")) {
28+
jsonResponse?.let { response ->
29+
// API 오류 응답 체크
30+
val resultCode = extractJsonValue(response, "response.header.resultCode") as? String
31+
if (resultCode == "03" || resultCode == "NO_DATA") {
3032
println("⚠️ 기상청 API NO_DATA 오류 - 발표시각을 조정해야 할 수 있습니다")
3133
return null
3234
}
3335

34-
val wfSvMatch = Regex("<wfSv><!\\[CDATA\\[(.*?)]]></wfSv>").find(response)
35-
wfSvMatch?.groupValues?.get(1)?.trim()
36+
extractJsonValue(response, "response.body.items.item[0].wfSv") as? String
3637
}
3738
} catch (e: Exception) {
38-
println("❌ 중기전망조회 API 오류: ${e.message}")
39+
println("❌ 중기전망조회 JSON API 오류: ${e.message}")
3940
null
4041
}
4142
}
4243

4344
// 2. 중기기온조회 (getMidTa) - 상세 기온 정보
4445
fun fetchTemperature(regionId: String, baseTime: String): TemperatureData? {
45-
val url = "$apiUrl/getMidTa?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=XML"
46+
val url = "$apiUrl/getMidTa?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=JSON"
4647

4748
println("🌡️ 중기기온조회 API 호출: $url")
4849

4950
return try {
50-
val xmlResponse = restTemplate.getForObject(url, String::class.java)
51-
println("📡 중기기온 응답 수신 (길이: ${xmlResponse?.length ?: 0})")
51+
@Suppress("UNCHECKED_CAST")
52+
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
53+
println("📡 중기기온 JSON 응답 수신")
5254

53-
xmlResponse?.let { parseTemperatureData(it) } ?: TemperatureData()
55+
jsonResponse?.let { parseTemperatureDataFromJson(it) } ?: TemperatureData()
5456
} catch (e: Exception) {
55-
println("❌ 중기기온조회 API 오류: ${e.message}")
57+
println("❌ 중기기온조회 JSON API 오류: ${e.message}")
5658
TemperatureData()
5759
}
5860
}
5961

6062
// 3. 중기육상예보조회 (getMidLandFcst) - 강수 확률
6163
fun fetchLandForecast(regionId: String, baseTime: String): PrecipitationData? {
62-
val url = "$apiUrl/getMidLandFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=XML"
64+
val url = "$apiUrl/getMidLandFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=JSON"
6365

6466
println("🌧️ 중기육상예보조회 API 호출: $url")
6567

6668
return try {
67-
val xmlResponse = restTemplate.getForObject(url, String::class.java)
68-
println("📡 중기육상예보 응답 수신 (길이: ${xmlResponse?.length ?: 0})")
69+
@Suppress("UNCHECKED_CAST")
70+
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
71+
println("📡 중기육상예보 JSON 응답 수신")
6972

70-
xmlResponse?.let { parsePrecipitationData(it) } ?: PrecipitationData()
73+
jsonResponse?.let { parsePrecipitationDataFromJson(it) } ?: PrecipitationData()
7174
} catch (e: Exception) {
72-
println("❌ 중기육상예보조회 API 오류: ${e.message}")
75+
println("❌ 중기육상예보조회 JSON API 오류: ${e.message}")
7376
PrecipitationData()
7477
}
7578
}
7679

77-
// 기온 데이터 파싱
78-
private fun parseTemperatureData(xmlResponse: String): TemperatureData {
80+
// 기온 데이터 JSON 파싱
81+
private fun parseTemperatureDataFromJson(jsonResponse: Map<String, Any>): TemperatureData {
7982
val temperatureData = TemperatureData()
80-
83+
8184
for (day in 4..10) {
82-
val minTemp = extractXmlValue(xmlResponse, "taMin$day")?.toIntOrNull()
83-
val maxTemp = extractXmlValue(xmlResponse, "taMax$day")?.toIntOrNull()
84-
val minTempLow = extractXmlValue(xmlResponse, "taMin${day}Low")?.toIntOrNull()
85-
val minTempHigh = extractXmlValue(xmlResponse, "taMin${day}High")?.toIntOrNull()
86-
val maxTempLow = extractXmlValue(xmlResponse, "taMax${day}Low")?.toIntOrNull()
87-
val maxTempHigh = extractXmlValue(xmlResponse, "taMax${day}High")?.toIntOrNull()
88-
85+
val minTemp = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin$day") as? Number)?.toInt()
86+
val maxTemp = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax$day") as? Number)?.toInt()
87+
val minTempLow = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin${day}Low") as? Number)?.toInt()
88+
val minTempHigh = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin${day}High") as? Number)?.toInt()
89+
val maxTempLow = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax${day}Low") as? Number)?.toInt()
90+
val maxTempHigh = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax${day}High") as? Number)?.toInt()
91+
8992
if (minTemp != null || maxTemp != null) {
9093
val tempInfo = TemperatureInfo(
9194
minTemp = minTemp,
9295
maxTemp = maxTemp,
9396
minTempRange = if (minTempLow != null && minTempHigh != null) "$minTempLow~$minTempHigh" else null,
9497
maxTempRange = if (maxTempLow != null && maxTempHigh != null) "$maxTempLow~$maxTempHigh" else null
9598
)
96-
99+
97100
temperatureData.setDay(day, tempInfo)
98101
}
99102
}
100-
103+
101104
return temperatureData
102105
}
103106

104-
// 강수 확률 데이터 파싱
105-
private fun parsePrecipitationData(xmlResponse: String): PrecipitationData {
107+
// 강수 확률 데이터 JSON 파싱
108+
private fun parsePrecipitationDataFromJson(jsonResponse: Map<String, Any>): PrecipitationData {
106109
val precipitationData = PrecipitationData()
107-
110+
108111
for (day in 4..10) {
109-
val amRain = extractXmlValue(xmlResponse, "rnSt${day}Am")?.toIntOrNull()
110-
val pmRain = extractXmlValue(xmlResponse, "rnSt${day}Pm")?.toIntOrNull()
111-
val amWeather = extractXmlValue(xmlResponse, "wf${day}Am")
112-
val pmWeather = extractXmlValue(xmlResponse, "wf${day}Pm")
113-
112+
val amRain = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt${day}Am") as? Number)?.toInt()
113+
val pmRain = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt${day}Pm") as? Number)?.toInt()
114+
val amWeather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf${day}Am") as? String
115+
val pmWeather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf${day}Pm") as? String
116+
114117
if (amRain != null || pmRain != null || !amWeather.isNullOrBlank() || !pmWeather.isNullOrBlank()) {
115118
val precipInfo = PrecipitationInfo(
116119
amRainPercent = amRain,
117120
pmRainPercent = pmRain,
118121
amWeather = amWeather,
119122
pmWeather = pmWeather
120123
)
121-
124+
122125
precipitationData.setDay(day, precipInfo)
123126
}
124127
}
125-
128+
126129
return precipitationData
127130
}
128131

129-
private fun extractXmlValue(xmlResponse: String, tagName: String): String? {
130-
val regex = Regex("<$tagName>(.*?)</$tagName>")
131-
return regex.find(xmlResponse)?.groupValues?.get(1)?.trim()?.takeIf { it.isNotBlank() }
132+
// JSON에서 값 추출 ("response.body.items.item[0].wfSv" 같은 경로로)
133+
private fun extractJsonValue(jsonMap: Map<String, Any>, path: String): Any? {
134+
var current: Any? = jsonMap
135+
val parts = path.split(".")
136+
137+
for (part in parts) {
138+
when {
139+
current == null -> return null
140+
part.contains("[") && part.contains("]") -> {
141+
// 배열 인덱스 처리 (item[0] 같은 경우)
142+
val arrayName = part.substringBefore("[")
143+
val index = part.substringAfter("[").substringBefore("]").toIntOrNull() ?: 0
144+
145+
current = (current as? Map<*, *>)?.get(arrayName)
146+
current = (current as? List<*>)?.getOrNull(index)
147+
}
148+
else -> {
149+
current = (current as? Map<*, *>)?.get(part)
150+
}
151+
}
152+
}
153+
154+
return current
132155
}
133156

134157
private fun getStnIdFromRegionCode(regionCode: String): String {

0 commit comments

Comments
 (0)