Skip to content

Commit 205e985

Browse files
authored
Merge pull request #17 from Nexters/16-weekly-weather-updates
[#16] 주간 날씨 업데이트 batch
2 parents 754b8b8 + 57f8342 commit 205e985

File tree

7 files changed

+332
-92
lines changed

7 files changed

+332
-92
lines changed

init.sql

Lines changed: 119 additions & 91 deletions
Large diffs are not rendered by default.

src/main/kotlin/nexters/weski/batch/ExternalWeatherService.kt

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
package nexters.weski.batch
22

3+
import com.fasterxml.jackson.databind.JsonNode
34
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
45
import jakarta.transaction.Transactional
56
import nexters.weski.ski_resort.SkiResort
67
import nexters.weski.ski_resort.SkiResortRepository
78
import nexters.weski.weather.CurrentWeather
89
import nexters.weski.weather.CurrentWeatherRepository
10+
import nexters.weski.weather.DailyWeather
11+
import nexters.weski.weather.DailyWeatherRepository
912
import org.springframework.beans.factory.annotation.Value
1013
import org.springframework.stereotype.Service
1114
import org.springframework.web.client.RestTemplate
15+
import java.time.LocalDate
1216
import java.time.LocalDateTime
1317
import java.time.format.DateTimeFormatter
1418
import kotlin.math.pow
1519

1620
@Service
1721
class ExternalWeatherService(
1822
private val currentWeatherRepository: CurrentWeatherRepository,
23+
private val dailyWeatherRepository: DailyWeatherRepository,
1924
private val skiResortRepository: SkiResortRepository
2025
) {
2126
@Value("\${weather.api.key}")
@@ -150,4 +155,194 @@ class ExternalWeatherService(
150155

151156
return "$prefix $postfix"
152157
}
158+
159+
@Transactional
160+
fun updateDailyWeather() {
161+
val baseDateTime = getBaseDateTime()
162+
val baseDate = baseDateTime.first
163+
val baseTime = baseDateTime.second
164+
165+
val tmFc = baseDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + baseTime
166+
// 기존 데이터 삭제
167+
dailyWeatherRepository.deleteByDDayGreaterThanEqual(2)
168+
skiResortRepository.findAll().forEach { resort ->
169+
val detailedAreaCode = resort.detailedAreaCode
170+
val broadAreaCode = resort.broadAreaCode
171+
172+
// 첫 번째 API 호출 (중기 기온 예보)
173+
val midTaUrl = buildMidTaUrl(detailedAreaCode, tmFc)
174+
val midTaResponse = restTemplate.getForObject(midTaUrl, String::class.java)
175+
val midTaData = parseMidTaResponse(midTaResponse)
176+
177+
// 두 번째 API 호출 (중기 육상 예보)
178+
val midLandUrl = buildMidLandUrl(broadAreaCode, tmFc)
179+
val midLandResponse = restTemplate.getForObject(midLandUrl, String::class.java)
180+
val midLandData = parseMidLandResponse(midLandResponse)
181+
182+
// 데이터 병합 및 처리
183+
val dailyWeathers = mergeWeatherData(resort, midTaData, midLandData)
184+
dailyWeatherRepository.saveAll(dailyWeathers)
185+
}
186+
}
187+
188+
private fun getBaseDateTime(): Pair<LocalDate, String> {
189+
// 어제 날짜
190+
val yesterday = LocalDate.now().minusDays(1)
191+
val hour = 18 // 18시 기준
192+
return Pair(yesterday, String.format("%02d00", hour))
193+
}
194+
195+
private fun buildMidTaUrl(areaCode: String, tmFc: String): String {
196+
return "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidTa" +
197+
"?serviceKey=$apiKey" +
198+
"&pageNo=1" +
199+
"&numOfRows=10" +
200+
"&dataType=JSON" +
201+
"&regId=$areaCode" +
202+
"&tmFc=$tmFc"
203+
}
204+
205+
private fun buildMidLandUrl(regId: String, tmFc: String): String {
206+
return "https://apis.data.go.kr/1360000/MidFcstInfoService/getMidLandFcst" +
207+
"?serviceKey=$apiKey" +
208+
"&pageNo=1" +
209+
"&numOfRows=10" +
210+
"&dataType=JSON" +
211+
"&regId=$regId" +
212+
"&tmFc=$tmFc"
213+
}
214+
215+
private fun parseMidTaResponse(response: String?): JsonNode? {
216+
response ?: return null
217+
val rootNode = objectMapper.readTree(response)
218+
return rootNode["response"]["body"]["items"]["item"]?.get(0)
219+
}
220+
221+
private fun parseMidLandResponse(response: String?): JsonNode? {
222+
response ?: return null
223+
val rootNode = objectMapper.readTree(response)
224+
return rootNode["response"]["body"]["items"]["item"]?.get(0)
225+
}
226+
227+
private fun mergeWeatherData(
228+
resort: SkiResort,
229+
midTaData: JsonNode?,
230+
midLandData: JsonNode?
231+
): List<DailyWeather> {
232+
val weatherList = mutableListOf<DailyWeather>()
233+
val now = LocalDate.now()
234+
235+
if (midTaData == null || midLandData == null) {
236+
return weatherList
237+
}
238+
239+
for (i in 3..10) {
240+
val forecastDate = now.plusDays(i.toLong() - 1)
241+
val dayOfWeek = forecastDate.dayOfWeek.name // 영어 요일명
242+
243+
val maxTemp = midTaData.get("taMax$i")?.asInt() ?: continue
244+
val minTemp = midTaData.get("taMin$i")?.asInt() ?: continue
245+
246+
val precipitationChance = getPrecipitationChance(midLandData, i)
247+
val condition = getCondition(midLandData, i)
248+
249+
val dailyWeather = DailyWeather(
250+
skiResort = resort,
251+
forecastDate = forecastDate,
252+
dayOfWeek = convertDayOfWeek(dayOfWeek),
253+
dDay = i - 1,
254+
precipitationChance = precipitationChance,
255+
maxTemp = maxTemp,
256+
minTemp = minTemp,
257+
condition = condition
258+
)
259+
weatherList.add(dailyWeather)
260+
}
261+
262+
return weatherList
263+
}
264+
265+
private fun getPrecipitationChance(midLandData: JsonNode, day: Int): Int {
266+
return when (day) {
267+
in 3..7 -> {
268+
val amChance = midLandData.get("rnSt${day}Am")?.asInt() ?: 0
269+
val pmChance = midLandData.get("rnSt${day}Pm")?.asInt() ?: 0
270+
maxOf(amChance, pmChance)
271+
}
272+
273+
in 8..10 -> {
274+
midLandData.get("rnSt$day")?.asInt() ?: 0
275+
}
276+
277+
else -> 0
278+
}
279+
}
280+
281+
private fun getCondition(midLandData: JsonNode, day: Int): String {
282+
return when (day) {
283+
in 3..7 -> {
284+
val amCondition = midLandData.get("wf${day}Am")?.asText() ?: ""
285+
val pmCondition = midLandData.get("wf${day}Pm")?.asText() ?: ""
286+
selectWorseCondition(amCondition, pmCondition)
287+
}
288+
289+
in 8..10 -> {
290+
midLandData.get("wf$day")?.asText() ?: ""
291+
}
292+
293+
else -> "알 수 없음"
294+
}
295+
}
296+
297+
private fun selectWorseCondition(am: String, pm: String): String {
298+
val conditionPriority = listOf(
299+
"맑음",
300+
"구름많음",
301+
"흐림",
302+
"구름많고 소나기",
303+
"구름많고 비",
304+
"구름많고 비/눈",
305+
"흐리고 비",
306+
"흐리고 소나기",
307+
"소나기",
308+
"",
309+
"비/눈",
310+
"흐리고 눈",
311+
"흐리고 비/눈",
312+
""
313+
)
314+
val amIndex = conditionPriority.indexOf(am)
315+
val pmIndex = conditionPriority.indexOf(pm)
316+
317+
return if (amIndex == -1 && pmIndex == -1) {
318+
"맑음"
319+
} else if (amIndex == -1) {
320+
pm
321+
} else if (pmIndex == -1) {
322+
am
323+
} else {
324+
if (amIndex > pmIndex) am else pm
325+
}
326+
}
327+
328+
private fun convertDayOfWeek(englishDay: String): String {
329+
return when (englishDay) {
330+
"MONDAY" -> "월요일"
331+
"TUESDAY" -> "화요일"
332+
"WEDNESDAY" -> "수요일"
333+
"THURSDAY" -> "목요일"
334+
"FRIDAY" -> "금요일"
335+
"SATURDAY" -> "토요일"
336+
"SUNDAY" -> "일요일"
337+
else -> "ERROR"
338+
}
339+
}
340+
341+
@Transactional
342+
fun updateDDayValues() {
343+
// d_day 값이 0인 데이터 삭제
344+
dailyWeatherRepository.deleteByDDay(0)
345+
// 나머지 데이터의 d_day 값을 1씩 감소
346+
dailyWeatherRepository.decrementDDayValues()
347+
}
153348
}

src/main/kotlin/nexters/weski/batch/WeatherScheduler.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@ class WeatherScheduler(
1111
fun scheduleWeatherUpdate() {
1212
externalWeatherService.updateCurrentWeather()
1313
}
14+
15+
@Scheduled(cron = "0 30 3 * * *")
16+
fun scheduledDailyWeatherUpdate() {
17+
externalWeatherService.updateDDayValues()
18+
externalWeatherService.updateDailyWeather()
19+
}
1420
}

src/main/kotlin/nexters/weski/ski_resort/SkiResort.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ data class SkiResort(
3434
val snowfallTime: String? = null,
3535
val xCoordinate: String,
3636
val yCoordinate: String,
37+
val detailedAreaCode: String,
38+
val broadAreaCode: String,
3739

3840
@OneToMany(mappedBy = "skiResort")
3941
val slopes: List<Slope> = emptyList(),

src/main/kotlin/nexters/weski/weather/DailyWeather.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ data class DailyWeather(
1414

1515
val forecastDate: LocalDate,
1616
val dayOfWeek: String,
17+
val dDay: Int,
1718
val precipitationChance: Int,
1819
val maxTemp: Int,
1920
val minTemp: Int,
21+
@Column(name = "`condition`")
2022
val condition: String,
2123

2224
@ManyToOne
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package nexters.weski.weather
22

33
import org.springframework.data.jpa.repository.JpaRepository
4+
import org.springframework.data.jpa.repository.Modifying
5+
import org.springframework.data.jpa.repository.Query
46

57
interface DailyWeatherRepository : JpaRepository<DailyWeather, Long> {
68
fun findAllBySkiResortResortId(resortId: Long): List<DailyWeather>
9+
fun deleteByDDayGreaterThanEqual(dDay: Int)
10+
fun deleteByDDay(dDay: Int)
11+
12+
@Modifying
13+
@Query("UPDATE DailyWeather dw SET dw.dDay = dw.dDay - 1 WHERE dw.dDay > 0")
14+
fun decrementDDayValues()
715
}

src/main/resources/application.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,3 @@ springdoc:
2424
weather:
2525
api:
2626
key: p6zNXOJrrBY4cuX7OYtdDMtmR8hiGeUaBLf0z6BXnm/qniV8wB0SuPwBgqKDTKV/24EW7xiRY3DCS21Ess/42Q==
27-

0 commit comments

Comments
 (0)