Skip to content

Commit 5909398

Browse files
authored
feat(be) : 전국 중기예보 리스트 (#15)
1 parent c5e374c commit 5909398

File tree

10 files changed

+277
-305
lines changed

10 files changed

+277
-305
lines changed
Lines changed: 13 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package com.back.koreaTravelGuide.domain.ai.weather.client
22

33
// TODO: 기상청 API 클라이언트 - HTTP 요청으로 날씨 데이터 조회 및 JSON 파싱
4-
import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureDto
5-
import com.back.koreaTravelGuide.domain.ai.weather.dto.remove.PrecipitationData
6-
import com.back.koreaTravelGuide.domain.ai.weather.dto.remove.PrecipitationInfo
7-
import com.back.koreaTravelGuide.domain.ai.weather.dto.remove.TemperatureData
8-
import com.back.koreaTravelGuide.domain.ai.weather.dto.remove.TemperatureInfo
4+
import com.back.koreaTravelGuide.domain.ai.weather.client.parser.DataParser
5+
import com.back.koreaTravelGuide.domain.ai.weather.client.tools.Tools
6+
import com.back.koreaTravelGuide.domain.ai.weather.dto.LandForecastData
7+
import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureData
98
import org.springframework.beans.factory.annotation.Value
109
import org.springframework.stereotype.Component
1110
import org.springframework.web.client.RestTemplate
1211

1312
@Component
1413
class WeatherApiClient(
1514
private val restTemplate: RestTemplate,
15+
private val tools : Tools,
16+
private val dataParser: DataParser,
1617
@Value("\${weather.api.key}") private val serviceKey: String,
1718
@Value("\${weather.api.base-url}") private val apiUrl: String,
1819
) {
@@ -21,7 +22,7 @@ class WeatherApiClient(
2122
regionId: String,
2223
baseTime: String,
2324
): String? {
24-
val stnId = getStnIdFromRegionCode(regionId)
25+
val stnId = tools.getStnIdFromRegionCode(regionId)
2526
val url = "$apiUrl/getMidFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&stnId=$stnId&tmFc=$baseTime&dataType=JSON"
2627

2728
println("🔮 중기전망조회 API 호출: $url")
@@ -33,13 +34,13 @@ class WeatherApiClient(
3334

3435
jsonResponse?.let { response ->
3536
// API 오류 응답 체크
36-
val resultCode = extractJsonValue(response, "response.header.resultCode") as? String
37+
val resultCode = dataParser.extractJsonValue(response, "response.header.resultCode") as? String
3738
if (resultCode == "03" || resultCode == "NO_DATA") {
3839
println("⚠️ 기상청 API NO_DATA 오류 - 발표시각을 조정해야 할 수 있습니다")
3940
return null
4041
}
4142

42-
extractJsonValue(response, "response.body.items.item[0].wfSv") as? String
43+
dataParser.extractJsonValue(response, "response.body.items.item[0].wfSv") as? String
4344
}
4445
} catch (e: Exception) {
4546
println("❌ 중기전망조회 JSON API 오류: ${e.message}")
@@ -51,7 +52,7 @@ class WeatherApiClient(
5152
fun fetchTemperature(
5253
regionId: String,
5354
baseTime: String,
54-
): TemperatureDto? {
55+
): TemperatureData? {
5556
val url = "$apiUrl/getMidTa?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=JSON"
5657

5758
println("🌡️ 중기기온조회 API 호출: $url")
@@ -61,10 +62,10 @@ class WeatherApiClient(
6162
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
6263
println("📡 중기기온 JSON 응답 수신")
6364

64-
jsonResponse?.let { parseTemperatureDataFromJson(it) } ?: TemperatureDto()
65+
jsonResponse?.let { dataParser.parseTemperatureDataFromJson(it) } ?: TemperatureData()
6566
} catch (e: Exception) {
6667
println("❌ 중기기온조회 JSON API 오류: ${e.message}")
67-
TemperatureDto()
68+
TemperatureData()
6869
}
6970
}
7071

@@ -82,127 +83,10 @@ class WeatherApiClient(
8283
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
8384
println("📡 중기육상예보 JSON 응답 수신")
8485

85-
jsonResponse?.let { parsePrecipitationDataFromJson(it) } ?: PrecipitationData()
86+
jsonResponse?.let { dataParser.parsePrecipitationDataFromJson(it) } ?: PrecipitationData()
8687
} catch (e: Exception) {
8788
println("❌ 중기육상예보조회 JSON API 오류: ${e.message}")
8889
PrecipitationData()
8990
}
9091
}
91-
92-
// 기온 데이터 JSON 파싱
93-
private fun parseTemperatureDataFromJson(jsonResponse: Map<String, Any>): TemperatureData {
94-
val temperatureData = TemperatureData()
95-
96-
for (day in 4..10) {
97-
val minTemp = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin$day") as? Number)?.toInt()
98-
val maxTemp = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax$day") as? Number)?.toInt()
99-
val minTempLow = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin${day}Low") as? Number)?.toInt()
100-
val minTempHigh = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin${day}High") as? Number)?.toInt()
101-
val maxTempLow = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax${day}Low") as? Number)?.toInt()
102-
val maxTempHigh = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax${day}High") as? Number)?.toInt()
103-
104-
if (minTemp != null || maxTemp != null) {
105-
val tempInfo =
106-
TemperatureInfo(
107-
minTemp = minTemp,
108-
maxTemp = maxTemp,
109-
minTempRange = if (minTempLow != null && minTempHigh != null) "$minTempLow~$minTempHigh" else null,
110-
maxTempRange = if (maxTempLow != null && maxTempHigh != null) "$maxTempLow~$maxTempHigh" else null,
111-
)
112-
113-
temperatureData.setDay(day, tempInfo)
114-
}
115-
}
116-
117-
return temperatureData
118-
}
119-
120-
// 강수 확률 데이터 JSON 파싱
121-
private fun parsePrecipitationDataFromJson(jsonResponse: Map<String, Any>): PrecipitationData {
122-
val precipitationData = PrecipitationData()
123-
124-
for (day in 4..10) {
125-
if (day <= 7) {
126-
// 4~7일: 오전/오후 구분
127-
val amRain = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt${day}Am") as? Number)?.toInt()
128-
val pmRain = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt${day}Pm") as? Number)?.toInt()
129-
val amWeather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf${day}Am") as? String
130-
val pmWeather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf${day}Pm") as? String
131-
132-
if (amRain != null || pmRain != null || !amWeather.isNullOrBlank() || !pmWeather.isNullOrBlank()) {
133-
val precipInfo =
134-
PrecipitationInfo(
135-
amRainPercent = amRain,
136-
pmRainPercent = pmRain,
137-
amWeather = amWeather,
138-
pmWeather = pmWeather,
139-
)
140-
141-
precipitationData.setDay(day, precipInfo)
142-
}
143-
} else {
144-
// 8~10일: 통합 (오전/오후 구분 없음)
145-
val rainPercent = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt$day") as? Number)?.toInt()
146-
val weather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf$day") as? String
147-
148-
if (rainPercent != null || !weather.isNullOrBlank()) {
149-
val precipInfo =
150-
PrecipitationInfo(
151-
amRainPercent = rainPercent,
152-
pmRainPercent = null,
153-
amWeather = weather,
154-
pmWeather = null,
155-
)
156-
157-
precipitationData.setDay(day, precipInfo)
158-
}
159-
}
160-
}
161-
162-
return precipitationData
163-
}
164-
165-
// JSON에서 값 추출 ("response.body.items.item[0].wfSv" 같은 경로로)
166-
private fun extractJsonValue(
167-
jsonMap: Map<String, Any>,
168-
path: String,
169-
): Any? {
170-
var current: Any? = jsonMap
171-
val parts = path.split(".")
172-
173-
for (part in parts) {
174-
when {
175-
current == null -> return null
176-
part.contains("[") && part.contains("]") -> {
177-
// 배열 인덱스 처리 (item[0] 같은 경우)
178-
val arrayName = part.substringBefore("[")
179-
val index = part.substringAfter("[").substringBefore("]").toIntOrNull() ?: 0
180-
181-
current = (current as? Map<*, *>)?.get(arrayName)
182-
current = (current as? List<*>)?.getOrNull(index)
183-
}
184-
else -> {
185-
current = (current as? Map<*, *>)?.get(part)
186-
}
187-
}
188-
}
189-
190-
return current
191-
}
192-
193-
private fun getStnIdFromRegionCode(regionCode: String): String {
194-
return when {
195-
regionCode.startsWith("11B") -> "109" // 서울,인천,경기도
196-
regionCode.startsWith("11D1") -> "105" // 강원도영서
197-
regionCode.startsWith("11D2") -> "105" // 강원도영동
198-
regionCode.startsWith("11C2") -> "133" // 대전,세종,충청남도
199-
regionCode.startsWith("11C1") -> "131" // 충청북도
200-
regionCode.startsWith("11F2") -> "156" // 광주,전라남도
201-
regionCode.startsWith("11F1") -> "146" // 전북자치도
202-
regionCode.startsWith("11H1") -> "143" // 대구,경상북도
203-
regionCode.startsWith("11H2") -> "159" // 부산,울산,경상남도
204-
regionCode.startsWith("11G") -> "184" // 제주도
205-
else -> "108" // 전국
206-
}
207-
}
20892
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.back.koreaTravelGuide.domain.ai.weather.client.parser
2+
3+
import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureData
4+
import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureInfo
5+
import org.springframework.stereotype.Component
6+
7+
@Component
8+
class DataParser {
9+
// JSON에서 값 추출 ("response.body.items.item[0].wfSv" 같은 경로로)
10+
fun extractJsonValue(
11+
jsonMap: Map<String, Any>,
12+
path: String,
13+
): Any? {
14+
var current: Any? = jsonMap
15+
val parts = path.split(".")
16+
17+
for (part in parts) {
18+
when {
19+
current == null -> return null
20+
part.contains("[") && part.contains("]") -> {
21+
// 배열 인덱스 처리 (item[0] 같은 경우)
22+
val arrayName = part.substringBefore("[")
23+
val index = part.substringAfter("[").substringBefore("]").toIntOrNull() ?: 0
24+
25+
current = (current as? Map<*, *>)?.get(arrayName)
26+
current = (current as? List<*>)?.getOrNull(index)
27+
}
28+
else -> {
29+
current = (current as? Map<*, *>)?.get(part)
30+
}
31+
}
32+
}
33+
34+
return current
35+
}
36+
37+
// 기온 데이터 JSON 파싱
38+
fun parseTemperatureDataFromJson(jsonResponse: Map<String, Any>): TemperatureData {
39+
val TemperatureData = TemperatureData()
40+
41+
for (day in 4..10) {
42+
val minTemp = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin$day") as? Number)?.toInt()
43+
val maxTemp = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax$day") as? Number)?.toInt()
44+
val minTempLow = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin${day}Low") as? Number)?.toInt()
45+
val minTempHigh = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMin${day}High") as? Number)?.toInt()
46+
val maxTempLow = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax${day}Low") as? Number)?.toInt()
47+
val maxTempHigh = (extractJsonValue(jsonResponse, "response.body.items.item[0].taMax${day}High") as? Number)?.toInt()
48+
49+
if (minTemp != null || maxTemp != null) {
50+
val tempInfo =
51+
TemperatureInfo(
52+
minTemp = minTemp,
53+
maxTemp = maxTemp,
54+
minTempRange = if (minTempLow != null && minTempHigh != null) "$minTempLow~$minTempHigh" else null,
55+
maxTempRange = if (maxTempLow != null && maxTempHigh != null) "$maxTempLow~$maxTempHigh" else null,
56+
)
57+
58+
TemperatureData.setDay(day, tempInfo)
59+
}
60+
}
61+
62+
return TemperatureData
63+
}
64+
65+
// 강수 확률 데이터 JSON 파싱
66+
fun parsePrecipitationDataFromJson(jsonResponse: Map<String, Any>): PrecipitationData {
67+
val precipitationData = PrecipitationData()
68+
69+
for (day in 4..10) {
70+
if (day <= 7) {
71+
// 4~7일: 오전/오후 구분
72+
val amRain = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt${day}Am") as? Number)?.toInt()
73+
val pmRain = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt${day}Pm") as? Number)?.toInt()
74+
val amWeather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf${day}Am") as? String
75+
val pmWeather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf${day}Pm") as? String
76+
77+
if (amRain != null || pmRain != null || !amWeather.isNullOrBlank() || !pmWeather.isNullOrBlank()) {
78+
val precipInfo =
79+
PrecipitationInfo(
80+
amRainPercent = amRain,
81+
pmRainPercent = pmRain,
82+
amWeather = amWeather,
83+
pmWeather = pmWeather,
84+
)
85+
86+
precipitationData.setDay(day, precipInfo)
87+
}
88+
} else {
89+
// 8~10일: 통합 (오전/오후 구분 없음)
90+
val rainPercent = (extractJsonValue(jsonResponse, "response.body.items.item[0].rnSt$day") as? Number)?.toInt()
91+
val weather = extractJsonValue(jsonResponse, "response.body.items.item[0].wf$day") as? String
92+
93+
if (rainPercent != null || !weather.isNullOrBlank()) {
94+
val precipInfo =
95+
PrecipitationInfo(
96+
amRainPercent = rainPercent,
97+
pmRainPercent = null,
98+
amWeather = weather,
99+
pmWeather = null,
100+
)
101+
102+
precipitationData.setDay(day, precipInfo)
103+
}
104+
}
105+
}
106+
107+
return precipitationData
108+
}
109+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.back.koreaTravelGuide.domain.ai.weather.client.tools
2+
3+
import org.springframework.stereotype.Component
4+
5+
@Component
6+
class Tools {
7+
fun getStnIdFromRegionCode(regionCode: String): String {
8+
return when {
9+
regionCode.startsWith("11B") -> "109" // 서울,인천,경기도
10+
regionCode.startsWith("11D1") -> "105" // 강원도영서
11+
regionCode.startsWith("11D2") -> "105" // 강원도영동
12+
regionCode.startsWith("11C2") -> "133" // 대전,세종,충청남도
13+
regionCode.startsWith("11C1") -> "131" // 충청북도
14+
regionCode.startsWith("11F2") -> "156" // 광주,전라남도
15+
regionCode.startsWith("11F1") -> "146" // 전북자치도
16+
regionCode.startsWith("11H1") -> "143" // 대구,경상북도
17+
regionCode.startsWith("11H2") -> "159" // 부산,울산,경상남도
18+
regionCode.startsWith("11G") -> "184" // 제주도
19+
else -> "108" // 전국
20+
}
21+
}
22+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.back.koreaTravelGuide.domain.ai.weather.dto
2+
3+
data class LandForecastData()

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/weather/dto/TemperatureDto.kt renamed to src/main/kotlin/com/back/koreaTravelGuide/domain/ai/weather/dto/TemperatureData.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import kotlin.text.set
44

55
// TODO: 날씨 내부 데이터 구조 - 기상청 API 응답 데이터 매핑용 내부 클래스들
66
@Suppress("unused") // JSON 직렬화를 위해 필요
7-
data class TemperatureDto(
7+
data class TemperatureData(
88
private val days: MutableMap<Int, TemperatureInfo?> = mutableMapOf(),
99
) {
1010
fun setDay(

0 commit comments

Comments
 (0)