Skip to content

Commit

Permalink
Merge pull request #19 from Nexters/feature/18-hourly-weather-update
Browse files Browse the repository at this point in the history
[#18] 시간대별 날씨 업데이트 batch
  • Loading branch information
jun108059 authored Nov 9, 2024
2 parents 205e985 + 47f0cec commit 3a8d54d
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 52 deletions.
238 changes: 226 additions & 12 deletions src/main/kotlin/nexters/weski/batch/ExternalWeatherService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import jakarta.transaction.Transactional
import nexters.weski.ski_resort.SkiResort
import nexters.weski.ski_resort.SkiResortRepository
import nexters.weski.weather.CurrentWeather
import nexters.weski.weather.CurrentWeatherRepository
import nexters.weski.weather.DailyWeather
import nexters.weski.weather.DailyWeatherRepository
import nexters.weski.weather.*
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
Expand All @@ -21,6 +18,7 @@ import kotlin.math.pow
class ExternalWeatherService(
private val currentWeatherRepository: CurrentWeatherRepository,
private val dailyWeatherRepository: DailyWeatherRepository,
private val hourlyWeatherRepository: HourlyWeatherRepository,
private val skiResortRepository: SkiResortRepository
) {
@Value("\${weather.api.key}")
Expand Down Expand Up @@ -158,7 +156,7 @@ class ExternalWeatherService(

@Transactional
fun updateDailyWeather() {
val baseDateTime = getBaseDateTime()
val baseDateTime = getYesterdayBaseDateTime()
val baseDate = baseDateTime.first
val baseTime = baseDateTime.second

Expand All @@ -185,7 +183,7 @@ class ExternalWeatherService(
}
}

private fun getBaseDateTime(): Pair<LocalDate, String> {
private fun getYesterdayBaseDateTime(): Pair<LocalDate, String> {
// 어제 날짜
val yesterday = LocalDate.now().minusDays(1)
val hour = 18 // 18시 기준
Expand Down Expand Up @@ -339,10 +337,226 @@ class ExternalWeatherService(
}

@Transactional
fun updateDDayValues() {
// d_day 값이 0인 데이터 삭제
dailyWeatherRepository.deleteByDDay(0)
// 나머지 데이터의 d_day 값을 1씩 감소
dailyWeatherRepository.decrementDDayValues()
fun updateHourlyAndDailyWeather() {
val baseDateTime = getBaseDateTime()
val baseDate = baseDateTime.first
val baseTime = baseDateTime.second

skiResortRepository.findAll().forEach { resort ->
val nx = resort.xCoordinate
val ny = resort.yCoordinate

val url = buildVilageFcstUrl(baseDate, baseTime, nx, ny)
val response = restTemplate.getForObject(url, String::class.java)
val forecastData = parseVilageFcstResponse(response)

// 시간대별 날씨 업데이트
val hourlyWeathers = createHourlyWeather(resort, forecastData)
hourlyWeatherRepository.deleteBySkiResort(resort)
hourlyWeatherRepository.saveAll(hourlyWeathers)

// 주간 날씨 업데이트
updateShortTermDailyWeather(resort, forecastData)
}
}

private fun getBaseDateTime(): Pair<String, String> {
// 전날 23시 return(ex: 20241109 2300)
val yesterday = LocalDateTime.now().minusDays(1)
.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
val baseTime = "2300"
return Pair(yesterday, baseTime)
}

private fun buildVilageFcstUrl(baseDate: String, baseTime: String, nx: String, ny: String): String {
return "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst" +
"?serviceKey=$apiKey" +
"&pageNo=1" +
"&numOfRows=1000" +
"&dataType=JSON" +
"&base_date=$baseDate" +
"&base_time=$baseTime" +
"&nx=$nx" +
"&ny=$ny"
}

private fun parseVilageFcstResponse(response: String?): List<ForecastItem> {
response ?: return emptyList()
val rootNode = objectMapper.readTree(response)
val itemsNode = rootNode["response"]["body"]["items"]["item"]
val items = mutableListOf<ForecastItem>()

itemsNode?.forEach { itemNode ->
val category = itemNode["category"].asText()
val fcstDate = itemNode["fcstDate"].asText()
val fcstTime = itemNode["fcstTime"].asText()
val fcstValue = itemNode["fcstValue"].asText()
items.add(ForecastItem(category, fcstDate, fcstTime, fcstValue))
}

return items
}

data class ForecastItem(
val category: String,
val fcstDate: String,
val fcstTime: String,
val fcstValue: String
)

private fun createHourlyWeather(
resort: SkiResort,
forecastData: List<ForecastItem>
): List<HourlyWeather> {
val hourlyWeathers = mutableListOf<HourlyWeather>()
val timeSlots = generateTimeSlots()

var priority = 1
for (timeSlot in timeSlots) {
val dataForTime = forecastData.filter { it.fcstDate == timeSlot.first && it.fcstTime == timeSlot.second }
if (dataForTime.isEmpty()) continue

val dataMap = dataForTime.groupBy { it.category }.mapValues { it.value.first().fcstValue }
val temperature = dataMap["TMP"]?.toIntOrNull() ?: continue
val precipitationChance = dataMap["POP"]?.toIntOrNull() ?: continue
val sky = dataMap["SKY"]?.toIntOrNull() ?: 1
val pty = dataMap["PTY"]?.toIntOrNull() ?: 0
val condition = determineCondition(sky, pty)

val forecastTime = formatForecastTime(timeSlot.second)
val hourlyWeather = HourlyWeather(
skiResort = resort,
forecastTime = forecastTime,
priority = priority,
temperature = temperature,
precipitationChance = precipitationChance,
condition = condition
)
hourlyWeathers.add(hourlyWeather)
priority++
}
return hourlyWeathers
}

private fun generateTimeSlots(): List<Pair<String, String>> {
val timeSlots = mutableListOf<Pair<String, String>>()
val today = LocalDate.now()
val tomorrow = today.plusDays(1)
val format = DateTimeFormatter.ofPattern("yyyyMMdd")

val times = listOf("0800", "1000", "1200", "1400", "1600", "1800", "2000", "2200", "0000", "0200")
for (time in times) {
val date = if (time == "0000" || time == "0200") tomorrow.format(format) else today.format(format)
timeSlots.add(Pair(date, time))
}
return timeSlots
}

private fun formatForecastTime(fcstTime: String): String {
val hour = fcstTime.substring(0, 2).toInt()
val period = if (hour < 12) "오전" else "오후"
val hourIn12 = if (hour == 0 || hour == 12) 12 else hour % 12
return "$period ${hourIn12}"
}

private fun determineCondition(sky: Int, pty: Int): String {
return when (pty) {
1 -> ""
2 -> "비/눈"
3 -> ""
4 -> "소나기"
else -> when (sky) {
1 -> "맑음"
3 -> "구름많음"
4 -> "흐림"
else -> "맑음"
}
}
}

private fun updateShortTermDailyWeather(
resort: SkiResort,
forecastData: List<ForecastItem>
) {
val today = LocalDate.now()
val tomorrow = today.plusDays(1)
val format = DateTimeFormatter.ofPattern("yyyyMMdd")

val days = listOf(Pair(today, 0), Pair(tomorrow, 1))
days.forEach { (date, dDay) ->
val dateStr = date.format(format)
val dataForDay = forecastData.filter { it.fcstDate == dateStr }
if (dataForDay.isEmpty()) return@forEach

// 최고 강수확률
val popValues = dataForDay.filter { it.category == "POP" }.mapNotNull { it.fcstValue.toIntOrNull() }
val precipitationChance = popValues.maxOrNull() ?: 0

// 가장 나쁜 상태
val conditions = dataForDay.filter { it.category == "SKY" || it.category == "PTY" }
.groupBy { Pair(it.fcstDate, it.fcstTime) }
.map { (_, items) ->
val sky = items.find { it.category == "SKY" }?.fcstValue?.toIntOrNull() ?: 1
val pty = items.find { it.category == "PTY" }?.fcstValue?.toIntOrNull() ?: 0
determineConditionPriority(sky, pty)
}

val worstCondition = conditions.maxByOrNull { it.priority }?.condition ?: "맑음"

// 최저기온과 최고기온 계산
val tmnValues =
dataForDay.filter { it.category == "TMN" }.mapNotNull { it.fcstValue.toDoubleOrNull()?.toInt() }
val tmxValues =
dataForDay.filter { it.category == "TMX" }.mapNotNull { it.fcstValue.toDoubleOrNull()?.toInt() }

val tmpValues =
dataForDay.filter { it.category == "TMP" }.mapNotNull { it.fcstValue.toDoubleOrNull()?.toInt() }

val minTemp = if (tmnValues.isNotEmpty()) tmnValues.minOrNull() ?: 0 else tmpValues.minOrNull() ?: 0
val maxTemp = if (tmxValues.isNotEmpty()) tmxValues.maxOrNull() ?: 0 else tmpValues.maxOrNull() ?: 0

// 주간 날씨 업데이트
val existingWeather = dailyWeatherRepository.findBySkiResortAndDDay(resort, dDay)
if (existingWeather != null) {
existingWeather.forecastDate = date
existingWeather.dayOfWeek = convertDayOfWeek(date.dayOfWeek.name)
existingWeather.precipitationChance = precipitationChance
existingWeather.condition = worstCondition
existingWeather.minTemp = minTemp
existingWeather.maxTemp = maxTemp
dailyWeatherRepository.save(existingWeather)
} else {
val dailyWeather = DailyWeather(
skiResort = resort,
forecastDate = date,
dayOfWeek = convertDayOfWeek(date.dayOfWeek.name),
dDay = dDay,
precipitationChance = precipitationChance,
maxTemp = maxTemp,
minTemp = minTemp,
condition = worstCondition
)
dailyWeatherRepository.save(dailyWeather)
}
}
}

data class ConditionPriority(val condition: String, val priority: Int)

private fun determineConditionPriority(sky: Int, pty: Int): ConditionPriority {
return when (pty) {
3 -> ConditionPriority("", 7)
2 -> ConditionPriority("비/눈", 6)
1 -> ConditionPriority("", 5)
4 -> ConditionPriority("소나기", 4)
0 -> when (sky) {
4 -> ConditionPriority("흐림", 3)
3 -> ConditionPriority("구름많음", 2)
1 -> ConditionPriority("맑음", 1)
else -> ConditionPriority("맑음", 1)
}

else -> ConditionPriority("맑음", 1)
}
}
}
}
8 changes: 6 additions & 2 deletions src/main/kotlin/nexters/weski/batch/WeatherScheduler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ class WeatherScheduler(

@Scheduled(cron = "0 30 3 * * *")
fun scheduledDailyWeatherUpdate() {
externalWeatherService.updateDDayValues()
externalWeatherService.updateDailyWeather()
}
}

@Scheduled(cron = "0 10 5 * * *")
fun scheduledHourlyAndDailyUpdate() {
externalWeatherService.updateHourlyAndDailyWeather()
}
}
12 changes: 6 additions & 6 deletions src/main/kotlin/nexters/weski/weather/DailyWeather.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ data class DailyWeather(
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

val forecastDate: LocalDate,
val dayOfWeek: String,
var forecastDate: LocalDate,
var dayOfWeek: String,
val dDay: Int,
val precipitationChance: Int,
val maxTemp: Int,
val minTemp: Int,
var precipitationChance: Int,
var maxTemp: Int,
var minTemp: Int,
@Column(name = "`condition`")
val condition: String,
var condition: String,

@ManyToOne
@JoinColumn(name = "resort_id")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package nexters.weski.weather

import nexters.weski.ski_resort.SkiResort
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query

interface DailyWeatherRepository : JpaRepository<DailyWeather, Long> {
fun findAllBySkiResortResortId(resortId: Long): List<DailyWeather>
fun deleteByDDayGreaterThanEqual(dDay: Int)
fun deleteByDDay(dDay: Int)

@Modifying
@Query("UPDATE DailyWeather dw SET dw.dDay = dw.dDay - 1 WHERE dw.dDay > 0")
fun decrementDDayValues()
fun findBySkiResortAndDDay(skiResort: SkiResort, dDay: Int): DailyWeather?
}
5 changes: 3 additions & 2 deletions src/main/kotlin/nexters/weski/weather/HourlyWeather.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package nexters.weski.weather
import jakarta.persistence.*
import nexters.weski.common.BaseEntity
import nexters.weski.ski_resort.SkiResort
import java.time.LocalDateTime

@Entity
@Table(name = "hourly_weather")
Expand All @@ -12,9 +11,11 @@ data class HourlyWeather(
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

val forecastTime: LocalDateTime,
val forecastTime: String,
val priority: Int,
val temperature: Int,
val precipitationChance: Int,
@Column(name = "`condition`")
val condition: String,

@ManyToOne
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package nexters.weski.weather

import nexters.weski.ski_resort.SkiResort
import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalDateTime

interface HourlyWeatherRepository : JpaRepository<HourlyWeather, Long> {
fun findAllBySkiResortResortIdAndForecastTimeBetween(
resortId: Long,
startTime: LocalDateTime,
endTime: LocalDateTime
): List<HourlyWeather>
fun deleteBySkiResort(skiResort: SkiResort)
}
2 changes: 1 addition & 1 deletion src/main/kotlin/nexters/weski/weather/WeatherDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ data class HourlyWeatherDto(
companion object {
fun fromEntity(entity: HourlyWeather): HourlyWeatherDto {
return HourlyWeatherDto(
time = entity.forecastTime.toLocalTimeString(),
time = entity.forecastTime,
temperature = entity.temperature,
precipitationChance = entity.precipitationChance.toPercentString(),
condition = entity.condition
Expand Down
9 changes: 1 addition & 8 deletions src/main/kotlin/nexters/weski/weather/WeatherService.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package nexters.weski.weather

import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.time.LocalTime

@Service
class WeatherService(
Expand All @@ -12,12 +10,7 @@ class WeatherService(
) {
fun getWeatherByResortId(resortId: Long): WeatherDto? {
val currentWeather = currentWeatherRepository.findBySkiResortResortId(resortId) ?: return null
// 오늘, 내일 날짜의 날씨 정보만 가져옴
val startTime = LocalDateTime.now().with(LocalTime.MIN)
val endTime = startTime.plusDays(2).with(LocalTime.MAX)
val hourlyWeather = hourlyWeatherRepository.findAllBySkiResortResortIdAndForecastTimeBetween(
resortId, startTime, endTime
)
val hourlyWeather = hourlyWeatherRepository.findAll()
val dailyWeather = dailyWeatherRepository.findAllBySkiResortResortId(resortId)

return WeatherDto.fromEntities(currentWeather, hourlyWeather, dailyWeather)
Expand Down
Loading

0 comments on commit 3a8d54d

Please sign in to comment.