Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.back.koreaTravelGuide

// TODO: 메인 애플리케이션 클래스 - 스프링 부트 시작점 및 환경변수 로딩
import io.github.cdimascio.dotenv.dotenv
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching

@EnableCaching
@SpringBootApplication(scanBasePackages = ["com.back.koreaTravelGuide"])
class KoreaTravelGuideApplication

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.back.koreaTravelGuide.common.logging

import org.slf4j.Logger
import org.slf4j.LoggerFactory

val <T : Any> T.log: Logger
get() = LoggerFactory.getLogger(this::class.java)
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
package com.back.koreaTravelGuide.domain.ai.weather.cache

// TODO: 날씨 정보 캐시 설정 - 12시간 캐시 주기 관리 (필요시 구현)
class WeatherCacheConfig
import org.springframework.context.annotation.Configuration

@Configuration
class WeatherCacheConfig {
// @Bean
// fun cacheManager(connectionFactory: RedisConnectionFactory): RedisCacheManager {
// val config = RedisCacheConfiguration.defaultCacheConfig()
// .entryTtl(Duration.ofHours(12))
// .disableCachingNullValues()
//
// return RedisCacheManager.builder(connectionFactory)
// .cacheDefaults(config)
// .build()
// }
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.back.koreaTravelGuide.domain.ai.weather.client

// TODO: 기상청 API 클라이언트 - HTTP 요청으로 날씨 데이터 조회 및 JSON 파싱
import com.back.koreaTravelGuide.common.logging.log
import com.back.koreaTravelGuide.domain.ai.weather.client.parser.DataParser
import com.back.koreaTravelGuide.domain.ai.weather.client.tools.Tools
import com.back.koreaTravelGuide.domain.ai.weather.dto.LandForecastData
Expand All @@ -25,25 +26,22 @@ class WeatherApiClient(
val stnId = tools.getStnIdFromRegionCode(regionId)
val url = "$apiUrl/getMidFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&stnId=$stnId&tmFc=$baseTime&dataType=JSON"

println("🔮 중기전망조회 API 호출: $url")

return try {
@Suppress("UNCHECKED_CAST")
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
println("📡 중기전망 JSON 응답 수신")

jsonResponse?.let { response ->
// API 오류 응답 체크
val resultCode = dataParser.extractJsonValue(response, "response.header.resultCode") as? String
if (resultCode == "03" || resultCode == "NO_DATA") {
println("⚠️ 기상청 API NO_DATA 오류 - 발표시각을 조정해야 할 수 있습니다")
log.warn("기상청 API NO_DATA 오류 - 발표시각을 조정해야 할 수 있습니다")
return null
}

dataParser.extractJsonValue(response, "response.body.items.item[0].wfSv") as? String
}
} catch (e: Exception) {
println("❌ 중기전망조회 JSON API 오류: ${e.message}")
log.warn("중기전망조회 JSON API 오류: ${e.message}")
null
}
}
Expand All @@ -55,16 +53,13 @@ class WeatherApiClient(
): TemperatureData? {
val url = "$apiUrl/getMidTa?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=JSON"

println("🌡️ 중기기온조회 API 호출: $url")

return try {
@Suppress("UNCHECKED_CAST")
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
println("📡 중기기온 JSON 응답 수신")

jsonResponse?.let { dataParser.parseTemperatureDataFromJson(it) } ?: TemperatureData()
} catch (e: Exception) {
println("❌ 중기기온조회 JSON API 오류: ${e.message}")
log.warn("중기기온조회 JSON API 오류: ${e.message}")
TemperatureData()
}
}
Expand All @@ -76,16 +71,13 @@ class WeatherApiClient(
): LandForecastData? {
val url = "$apiUrl/getMidLandFcst?serviceKey=$serviceKey&numOfRows=10&pageNo=1&regId=$regionId&tmFc=$baseTime&dataType=JSON"

println("🌧️ 중기육상예보조회 API 호출: $url")

return try {
@Suppress("UNCHECKED_CAST")
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
println("📡 중기육상예보 JSON 응답 수신")

jsonResponse?.let { dataParser.parsePrecipitationDataFromJson(it) } ?: LandForecastData()
} catch (e: Exception) {
println("❌ 중기육상예보조회 JSON API 오류: ${e.message}")
log.warn("중기육상예보조회 JSON API 오류: ${e.message}")
LandForecastData()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.back.koreaTravelGuide.domain.ai.weather.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component

@Component
@ConfigurationProperties(prefix = "weather.region")
data class RegionCodeProperties(
var codes: Map<String, String> = emptyMap(),
) {
fun getCodeByLocation(location: String): String {
return codes[location] ?: "11B10101"
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,27 @@
package com.back.koreaTravelGuide.domain.ai.weather.service

// TODO: 날씨 정보 캐싱 서비스 - @Cacheable 어노테이션 기반 캐싱
import com.back.koreaTravelGuide.domain.ai.weather.client.WeatherApiClient
import com.back.koreaTravelGuide.domain.ai.weather.dto.MidForecastDto
import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureAndLandForecastDto
import com.back.koreaTravelGuide.domain.weather.dto.parser.DtoParser
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.scheduling.annotation.Scheduled
import com.back.koreaTravelGuide.domain.ai.weather.service.tools.Tools
import org.springframework.stereotype.Service

@Service
class WeatherService(
private val weatherApiClient: WeatherApiClient,
private val parser: DtoParser,
val weatherServiceCore: WeatherServiceCore,
val tools: Tools,
) {
@Cacheable("weatherMidFore", key = "'전국_' + #actualBaseTime")
fun fetchMidForecast(actualBaseTime: String): List<MidForecastDto>? {
val prefixes = listOf("11B", "11D1", "11D2", "11C2", "11C1", "11F2", "11F1", "11H1", "11H2", "11G")
val midForecastList = mutableListOf<MidForecastDto>()
fun getWeatherForecast(): List<MidForecastDto>? {
val actualBaseTime = tools.getCurrentBaseTime()

for (regionId in prefixes) {
val info = weatherApiClient.fetchMidForecast(regionId, actualBaseTime)
if (info.isNullOrBlank()) return null

val dto = parser.parseMidForecast(regionId, actualBaseTime, info)
midForecastList.add(dto)
}
return midForecastList
return weatherServiceCore.fetchMidForecast(actualBaseTime)
}

@Cacheable("weatherTempAndLandFore", key = "#actualRegionCode + '_' + #actualBaseTime")
fun fetchTemperatureAndLandForecast(
actualRegionCode: String,
actualBaseTime: String,
): List<TemperatureAndLandForecastDto>? {
val tempInfo = weatherApiClient.fetchTemperature(actualRegionCode, actualBaseTime)
val landInfo = weatherApiClient.fetchLandForecast(actualRegionCode, actualBaseTime)

if (tempInfo == null || landInfo == null) return null
fun getTemperatureAndLandForecast(location: String?): List<TemperatureAndLandForecastDto>? {
val actualLocation = location ?: "서울"
val actualRegionCode = tools.getRegionCodeFromLocation(actualLocation)

return parser.parseTemperatureAndLandForecast(actualRegionCode, actualBaseTime, tempInfo, landInfo)
}
val actualBaseTime = tools.getCurrentBaseTime()

@CacheEvict(cacheNames = ["weatherMidFore", "weatherTempAndLandFore"], allEntries = true)
@Scheduled(fixedRate = 43200000) // 12시간마다 (12 * 60 * 60 * 1000)
fun clearWeatherCache() {
println("🗑️ 날씨 캐시 자동 삭제 완료")
return weatherServiceCore.fetchTemperatureAndLandForecast(actualRegionCode, actualBaseTime)
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,61 @@
package com.back.koreaTravelGuide.domain.ai.weather.service

// TODO: 날씨 정보 캐싱 서비스 - @Cacheable 어노테이션 기반 캐싱
import com.back.koreaTravelGuide.common.logging.log
import com.back.koreaTravelGuide.domain.ai.weather.client.WeatherApiClient
import com.back.koreaTravelGuide.domain.ai.weather.dto.MidForecastDto
import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureAndLandForecastDto
import com.back.koreaTravelGuide.domain.ai.weather.service.tools.Tools
import com.back.koreaTravelGuide.domain.weather.dto.parser.DtoParser
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.Cacheable
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service

@Service
class WeatherServiceCore(
val weatherService: WeatherService,
val tools: Tools,
private val weatherApiClient: WeatherApiClient,
private val parser: DtoParser,
) {
fun getWeatherForecast(baseTime: String?): List<MidForecastDto>? {
// baseTime 유효성 검사 - 06시 또는 18시만 허용
val actualBaseTime = tools.validBaseTime(baseTime)
@Cacheable("weatherMidFore", key = "'전국_' + #actualBaseTime")
fun fetchMidForecast(actualBaseTime: String): List<MidForecastDto>? {
val prefixes = listOf("11B", "11D1", "11D2", "11C2", "11C1", "11F2", "11F1", "11H1", "11H2", "11G")
val midForecastList = mutableListOf<MidForecastDto>()

return weatherService.fetchMidForecast(actualBaseTime)
for (regionId in prefixes) {
val info = weatherApiClient.fetchMidForecast(regionId, actualBaseTime)
if (info.isNullOrBlank()) {
log.warn("MidForecast Api error => regionId: $regionId")
continue
}

val dto = parser.parseMidForecast(regionId, actualBaseTime, info)
midForecastList.add(dto)
}
// 리스트가 비어있으면 null 반환 -> api 호출 실패 처리해야함.
if (midForecastList.isEmpty()) return null
return midForecastList
}

fun getTemperatureAndLandForecast(
location: String?,
regionCode: String?,
baseTime: String?,
@Cacheable("weatherTempAndLandFore", key = "#actualRegionCode + '_' + #actualBaseTime")
fun fetchTemperatureAndLandForecast(
actualRegionCode: String,
actualBaseTime: String,
): List<TemperatureAndLandForecastDto>? {
val actualLocation = location ?: "서울"
val actualRegionCode = regionCode ?: tools.getRegionCodeFromLocation(actualLocation)
val tempInfo = weatherApiClient.fetchTemperature(actualRegionCode, actualBaseTime)
val landInfo = weatherApiClient.fetchLandForecast(actualRegionCode, actualBaseTime)

// 둘 중 하나라도 null이면 null 반환 -> api 호출 실패 처리해야함.
if (tempInfo == null || landInfo == null) {
log.warn("Temp, Land Api error => actualRegionCode: $actualRegionCode")
return null
}

// baseTime 유효성 검사 - 06시 또는 18시만 허용
val actualBaseTime = tools.validBaseTime(baseTime)
return parser.parseTemperatureAndLandForecast(actualRegionCode, actualBaseTime, tempInfo, landInfo)
}

return weatherService.fetchTemperatureAndLandForecast(actualRegionCode, actualBaseTime)
// 인스턴스가 여러개 있는 상황에서는 중복 삭제될 수 있음. 나중에 분산 락 고려
@CacheEvict(cacheNames = ["weatherMidFore", "weatherTempAndLandFore"], allEntries = true)
@Scheduled(fixedRate = 43200000) // 12시간마다 (12 * 60 * 60 * 1000)
fun clearWeatherCache() {
log.info("clearWeatherCache")
}
}
Loading