@@ -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