Skip to content

Commit 1e1cb14

Browse files
JIWONKIMSclaude
andauthored
feat(be) :refeat, add test(#50) (#56)
* feat(be): Add WeatherApiClientTest with ktlint compliance - Add comprehensive WeatherApiClientTest for Weather API integration testing - Fix ktlint violations across weather module files: - Remove unnecessary blank lines and add proper spacing - Replace wildcard imports with explicit imports - Add trailing commas and fix comment formatting - Add missing newlines at end of files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(be): Apply ktlint formatting fixes - Fix missing newlines and formatting issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix(be): Update imports and remove unused dependencies - Fix wildcard imports in AiChatController - Update WeatherApiClientTest imports - Remove unused WeatherService dependency from AiChatController 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent a940925 commit 1e1cb14

File tree

14 files changed

+407
-407
lines changed

14 files changed

+407
-407
lines changed

src/main/kotlin/com/back/koreaTravelGuide/KoreaTravelGuideApplication.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package com.back.koreaTravelGuide
22

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

8+
@EnableCaching
89
@SpringBootApplication(scanBasePackages = ["com.back.koreaTravelGuide"])
910
class KoreaTravelGuideApplication
1011

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.back.koreaTravelGuide.common.logging
2+
3+
import org.slf4j.Logger
4+
import org.slf4j.LoggerFactory
5+
6+
val <T : Any> T.log: Logger
7+
get() = LoggerFactory.getLogger(this::class.java)
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
package com.back.koreaTravelGuide.domain.ai.weather.cache
22

3-
// TODO: 날씨 정보 캐시 설정 - 12시간 캐시 주기 관리 (필요시 구현)
4-
class WeatherCacheConfig
3+
import org.springframework.context.annotation.Configuration
4+
5+
@Configuration
6+
class WeatherCacheConfig {
7+
// @Bean
8+
// fun cacheManager(connectionFactory: RedisConnectionFactory): RedisCacheManager {
9+
// val config = RedisCacheConfiguration.defaultCacheConfig()
10+
// .entryTtl(Duration.ofHours(12))
11+
// .disableCachingNullValues()
12+
//
13+
// return RedisCacheManager.builder(connectionFactory)
14+
// .cacheDefaults(config)
15+
// .build()
16+
// }
17+
}

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/weather/client/WeatherApiClient.kt

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.back.koreaTravelGuide.domain.ai.weather.client
22

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

28-
println("🔮 중기전망조회 API 호출: $url")
29-
3029
return try {
3130
@Suppress("UNCHECKED_CAST")
3231
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
33-
println("📡 중기전망 JSON 응답 수신")
3432

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

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

58-
println("🌡️ 중기기온조회 API 호출: $url")
59-
6056
return try {
6157
@Suppress("UNCHECKED_CAST")
6258
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
63-
println("📡 중기기온 JSON 응답 수신")
6459

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

79-
println("🌧️ 중기육상예보조회 API 호출: $url")
80-
8174
return try {
8275
@Suppress("UNCHECKED_CAST")
8376
val jsonResponse = restTemplate.getForObject(url, Map::class.java) as? Map<String, Any>
84-
println("📡 중기육상예보 JSON 응답 수신")
8577

8678
jsonResponse?.let { dataParser.parsePrecipitationDataFromJson(it) } ?: LandForecastData()
8779
} catch (e: Exception) {
88-
println("중기육상예보조회 JSON API 오류: ${e.message}")
80+
log.warn("중기육상예보조회 JSON API 오류: ${e.message}")
8981
LandForecastData()
9082
}
9183
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.back.koreaTravelGuide.domain.ai.weather.config
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties
4+
import org.springframework.stereotype.Component
5+
6+
@Component
7+
@ConfigurationProperties(prefix = "weather.region")
8+
data class RegionCodeProperties(
9+
var codes: Map<String, String> = emptyMap(),
10+
) {
11+
fun getCodeByLocation(location: String): String {
12+
return codes[location] ?: "11B10101"
13+
}
14+
}
Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,27 @@
11
package com.back.koreaTravelGuide.domain.ai.weather.service
22

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

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

23-
for (regionId in prefixes) {
24-
val info = weatherApiClient.fetchMidForecast(regionId, actualBaseTime)
25-
if (info.isNullOrBlank()) return null
26-
27-
val dto = parser.parseMidForecast(regionId, actualBaseTime, info)
28-
midForecastList.add(dto)
29-
}
30-
return midForecastList
16+
return weatherServiceCore.fetchMidForecast(actualBaseTime)
3117
}
3218

33-
@Cacheable("weatherTempAndLandFore", key = "#actualRegionCode + '_' + #actualBaseTime")
34-
fun fetchTemperatureAndLandForecast(
35-
actualRegionCode: String,
36-
actualBaseTime: String,
37-
): List<TemperatureAndLandForecastDto>? {
38-
val tempInfo = weatherApiClient.fetchTemperature(actualRegionCode, actualBaseTime)
39-
val landInfo = weatherApiClient.fetchLandForecast(actualRegionCode, actualBaseTime)
40-
41-
if (tempInfo == null || landInfo == null) return null
19+
fun getTemperatureAndLandForecast(location: String?): List<TemperatureAndLandForecastDto>? {
20+
val actualLocation = location ?: "서울"
21+
val actualRegionCode = tools.getRegionCodeFromLocation(actualLocation)
4222

43-
return parser.parseTemperatureAndLandForecast(actualRegionCode, actualBaseTime, tempInfo, landInfo)
44-
}
23+
val actualBaseTime = tools.getCurrentBaseTime()
4524

46-
@CacheEvict(cacheNames = ["weatherMidFore", "weatherTempAndLandFore"], allEntries = true)
47-
@Scheduled(fixedRate = 43200000) // 12시간마다 (12 * 60 * 60 * 1000)
48-
fun clearWeatherCache() {
49-
println("🗑️ 날씨 캐시 자동 삭제 완료")
25+
return weatherServiceCore.fetchTemperatureAndLandForecast(actualRegionCode, actualBaseTime)
5026
}
5127
}
Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,61 @@
11
package com.back.koreaTravelGuide.domain.ai.weather.service
22

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

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

18-
return weatherService.fetchMidForecast(actualBaseTime)
23+
for (regionId in prefixes) {
24+
val info = weatherApiClient.fetchMidForecast(regionId, actualBaseTime)
25+
if (info.isNullOrBlank()) {
26+
log.warn("MidForecast Api error => regionId: $regionId")
27+
continue
28+
}
29+
30+
val dto = parser.parseMidForecast(regionId, actualBaseTime, info)
31+
midForecastList.add(dto)
32+
}
33+
// 리스트가 비어있으면 null 반환 -> api 호출 실패 처리해야함.
34+
if (midForecastList.isEmpty()) return null
35+
return midForecastList
1936
}
2037

21-
fun getTemperatureAndLandForecast(
22-
location: String?,
23-
regionCode: String?,
24-
baseTime: String?,
38+
@Cacheable("weatherTempAndLandFore", key = "#actualRegionCode + '_' + #actualBaseTime")
39+
fun fetchTemperatureAndLandForecast(
40+
actualRegionCode: String,
41+
actualBaseTime: String,
2542
): List<TemperatureAndLandForecastDto>? {
26-
val actualLocation = location ?: "서울"
27-
val actualRegionCode = regionCode ?: tools.getRegionCodeFromLocation(actualLocation)
43+
val tempInfo = weatherApiClient.fetchTemperature(actualRegionCode, actualBaseTime)
44+
val landInfo = weatherApiClient.fetchLandForecast(actualRegionCode, actualBaseTime)
45+
46+
// 둘 중 하나라도 null이면 null 반환 -> api 호출 실패 처리해야함.
47+
if (tempInfo == null || landInfo == null) {
48+
log.warn("Temp, Land Api error => actualRegionCode: $actualRegionCode")
49+
return null
50+
}
2851

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

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

0 commit comments

Comments
 (0)