Skip to content

Commit f9f7daa

Browse files
committed
πŸ› Update: forecastDate 3 to 5
#35
1 parent a1644dc commit f9f7daa

File tree

5 files changed

+252
-26
lines changed

5 files changed

+252
-26
lines changed

β€Žsrc/main/kotlin/nexters/weski/batch/ExternalWeatherService.ktβ€Ž

Lines changed: 200 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import org.springframework.stereotype.Service
1111
import org.springframework.web.client.RestTemplate
1212
import java.time.LocalDate
1313
import java.time.LocalDateTime
14+
import java.time.Period
1415
import java.time.format.DateTimeFormatter
1516
import 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
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package nexters.weski.weather
2+
3+
import java.time.LocalDate
4+
5+
data class DailyForecast(
6+
val date: LocalDate,
7+
var minTemp: Int = Int.MAX_VALUE,
8+
var maxTemp: Int = Int.MIN_VALUE,
9+
var precipitationChance: Int = 0,
10+
var ptyCode: Int = 0, // PTY: κ°•μˆ˜ν˜•νƒœ μ½”λ“œ
11+
var skyCode: Int = 1 // SKY: 기본값을 λ§‘μŒ(1)으둜
12+
) {
13+
// μ΅œμ’… condition 계산
14+
fun getCondition(): String {
15+
// PTYκ°€ 0이 μ•„λ‹ˆλ©΄ λΉ„/눈/μ†Œλ‚˜κΈ° λ“±
16+
// PTY μ½”λ“œ 맀핑은 상황에 맞게 더 정ꡐ화 κ°€λŠ₯
17+
if (ptyCode != 0) {
18+
return when (ptyCode) {
19+
1 -> "λΉ„"
20+
2 -> "λΉ„/눈"
21+
3 -> "눈"
22+
4 -> "μ†Œλ‚˜κΈ°" // λ‹¨κΈ°μ˜ˆλ³΄μ—μ„œ 4λŠ” μ†Œλ‚˜κΈ°
23+
else -> "κΈ°νƒ€κ°•μˆ˜"
24+
}
25+
} else {
26+
// PTY = 0인 경우 SKY μ½”λ“œ 확인
27+
return when (skyCode) {
28+
1 -> "λ§‘μŒ"
29+
3 -> "κ΅¬λ¦„λ§ŽμŒ"
30+
4 -> "흐림"
31+
else -> "λ§‘μŒ"
32+
}
33+
}
34+
}
35+
}

β€Žsrc/main/kotlin/nexters/weski/weather/DailyWeather.ktβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ data class DailyWeather(
1414

1515
var forecastDate: LocalDate,
1616
var dayOfWeek: String,
17-
val dDay: Int,
17+
var dDay: Int,
1818
var precipitationChance: Int,
1919
var maxTemp: Int,
2020
var minTemp: Int,

β€Žsrc/main/kotlin/nexters/weski/weather/DailyWeatherRepository.ktβ€Ž

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import java.time.LocalDate
66

77
interface DailyWeatherRepository : JpaRepository<DailyWeather, Long> {
88
fun findAllBySkiResortResortId(resortId: Long): List<DailyWeather>
9-
fun deleteByDDayGreaterThanEqual(dDay: Int)
10-
fun findBySkiResortAndDDay(skiResort: SkiResort, dDay: Int): DailyWeather?
11-
fun findBySkiResortAndForecastDate(skiResort: SkiResort, forecastDate: LocalDate): List<DailyWeather>
9+
fun findBySkiResortAndForecastDate(skiResort: SkiResort, forecastDate: LocalDate): DailyWeather?
10+
fun findAllBySkiResortResortIdAndForecastDateBetween(
11+
resortId: Long,
12+
startDate: LocalDate,
13+
endDate: LocalDate
14+
): List<DailyWeather>
1215
}

β€Žsrc/main/kotlin/nexters/weski/weather/WeatherService.ktβ€Ž

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package nexters.weski.weather
22

33
import org.springframework.stereotype.Service
4+
import java.time.LocalDate
45

56
@Service
67
class WeatherService(
@@ -11,7 +12,15 @@ class WeatherService(
1112
fun getWeatherByResortId(resortId: Long): WeatherDto? {
1213
val currentWeather = currentWeatherRepository.findBySkiResortResortId(resortId) ?: return null
1314
val hourlyWeather = hourlyWeatherRepository.findBySkiResortResortId(resortId)
14-
val dailyWeather = dailyWeatherRepository.findAllBySkiResortResortId(resortId)
15+
16+
val today = LocalDate.now()
17+
val after7Days = today.plusDays(7)
18+
19+
val dailyWeather = dailyWeatherRepository.findAllBySkiResortResortIdAndForecastDateBetween(
20+
resortId = resortId,
21+
startDate = today,
22+
endDate = after7Days
23+
)
1524

1625
return WeatherDto.fromEntities(currentWeather, hourlyWeather, dailyWeather)
1726
}

0 commit comments

Comments
Β (0)