11package nexters.weski.batch
22
3+ import com.fasterxml.jackson.databind.JsonNode
34import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
45import jakarta.transaction.Transactional
56import nexters.weski.ski_resort.SkiResort
67import nexters.weski.ski_resort.SkiResortRepository
78import nexters.weski.weather.CurrentWeather
89import nexters.weski.weather.CurrentWeatherRepository
10+ import nexters.weski.weather.DailyWeather
11+ import nexters.weski.weather.DailyWeatherRepository
912import org.springframework.beans.factory.annotation.Value
1013import org.springframework.stereotype.Service
1114import org.springframework.web.client.RestTemplate
15+ import java.time.LocalDate
1216import java.time.LocalDateTime
1317import java.time.format.DateTimeFormatter
1418import kotlin.math.pow
1519
1620@Service
1721class 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+ " ®Id=$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+ " ®Id=$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}
0 commit comments