Skip to content

Commit

Permalink
Merge pull request #15 from Nexters/feature/14-real-time-weather-updates
Browse files Browse the repository at this point in the history
[#14] 실시간 날씨 업데이트 batch
  • Loading branch information
jun108059 authored Nov 9, 2024
2 parents 7e273d5 + b9aba35 commit 754b8b8
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 7 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")
implementation("org.springframework.boot:spring-boot-starter-quartz")
runtimeOnly("com.mysql:mysql-connector-j")

testImplementation("org.mockito:mockito-core:4.11.0")
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/nexters/weski/WeskiApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package nexters.weski

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling

@SpringBootApplication(scanBasePackages = ["nexters.weski"])
@EnableScheduling
class WeskiApplication

fun main(args: Array<String>) {
Expand Down
153 changes: 153 additions & 0 deletions src/main/kotlin/nexters/weski/batch/ExternalWeatherService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package nexters.weski.batch

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 org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import kotlin.math.pow

@Service
class ExternalWeatherService(
private val currentWeatherRepository: CurrentWeatherRepository,
private val skiResortRepository: SkiResortRepository
) {
@Value("\${weather.api.key}")
lateinit var apiKey: String

val restTemplate = RestTemplate()
val objectMapper = jacksonObjectMapper()

@Transactional
fun updateCurrentWeather() {
val baseDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
val baseTime = getBaseTime()
skiResortRepository.findAll().forEach { resort ->
val url = "https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst" +
"?serviceKey=$apiKey" +
"&pageNo=1" +
"&numOfRows=1000" +
"&dataType=JSON" +
"&base_date=$baseDate" +
"&base_time=$baseTime" +
"&nx=${resort.xCoordinate}" +
"&ny=${resort.yCoordinate}"
val response = restTemplate.getForObject(url, String::class.java)
val weatherData = parseWeatherData(response)
val newCurrentWeather = mapToCurrentWeather(weatherData, resort)

// 기존 데이터 조회
val existingWeather = currentWeatherRepository.findBySkiResortResortId(resort.resortId)

if (existingWeather != null) {
// 기존 데이터의 ID를 사용하여 새로운 엔티티 생성
val updatedWeather = newCurrentWeather.copy(id = existingWeather.id)
currentWeatherRepository.save(updatedWeather)
} else {
// 새로운 데이터 삽입
currentWeatherRepository.save(newCurrentWeather)
}
}
}

private fun getBaseTime(): String {
val now = LocalDateTime.now().minusHours(1)
val hour = now.hour.toString().padStart(2, '0')
return "${hour}00"
}

private fun parseWeatherData(response: String?): Map<String, String> {
val data = mutableMapOf<String, String>()

response?.let {
val rootNode = objectMapper.readTree(it)
val items = rootNode["response"]["body"]["items"]["item"]

items.forEach { item ->
val category = item["category"].asText()
val value = item["obsrValue"].asText()
data[category] = value
}
}

return data
}

private fun mapToCurrentWeather(
data: Map<String, String>,
resort: SkiResort
): CurrentWeather {
val temperature = data["T1H"]?.toDoubleOrNull()?.toInt() ?: 0
val windSpeed = data["WSD"]?.toDoubleOrNull() ?: 0.0
val feelsLike = calculateFeelsLike(temperature, windSpeed)
val condition = determineCondition(data)
val description = generateDescription(condition, temperature)

return CurrentWeather(
temperature = temperature,
maxTemp = data["TMX"]?.toDoubleOrNull()?.toInt() ?: temperature,
minTemp = data["TMN"]?.toDoubleOrNull()?.toInt() ?: temperature,
feelsLike = feelsLike,
condition = condition,
description = description,
skiResort = resort
)
}

private fun calculateFeelsLike(temperature: Int, windSpeed: Double): Int {
return if (temperature <= 10 && windSpeed >= 4.8) {
val feelsLike =
13.12 + 0.6215 * temperature - 11.37 * windSpeed.pow(0.16) + 0.3965 * temperature * windSpeed.pow(
0.16
)
feelsLike.toInt()
} else {
temperature
}
}

private fun determineCondition(data: Map<String, String>): String {
val pty = data["PTY"]?.toIntOrNull() ?: 0
val sky = data["SKY"]?.toIntOrNull() ?: 1

return when {
pty == 1 || pty == 4 -> ""
pty == 2 -> "비/눈"
pty == 3 -> ""
sky == 1 -> "맑음"
sky == 3 -> "구름많음"
sky == 4 -> "흐림"
else -> "맑음"
}
}

private fun generateDescription(condition: String, temperature: Int): String {
val prefix = when (condition) {
"맑음" -> "화창하고"
"구름많음" -> "구름이 많고"
"흐림" -> "흐리고"
"" -> "비가 오고"
"비/눈" -> "눈비가 내리고"
"" -> "눈이 오고"
else -> ""
}

val postfix = when {
temperature <= -15 -> "매우 추워요"
temperature in -14..-10 -> "다소 추워요"
temperature in -9..-5 -> "적당한 온도에요"
temperature in -4..0 -> "조금 따뜻해요"
temperature in 1..5 -> "따뜻해요"
temperature in 6..10 -> "다소 더워요"
else -> "더워요"
}

return "$prefix $postfix"
}
}
14 changes: 14 additions & 0 deletions src/main/kotlin/nexters/weski/batch/WeatherScheduler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package nexters.weski.batch

import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class WeatherScheduler(
private val externalWeatherService: ExternalWeatherService
) {
@Scheduled(cron = "0 0 * * * ?")
fun scheduleWeatherUpdate() {
externalWeatherService.updateCurrentWeather()
}
}
8 changes: 6 additions & 2 deletions src/main/kotlin/nexters/weski/ski_resort/SkiResort.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import jakarta.persistence.*
import nexters.weski.common.BaseEntity
import nexters.weski.slope.Slope
import nexters.weski.webcam.Webcam
import java.time.LocalDate

@Entity
@Table(name = "ski_resorts")
Expand All @@ -17,9 +18,9 @@ data class SkiResort(
@Enumerated(EnumType.STRING)
val status: ResortStatus,

val openingDate: java.time.LocalDate? = null,
val openingDate: LocalDate? = null,

val closingDate: java.time.LocalDate? = null,
val closingDate: LocalDate? = null,

val openSlopes: Int = 0,

Expand All @@ -30,6 +31,9 @@ data class SkiResort(
val lateNightOperatingHours: String? = null,
val dawnOperatingHours: String? = null,
val midnightOperatingHours: String? = null,
val snowfallTime: String? = null,
val xCoordinate: String,
val yCoordinate: String,

@OneToMany(mappedBy = "skiResort")
val slopes: List<Slope> = emptyList(),
Expand Down
9 changes: 5 additions & 4 deletions src/main/kotlin/nexters/weski/weather/CurrentWeather.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ import nexters.weski.ski_resort.SkiResort
@Entity
@Table(name = "current_weather")
data class CurrentWeather(
@Id
val resortId: Long,
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

val temperature: Int,
val maxTemp: Int,
val minTemp: Int,
val feelsLike: Int,
val description: String,

@Column(name = "`condition`")
val condition: String,

@OneToOne
@MapsId
@JoinColumn(name = "resort_id")
@JoinColumn(name = "resort_id", unique = true)
val skiResort: SkiResort
) : BaseEntity()
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 @@ -15,7 +15,7 @@ data class WeatherDto(
dailyWeather: List<DailyWeather>
): WeatherDto {
return WeatherDto(
resortId = currentWeather.resortId,
resortId = currentWeather.skiResort.resortId,
currentWeather = CurrentWeatherDto.fromEntity(currentWeather),
hourlyWeather = hourlyWeather.map { HourlyWeatherDto.fromEntity(it) },
weeklyWeather = dailyWeather.map { DailyWeatherDto.fromEntity(it) }
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ springdoc:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
weather:
api:
key: p6zNXOJrrBY4cuX7OYtdDMtmR8hiGeUaBLf0z6BXnm/qniV8wB0SuPwBgqKDTKV/24EW7xiRY3DCS21Ess/42Q==

0 comments on commit 754b8b8

Please sign in to comment.