@@ -11,6 +11,7 @@ import org.springframework.stereotype.Service
1111import org.springframework.web.client.RestTemplate
1212import java.time.LocalDate
1313import java.time.LocalDateTime
14+ import java.time.Period
1415import java.time.format.DateTimeFormatter
1516import kotlin.math.pow
1617
@@ -37,6 +38,7 @@ class ExternalWeatherService(
3738 }
3839 val baseDate = baseLocalDateTime.format(DateTimeFormatter .ofPattern(" yyyyMMdd" ))
3940 skiResortRepository.findAll().forEach { resort ->
41+ // μ΄λ¨κΈ° μ€ν© API νΈμΆ
4042 val url = " https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst" +
4143 " ?serviceKey=$apiKey " +
4244 " &pageNo=1" +
@@ -99,7 +101,8 @@ class ExternalWeatherService(
99101 val feelsLike = calculateFeelsLike(temperature, windSpeed)
100102 val condition = determineCondition(data)
101103 val description = generateDescription(condition, temperature)
102- val dailyWeather = dailyWeatherRepository.findBySkiResortAndForecastDate(resort, LocalDate .now())[0 ]
104+ val dailyWeather = dailyWeatherRepository.findBySkiResortAndForecastDate(resort, LocalDate .now())
105+ ? : throw IllegalStateException (" Daily weather not found for today" )
103106 // dailyWeather.maxTempλ³΄λ€ temperatureμ΄ λμΌλ©΄ maxTempλ₯Ό μ
λ°μ΄νΈ
104107 if (temperature > dailyWeather.maxTemp) {
105108 dailyWeather.maxTemp = temperature
@@ -174,17 +177,63 @@ class ExternalWeatherService(
174177
175178 @Transactional
176179 fun updateDailyWeather () {
177- val baseDateTime = getYesterdayBaseDateTime ()
178- val baseDate = baseDateTime .first
179- val baseTime = baseDateTime .second
180+ val baseDateTime23 = getYesterdayBaseDateTime23 ()
181+ val baseDate = baseDateTime23 .first
182+ val baseTime = baseDateTime23 .second
180183
181- val tmFc = baseDate.format(DateTimeFormatter .ofPattern(" yyyyMMdd" )) + baseTime
182- // κΈ°μ‘΄ λ°μ΄ν° μμ
183- dailyWeatherRepository.deleteByDDayGreaterThanEqual(4 )
184184 skiResortRepository.findAll().forEach { resort ->
185+ val groupedMap = getShortTermDataGroupedByDate(
186+ baseDate = baseDate.format(DateTimeFormatter .ofPattern(" yyyyMMdd" )),
187+ baseTime = baseTime,
188+ nx = resort.xCoordinate,
189+ ny = resort.yCoordinate
190+ )
191+ // groupedMap: Map<LocalDate, List<JsonNode>>
192+ // key: μ보λ μ§, value: ν΄λΉ λ μ§μ μκ°λ³ νλͺ©λ€
193+ groupedMap.forEach { (date, items) ->
194+ // 3μΌ λ€(= μ€λ ν¬ν¨ μ΅λ 3~4μΌ) μ λκΉμ§λ§ μ μ₯νλ€κ³ κ°μ
195+ // νμνλ©΄ 쑰건문μΌλ‘ νν°λ§ κ°λ₯
196+ if (date.isAfter(LocalDate .now().plusDays(3 ))) {
197+ return @forEach // 3μΌ μ΄ν λ°μ΄ν°λ 무μ(μμ)
198+ }
199+
200+ val parsedDaily = parseDailyForecastByDay(date, items)
201+
202+ // DBμμ (resort, date)λ‘ κΈ°μ‘΄ μν°ν°κ° μλμ§ κ²μ
203+ val existing = dailyWeatherRepository.findBySkiResortAndForecastDate(resort, date)
204+ if (existing != null ) {
205+ // update
206+ existing.minTemp = parsedDaily.minTemp
207+ existing.maxTemp = parsedDaily.maxTemp
208+ existing.precipitationChance = parsedDaily.precipitationChance
209+ existing.condition = parsedDaily.getCondition()
210+
211+ // dayOfWeek, dDayλ μ¬κ³μ°
212+ existing.dayOfWeek = convertDayOfWeek(date.dayOfWeek.name)
213+ existing.dDay = calcDDay(date)
214+
215+ dailyWeatherRepository.save(existing)
216+ } else {
217+ // insert
218+ val newDaily = DailyWeather (
219+ forecastDate = date,
220+ dayOfWeek = convertDayOfWeek(date.dayOfWeek.name),
221+ dDay = calcDDay(date),
222+ precipitationChance = parsedDaily.precipitationChance,
223+ maxTemp = parsedDaily.maxTemp,
224+ minTemp = parsedDaily.minTemp,
225+ condition = parsedDaily.getCondition(),
226+ skiResort = resort
227+ )
228+ dailyWeatherRepository.save(newDaily)
229+ }
230+ }
185231 val detailedAreaCode = resort.detailedAreaCode
186232 val broadAreaCode = resort.broadAreaCode
187-
233+ val baseDateTime18 = getYesterdayBaseDateTime18()
234+ val baseDate18 = baseDateTime18.first
235+ val baseTime18 = baseDateTime18.second
236+ val tmFc = baseDate18.format(DateTimeFormatter .ofPattern(" yyyyMMdd" )) + baseTime18
188237 // 첫 λ²μ§Έ API νΈμΆ (μ€κΈ° κΈ°μ¨ μ보)
189238 val midTaUrl = buildMidTaUrl(detailedAreaCode, tmFc)
190239 val midTaResponse = restTemplate.getForObject(midTaUrl, String ::class .java)
@@ -201,7 +250,14 @@ class ExternalWeatherService(
201250 }
202251 }
203252
204- private fun getYesterdayBaseDateTime (): Pair <LocalDate , String > {
253+ private fun getYesterdayBaseDateTime23 (): Pair <LocalDate , String > {
254+ // μ΄μ λ μ§
255+ val yesterday = LocalDate .now().minusDays(1 )
256+ val hour = 23 // 23μ κΈ°μ€
257+ return Pair (yesterday, String .format(" %02d00" , hour))
258+ }
259+
260+ private fun getYesterdayBaseDateTime18 (): Pair <LocalDate , String > {
205261 // μ΄μ λ μ§
206262 val yesterday = LocalDate .now().minusDays(1 )
207263 val hour = 18 // 18μ κΈ°μ€
@@ -262,17 +318,32 @@ class ExternalWeatherService(
262318 val precipitationChance = getPrecipitationChance(midLandData, i)
263319 val condition = getCondition(midLandData, i)
264320
265- val dailyWeather = DailyWeather (
266- skiResort = resort,
267- forecastDate = forecastDate,
268- dayOfWeek = convertDayOfWeek(dayOfWeek),
269- dDay = i - 1 ,
270- precipitationChance = precipitationChance,
271- maxTemp = maxTemp,
272- minTemp = minTemp,
273- condition = condition
274- )
275- weatherList.add(dailyWeather)
321+ // 1) λ¨Όμ DBμμ (리쑰νΈ, forecastDate)λ‘ μ‘°ν
322+ val existingWeather: DailyWeather ? = dailyWeatherRepository.findBySkiResortAndForecastDate(resort, forecastDate)
323+ if (existingWeather != null ) {
324+ existingWeather.dayOfWeek = convertDayOfWeek(dayOfWeek)
325+ existingWeather.precipitationChance = precipitationChance
326+ existingWeather.maxTemp = maxTemp
327+ existingWeather.minTemp = minTemp
328+ existingWeather.condition = condition
329+ existingWeather.dDay = i - 1
330+ dailyWeatherRepository.save(existingWeather)
331+ weatherList.add(existingWeather)
332+ continue
333+ } else {
334+ // 2) μμΌλ©΄ μλ‘ μμ±
335+ val dailyWeather = DailyWeather (
336+ skiResort = resort,
337+ forecastDate = forecastDate,
338+ dayOfWeek = convertDayOfWeek(dayOfWeek),
339+ dDay = i - 1 ,
340+ precipitationChance = precipitationChance,
341+ maxTemp = maxTemp,
342+ minTemp = minTemp,
343+ condition = condition
344+ )
345+ weatherList.add(dailyWeather)
346+ }
276347 }
277348
278349 return weatherList
@@ -310,6 +381,10 @@ class ExternalWeatherService(
310381 }
311382 }
312383
384+ /* *
385+ * μ€μ /μ€ν μ보 μ€ 'μ°μ μμκ° λ λμ' μͺ½μ κ³ λ₯΄λ λ‘μ§ (μ: "λΉ"κ° "ꡬλ¦λ§μ"λ³΄λ€ μ°μ )
386+ * μν©μ λ§κ² μ°μ μμλ₯Ό μ‘°μ ν μ μμ
387+ */
313388 private fun selectWorseCondition (am : String , pm : String ): String {
314389 val conditionPriority = listOf (
315390 " λ§μ" ,
@@ -364,6 +439,7 @@ class ExternalWeatherService(
364439 val nx = resort.xCoordinate
365440 val ny = resort.yCoordinate
366441
442+ // λ¨κΈ°μ보쑰ν
367443 val url = buildVilageFcstUrl(baseDate, baseTime, nx, ny)
368444 val response = restTemplate.getForObject(url, String ::class .java)
369445 val forecastData = parseVilageFcstResponse(response)
@@ -534,7 +610,7 @@ class ExternalWeatherService(
534610 val maxTemp = if (tmxValues.isNotEmpty()) tmxValues.maxOrNull() ? : 0 else tmpValues.maxOrNull() ? : 0
535611
536612 // μ£Όκ° λ μ¨ μ
λ°μ΄νΈ
537- val existingWeather = dailyWeatherRepository.findBySkiResortAndDDay (resort, dDay )
613+ val existingWeather = dailyWeatherRepository.findBySkiResortAndForecastDate (resort, date )
538614 if (existingWeather != null ) {
539615 existingWeather.forecastDate = date
540616 existingWeather.dayOfWeek = convertDayOfWeek(date.dayOfWeek.name)
@@ -577,4 +653,107 @@ class ExternalWeatherService(
577653 else -> ConditionPriority (" λ§μ" , 1 )
578654 }
579655 }
656+
657+ /* *
658+ * λ¨κΈ° μ보(μ΅λ 3μΌ ν) λ°μ΄ν°λ₯Ό κ°μ Έμ€κ³ , λ μ§λ³λ‘ λ¬Άμ΄ λ°ν
659+ * @param baseDate : μ‘°ν κΈ°μ€ λ μ§(yyyyMMdd)
660+ * @param baseTime : μ‘°ν κΈ°μ€ μκ°(HHmm)
661+ * @param nx, ny : 격μ μ’ν
662+ * @return Map<LocalDate, List<JsonNode>> ννλ‘, λ μ§λ³λ‘ μμ΄ν
λ€μ λ¬Άμ΄μ λ°ν
663+ */
664+ fun getShortTermDataGroupedByDate (
665+ baseDate : String ,
666+ baseTime : String ,
667+ nx : String ,
668+ ny : String
669+ ): Map <LocalDate , List <JsonNode >> {
670+ // λ¨κΈ°μ보쑰ν URL
671+ val url = buildShortTermUrl(baseDate, baseTime, nx, ny)
672+ val response = restTemplate.getForObject(url, String ::class .java) ? : return emptyMap()
673+
674+ val rootNode = objectMapper.readTree(response)
675+ val items = rootNode[" response" ][" body" ][" items" ]?.get(" item" ) ? : return emptyMap()
676+
677+ // λ μ§λ³λ‘ λ¬ΆκΈ° μν λ§΅
678+ val groupedMap = mutableMapOf<LocalDate , MutableList <JsonNode >>()
679+
680+ for (item in items) {
681+ val fcstDateStr = item[" fcstDate" ].asText() // μ) "20250116"
682+ val localDate = LocalDate .parse(fcstDateStr, DateTimeFormatter .ofPattern(" yyyyMMdd" ))
683+
684+ // μμ§ μ‘΄μ¬νμ§ μλ ν€λ©΄ μλ‘μ΄ λ¦¬μ€νΈλ‘ μ΄κΈ°ν
685+ val listForDate = groupedMap.getOrPut(localDate) { mutableListOf () }
686+ listForDate.add(item)
687+ }
688+
689+ return groupedMap
690+ }
691+
692+ private fun buildShortTermUrl (baseDate : String , baseTime : String , nx : String , ny : String ): String {
693+ return " https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst" +
694+ " ?serviceKey=$apiKey " +
695+ " &pageNo=1" +
696+ " &numOfRows=1000" +
697+ " &dataType=JSON" +
698+ " &base_date=$baseDate " +
699+ " &base_time=$baseTime " +
700+ " &nx=$nx " +
701+ " &ny=$ny "
702+ }
703+
704+ /* *
705+ * λ μ§λ³ List<JsonNode> μμ μΌμ΅μ /μΌμ΅κ³ κΈ°μ¨, POP, PTY, SKY λ±μ μΆμΆν΄ DailyForecast μμ±
706+ */
707+ fun parseDailyForecastByDay (date : LocalDate , items : List <JsonNode >): DailyForecast {
708+ val daily = DailyForecast (date = date)
709+
710+ for (item in items) {
711+ val category = item[" category" ].asText() // μ: TMP, TMN, TMX, POP, PTY, SKY
712+ val fcstValue = item[" fcstValue" ].asText()
713+
714+ when (category) {
715+ " TMP" -> {
716+ val tmpVal = fcstValue.toIntOrNull() ? : continue
717+ daily.minTemp = minOf(daily.minTemp, tmpVal)
718+ daily.maxTemp = maxOf(daily.maxTemp, tmpVal)
719+ }
720+ " TMN" -> {
721+ val tmnVal = fcstValue.toIntOrNull() ? : continue
722+ daily.minTemp = tmnVal
723+ }
724+ " TMX" -> {
725+ val tmxVal = fcstValue.toIntOrNull() ? : continue
726+ daily.maxTemp = tmxVal
727+ }
728+ " POP" -> {
729+ // ν루 μ€ κ°μ₯ λμ κ°μνλ₯ μ κ·Έλ νλ₯ λ‘ λ³Έλ€
730+ val popVal = fcstValue.toIntOrNull() ? : 0
731+ daily.precipitationChance = maxOf(daily.precipitationChance, popVal)
732+ }
733+ " PTY" -> {
734+ // κ°μνν μ½λ μ€ 'κ°μ₯ μ μ’μ(ν°) κ°'μ μ°μ
735+ val ptyVal = fcstValue.toIntOrNull() ? : 0
736+ daily.ptyCode = maxOf(daily.ptyCode, ptyVal)
737+ }
738+ " SKY" -> {
739+ // λ§μ°¬κ°μ§λ‘ SKYλ 'κ°μ₯ νλ¦°(ν°) κ°'μ μ°μ
740+ // (1=λ§μ, 3=ꡬλ¦λ§μ, 4=νλ¦Ό)
741+ val skyVal = fcstValue.toIntOrNull() ? : 1
742+ daily.skyCode = maxOf(daily.skyCode, skyVal)
743+ }
744+ }
745+ }
746+
747+ // λ§μ½ TMN, TMX λ λ€ μμλ€λ©΄ TMP κΈ°λ°μ min/maxTempλ₯Ό κ·Έλλ‘ μ¬μ©
748+ // νλλΌλ μμΌλ©΄ ν΄λΉ κ°μ μ°μ μΌλ‘(μ΄λ―Έ μμμ λμ
)
749+ if (daily.minTemp == Int .MAX_VALUE ) daily.minTemp = 0
750+ if (daily.maxTemp == Int .MIN_VALUE ) daily.maxTemp = 0
751+
752+ return daily
753+ }
754+
755+ private fun calcDDay (forecastDate : LocalDate ): Int {
756+ val today = LocalDate .now()
757+ return Period .between(today, forecastDate).days
758+ }
580759}
0 commit comments