@@ -5,10 +5,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
55import jakarta.transaction.Transactional
66import nexters.weski.ski_resort.SkiResort
77import nexters.weski.ski_resort.SkiResortRepository
8- import nexters.weski.weather.CurrentWeather
9- import nexters.weski.weather.CurrentWeatherRepository
10- import nexters.weski.weather.DailyWeather
11- import nexters.weski.weather.DailyWeatherRepository
8+ import nexters.weski.weather.*
129import org.springframework.beans.factory.annotation.Value
1310import org.springframework.stereotype.Service
1411import org.springframework.web.client.RestTemplate
@@ -21,6 +18,7 @@ import kotlin.math.pow
2118class ExternalWeatherService (
2219 private val currentWeatherRepository : CurrentWeatherRepository ,
2320 private val dailyWeatherRepository : DailyWeatherRepository ,
21+ private val hourlyWeatherRepository : HourlyWeatherRepository ,
2422 private val skiResortRepository : SkiResortRepository
2523) {
2624 @Value(" \$ {weather.api.key}" )
@@ -158,7 +156,7 @@ class ExternalWeatherService(
158156
159157 @Transactional
160158 fun updateDailyWeather () {
161- val baseDateTime = getBaseDateTime ()
159+ val baseDateTime = getYesterdayBaseDateTime ()
162160 val baseDate = baseDateTime.first
163161 val baseTime = baseDateTime.second
164162
@@ -185,7 +183,7 @@ class ExternalWeatherService(
185183 }
186184 }
187185
188- private fun getBaseDateTime (): Pair <LocalDate , String > {
186+ private fun getYesterdayBaseDateTime (): Pair <LocalDate , String > {
189187 // 어제 날짜
190188 val yesterday = LocalDate .now().minusDays(1 )
191189 val hour = 18 // 18시 기준
@@ -339,10 +337,226 @@ class ExternalWeatherService(
339337 }
340338
341339 @Transactional
342- fun updateDDayValues () {
343- // d_day 값이 0인 데이터 삭제
344- dailyWeatherRepository.deleteByDDay(0 )
345- // 나머지 데이터의 d_day 값을 1씩 감소
346- dailyWeatherRepository.decrementDDayValues()
340+ fun updateHourlyAndDailyWeather () {
341+ val baseDateTime = getBaseDateTime()
342+ val baseDate = baseDateTime.first
343+ val baseTime = baseDateTime.second
344+
345+ skiResortRepository.findAll().forEach { resort ->
346+ val nx = resort.xCoordinate
347+ val ny = resort.yCoordinate
348+
349+ val url = buildVilageFcstUrl(baseDate, baseTime, nx, ny)
350+ val response = restTemplate.getForObject(url, String ::class .java)
351+ val forecastData = parseVilageFcstResponse(response)
352+
353+ // 시간대별 날씨 업데이트
354+ val hourlyWeathers = createHourlyWeather(resort, forecastData)
355+ hourlyWeatherRepository.deleteBySkiResort(resort)
356+ hourlyWeatherRepository.saveAll(hourlyWeathers)
357+
358+ // 주간 날씨 업데이트
359+ updateShortTermDailyWeather(resort, forecastData)
360+ }
361+ }
362+
363+ private fun getBaseDateTime (): Pair <String , String > {
364+ // 전날 23시 return(ex: 20241109 2300)
365+ val yesterday = LocalDateTime .now().minusDays(1 )
366+ .format(DateTimeFormatter .ofPattern(" yyyyMMdd" ))
367+ val baseTime = " 2300"
368+ return Pair (yesterday, baseTime)
369+ }
370+
371+ private fun buildVilageFcstUrl (baseDate : String , baseTime : String , nx : String , ny : String ): String {
372+ return " http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst" +
373+ " ?serviceKey=$apiKey " +
374+ " &pageNo=1" +
375+ " &numOfRows=1000" +
376+ " &dataType=JSON" +
377+ " &base_date=$baseDate " +
378+ " &base_time=$baseTime " +
379+ " &nx=$nx " +
380+ " &ny=$ny "
381+ }
382+
383+ private fun parseVilageFcstResponse (response : String? ): List <ForecastItem > {
384+ response ? : return emptyList()
385+ val rootNode = objectMapper.readTree(response)
386+ val itemsNode = rootNode[" response" ][" body" ][" items" ][" item" ]
387+ val items = mutableListOf<ForecastItem >()
388+
389+ itemsNode?.forEach { itemNode ->
390+ val category = itemNode[" category" ].asText()
391+ val fcstDate = itemNode[" fcstDate" ].asText()
392+ val fcstTime = itemNode[" fcstTime" ].asText()
393+ val fcstValue = itemNode[" fcstValue" ].asText()
394+ items.add(ForecastItem (category, fcstDate, fcstTime, fcstValue))
395+ }
396+
397+ return items
398+ }
399+
400+ data class ForecastItem (
401+ val category : String ,
402+ val fcstDate : String ,
403+ val fcstTime : String ,
404+ val fcstValue : String
405+ )
406+
407+ private fun createHourlyWeather (
408+ resort : SkiResort ,
409+ forecastData : List <ForecastItem >
410+ ): List <HourlyWeather > {
411+ val hourlyWeathers = mutableListOf<HourlyWeather >()
412+ val timeSlots = generateTimeSlots()
413+
414+ var priority = 1
415+ for (timeSlot in timeSlots) {
416+ val dataForTime = forecastData.filter { it.fcstDate == timeSlot.first && it.fcstTime == timeSlot.second }
417+ if (dataForTime.isEmpty()) continue
418+
419+ val dataMap = dataForTime.groupBy { it.category }.mapValues { it.value.first().fcstValue }
420+ val temperature = dataMap[" TMP" ]?.toIntOrNull() ? : continue
421+ val precipitationChance = dataMap[" POP" ]?.toIntOrNull() ? : continue
422+ val sky = dataMap[" SKY" ]?.toIntOrNull() ? : 1
423+ val pty = dataMap[" PTY" ]?.toIntOrNull() ? : 0
424+ val condition = determineCondition(sky, pty)
425+
426+ val forecastTime = formatForecastTime(timeSlot.second)
427+ val hourlyWeather = HourlyWeather (
428+ skiResort = resort,
429+ forecastTime = forecastTime,
430+ priority = priority,
431+ temperature = temperature,
432+ precipitationChance = precipitationChance,
433+ condition = condition
434+ )
435+ hourlyWeathers.add(hourlyWeather)
436+ priority++
437+ }
438+ return hourlyWeathers
439+ }
440+
441+ private fun generateTimeSlots (): List <Pair <String , String >> {
442+ val timeSlots = mutableListOf<Pair <String , String >>()
443+ val today = LocalDate .now()
444+ val tomorrow = today.plusDays(1 )
445+ val format = DateTimeFormatter .ofPattern(" yyyyMMdd" )
446+
447+ val times = listOf (" 0800" , " 1000" , " 1200" , " 1400" , " 1600" , " 1800" , " 2000" , " 2200" , " 0000" , " 0200" )
448+ for (time in times) {
449+ val date = if (time == " 0000" || time == " 0200" ) tomorrow.format(format) else today.format(format)
450+ timeSlots.add(Pair (date, time))
451+ }
452+ return timeSlots
453+ }
454+
455+ private fun formatForecastTime (fcstTime : String ): String {
456+ val hour = fcstTime.substring(0 , 2 ).toInt()
457+ val period = if (hour < 12 ) " 오전" else " 오후"
458+ val hourIn12 = if (hour == 0 || hour == 12 ) 12 else hour % 12
459+ return " $period ${hourIn12} 시"
460+ }
461+
462+ private fun determineCondition (sky : Int , pty : Int ): String {
463+ return when (pty) {
464+ 1 -> " 비"
465+ 2 -> " 비/눈"
466+ 3 -> " 눈"
467+ 4 -> " 소나기"
468+ else -> when (sky) {
469+ 1 -> " 맑음"
470+ 3 -> " 구름많음"
471+ 4 -> " 흐림"
472+ else -> " 맑음"
473+ }
474+ }
475+ }
476+
477+ private fun updateShortTermDailyWeather (
478+ resort : SkiResort ,
479+ forecastData : List <ForecastItem >
480+ ) {
481+ val today = LocalDate .now()
482+ val tomorrow = today.plusDays(1 )
483+ val format = DateTimeFormatter .ofPattern(" yyyyMMdd" )
484+
485+ val days = listOf (Pair (today, 0 ), Pair (tomorrow, 1 ))
486+ days.forEach { (date, dDay) ->
487+ val dateStr = date.format(format)
488+ val dataForDay = forecastData.filter { it.fcstDate == dateStr }
489+ if (dataForDay.isEmpty()) return @forEach
490+
491+ // 최고 강수확률
492+ val popValues = dataForDay.filter { it.category == " POP" }.mapNotNull { it.fcstValue.toIntOrNull() }
493+ val precipitationChance = popValues.maxOrNull() ? : 0
494+
495+ // 가장 나쁜 상태
496+ val conditions = dataForDay.filter { it.category == " SKY" || it.category == " PTY" }
497+ .groupBy { Pair (it.fcstDate, it.fcstTime) }
498+ .map { (_, items) ->
499+ val sky = items.find { it.category == " SKY" }?.fcstValue?.toIntOrNull() ? : 1
500+ val pty = items.find { it.category == " PTY" }?.fcstValue?.toIntOrNull() ? : 0
501+ determineConditionPriority(sky, pty)
502+ }
503+
504+ val worstCondition = conditions.maxByOrNull { it.priority }?.condition ? : " 맑음"
505+
506+ // 최저기온과 최고기온 계산
507+ val tmnValues =
508+ dataForDay.filter { it.category == " TMN" }.mapNotNull { it.fcstValue.toDoubleOrNull()?.toInt() }
509+ val tmxValues =
510+ dataForDay.filter { it.category == " TMX" }.mapNotNull { it.fcstValue.toDoubleOrNull()?.toInt() }
511+
512+ val tmpValues =
513+ dataForDay.filter { it.category == " TMP" }.mapNotNull { it.fcstValue.toDoubleOrNull()?.toInt() }
514+
515+ val minTemp = if (tmnValues.isNotEmpty()) tmnValues.minOrNull() ? : 0 else tmpValues.minOrNull() ? : 0
516+ val maxTemp = if (tmxValues.isNotEmpty()) tmxValues.maxOrNull() ? : 0 else tmpValues.maxOrNull() ? : 0
517+
518+ // 주간 날씨 업데이트
519+ val existingWeather = dailyWeatherRepository.findBySkiResortAndDDay(resort, dDay)
520+ if (existingWeather != null ) {
521+ existingWeather.forecastDate = date
522+ existingWeather.dayOfWeek = convertDayOfWeek(date.dayOfWeek.name)
523+ existingWeather.precipitationChance = precipitationChance
524+ existingWeather.condition = worstCondition
525+ existingWeather.minTemp = minTemp
526+ existingWeather.maxTemp = maxTemp
527+ dailyWeatherRepository.save(existingWeather)
528+ } else {
529+ val dailyWeather = DailyWeather (
530+ skiResort = resort,
531+ forecastDate = date,
532+ dayOfWeek = convertDayOfWeek(date.dayOfWeek.name),
533+ dDay = dDay,
534+ precipitationChance = precipitationChance,
535+ maxTemp = maxTemp,
536+ minTemp = minTemp,
537+ condition = worstCondition
538+ )
539+ dailyWeatherRepository.save(dailyWeather)
540+ }
541+ }
542+ }
543+
544+ data class ConditionPriority (val condition : String , val priority : Int )
545+
546+ private fun determineConditionPriority (sky : Int , pty : Int ): ConditionPriority {
547+ return when (pty) {
548+ 3 -> ConditionPriority (" 눈" , 7 )
549+ 2 -> ConditionPriority (" 비/눈" , 6 )
550+ 1 -> ConditionPriority (" 비" , 5 )
551+ 4 -> ConditionPriority (" 소나기" , 4 )
552+ 0 -> when (sky) {
553+ 4 -> ConditionPriority (" 흐림" , 3 )
554+ 3 -> ConditionPriority (" 구름많음" , 2 )
555+ 1 -> ConditionPriority (" 맑음" , 1 )
556+ else -> ConditionPriority (" 맑음" , 1 )
557+ }
558+
559+ else -> ConditionPriority (" 맑음" , 1 )
560+ }
347561 }
348- }
562+ }
0 commit comments