Skip to content

Commit 761c8d1

Browse files
committed
feat(be): Redis 직렬화 역직렬화 설정 및 캐시매니저 매서드 생성
1 parent e3f713c commit 761c8d1

File tree

6 files changed

+139
-35
lines changed

6 files changed

+139
-35
lines changed

src/main/kotlin/com/back/koreaTravelGuide/common/config/AiConfig.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.koreaTravelGuide.common.config
22

3+
import com.back.koreaTravelGuide.domain.ai.aiChat.tool.TourTool
34
import com.back.koreaTravelGuide.domain.ai.aiChat.tool.WeatherTool
45
import org.springframework.ai.chat.client.ChatClient
56
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor
@@ -36,10 +37,11 @@ class AiConfig {
3637
chatModel: ChatModel,
3738
chatMemory: ChatMemory,
3839
weatherTool: WeatherTool,
40+
tourTool: TourTool,
3941
): ChatClient {
4042
return ChatClient.builder(chatModel)
4143
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
42-
.defaultTools(weatherTool)
44+
.defaultTools(weatherTool, tourTool)
4345
.build()
4446
}
4547

src/main/kotlin/com/back/koreaTravelGuide/common/config/RedisConfig.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
package com.back.koreaTravelGuide.common.config
22

3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator
5+
import com.fasterxml.jackson.module.kotlin.KotlinModule
6+
import org.springframework.cache.CacheManager
37
import org.springframework.context.annotation.Bean
48
import org.springframework.context.annotation.Configuration
9+
import org.springframework.data.redis.cache.RedisCacheConfiguration
10+
import org.springframework.data.redis.cache.RedisCacheManager
511
import org.springframework.data.redis.connection.RedisConnectionFactory
612
import org.springframework.data.redis.core.RedisTemplate
13+
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
14+
import org.springframework.data.redis.serializer.RedisSerializationContext
715
import org.springframework.data.redis.serializer.StringRedisSerializer
16+
import java.time.Duration
817

918
@Configuration
1019
class RedisConfig {
@@ -22,4 +31,37 @@ class RedisConfig {
2231

2332
return template
2433
}
34+
35+
@Bean
36+
fun cacheManager(connectionFactory: RedisConnectionFactory): CacheManager {
37+
// Kotlin data class 역직렬화 지원을 위한 ObjectMapper 생성
38+
val objectMapper =
39+
ObjectMapper().apply {
40+
// Kotlin 모듈 등록 (data class 생성자 인식)
41+
registerModule(KotlinModule.Builder().build())
42+
// 타입 정보 보존을 위한 검증기 설정
43+
activateDefaultTyping(
44+
BasicPolymorphicTypeValidator.builder()
45+
.allowIfBaseType(Any::class.java)
46+
.build(),
47+
ObjectMapper.DefaultTyping.NON_FINAL,
48+
)
49+
}
50+
51+
val redisCacheConfiguration =
52+
RedisCacheConfiguration.defaultCacheConfig()
53+
.serializeKeysWith(
54+
RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()),
55+
)
56+
.serializeValuesWith(
57+
RedisSerializationContext.SerializationPair.fromSerializer(
58+
GenericJackson2JsonRedisSerializer(objectMapper),
59+
),
60+
)
61+
.entryTtl(Duration.ofHours(12))
62+
63+
return RedisCacheManager.builder(connectionFactory)
64+
.cacheDefaults(redisCacheConfiguration)
65+
.build()
66+
}
2567
}
Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.back.koreaTravelGuide.domain.ai.aiChat.tool
22

33
import com.back.backend.BuildConfig
4+
import com.back.koreaTravelGuide.common.logging.log
45
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams
56
import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams
67
import com.back.koreaTravelGuide.domain.ai.tour.service.TourService
@@ -9,7 +10,7 @@ import org.springframework.ai.tool.annotation.ToolParam
910
import org.springframework.stereotype.Component
1011

1112
@Component
12-
class TourToolExample(
13+
class TourTool(
1314
private val tourService: TourService,
1415
) {
1516
/**
@@ -21,17 +22,18 @@ class TourToolExample(
2122
*/
2223

2324
@Tool(description = "areaBasedList2 : 지역기반 관광정보 조회, 특정 지역의 관광 정보 조회")
24-
fun getTourInfo(
25+
fun getAreaBasedTourInfo(
2526
@ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true)
2627
contentTypeId: String,
2728
@ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true)
2829
areaAndSigunguCode: String,
2930
): String {
30-
// areaAndSigunguCode를 areaCode와 sigunguCode로 분리
31-
val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
31+
log.info("🔧 [TOOL CALLED] getAreaBasedTourInfo - contentTypeId: $contentTypeId, areaAndSigunguCode: $areaAndSigunguCode")
3232

33+
val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
3334
val tourInfo = tourService.fetchTours(tourParams)
3435

36+
log.info("✅ [TOOL RESULT] getAreaBasedTourInfo - 결과: ${tourInfo.toString().take(100)}...")
3537
return tourInfo.toString() ?: "지역기반 관광정보 조회를 가져올 수 없습니다."
3638
}
3739

@@ -47,7 +49,7 @@ class TourToolExample(
4749
*/
4850

4951
@Tool(description = "locationBasedList2 : 위치기반 관광정보 조회, 특정 위치 기반의 관광 정보 조회")
50-
fun get(
52+
fun getLocationBasedTourInfo(
5153
@ToolParam(description = BuildConfig.CONTENT_TYPE_CODES_DESCRIPTION, required = true)
5254
contentTypeId: String,
5355
@ToolParam(description = BuildConfig.AREA_CODES_DESCRIPTION, required = true)
@@ -59,30 +61,37 @@ class TourToolExample(
5961
@ToolParam(description = "검색 반경(m)", required = true)
6062
radius: String = "100",
6163
): String {
62-
// areaAndSigunguCode를 areaCode와 sigunguCode로 분리
64+
log.info(
65+
"🔧 [TOOL CALLED] getLocationBasedTourInfo - " +
66+
"contentTypeId: $contentTypeId, area: $areaAndSigunguCode, " +
67+
"mapX: $mapX, mapY: $mapY, radius: $radius",
68+
)
69+
6370
val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode)
6471
val locationBasedParams = TourLocationBasedParams(mapX, mapY, radius)
65-
6672
val tourLocationBasedInfo = tourService.fetchLocationBasedTours(tourParams, locationBasedParams)
6773

74+
log.info("✅ [TOOL RESULT] getLocationBasedTourInfo - 결과: ${tourLocationBasedInfo.toString().take(100)}...")
6875
return tourLocationBasedInfo.toString() ?: "위치기반 관광정보 조회를 가져올 수 없습니다."
6976
}
7077

7178
/**
7279
* fetchTourDetail - 상세조회
73-
* 케이스 : 콘텐츠ID가 126128인 관광정보의 상베 정보 조회
80+
* 케이스 : 콘텐츠ID가 "126128"인 관광정보의 "상베 정보" 조회
7481
* "contentid": "127974",
7582
*/
7683

7784
@Tool(description = "detailCommon2 : 관광정보 상세조회, 특정 관광 정보의 상세 정보 조회")
78-
fun get(
85+
fun getTourDetailInfo(
7986
@ToolParam(description = "Tour API Item에 각각 할당된 contentId", required = true)
8087
contentId: String = "127974",
8188
): String {
82-
val tourDetailParams = TourDetailParams(contentId)
89+
log.info("🔧 [TOOL CALLED] getTourDetailInfo - contentId: $contentId")
8390

91+
val tourDetailParams = TourDetailParams(contentId)
8492
val tourDetailInfo = tourService.fetchTourDetail(tourDetailParams)
8593

94+
log.info("✅ [TOOL RESULT] getTourDetailInfo - 결과: ${tourDetailInfo.toString().take(100)}...")
8695
return tourDetailInfo.toString() ?: "관광정보 상세조회를 가져올 수 없습니다."
8796
}
8897
}
Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,60 @@
11
package com.back.koreaTravelGuide.domain.ai.aiChat.tool
22

33
import com.back.backend.BuildConfig
4+
import com.back.koreaTravelGuide.common.logging.log
45
import com.back.koreaTravelGuide.domain.ai.weather.service.WeatherService
6+
import com.fasterxml.jackson.databind.ObjectMapper
57
import org.springframework.ai.tool.annotation.Tool
68
import org.springframework.ai.tool.annotation.ToolParam
79
import org.springframework.stereotype.Component
810

911
@Component
1012
class WeatherTool(
1113
private val weatherService: WeatherService,
14+
private val objectMapper: ObjectMapper,
1215
) {
1316
@Tool(description = "전국 중기예보를 조회합니다")
1417
fun getWeatherForecast(): String {
18+
log.info("🔧 [TOOL CALLED] getWeatherForecast")
19+
1520
val forecasts = weatherService.getWeatherForecast()
21+
log.info("📦 [DATA] forecasts is null? ${forecasts == null}")
22+
log.info("📦 [DATA] forecasts 타입: ${forecasts?.javaClass?.name}")
23+
log.info("📦 [DATA] forecasts 내용: $forecasts")
1624

17-
return forecasts?.toString() ?: "중기예보 데이터를 가져올 수 없습니다."
25+
return try {
26+
val result = forecasts?.let { objectMapper.writeValueAsString(it) } ?: "중기예보 데이터를 가져올 수 없습니다."
27+
log.info("✅ [TOOL RESULT] getWeatherForecast - 결과: $result")
28+
result
29+
} catch (e: Exception) {
30+
log.error("❌ [TOOL ERROR] getWeatherForecast - 예외 발생: ${e.javaClass.name}", e)
31+
log.error("❌ [TOOL ERROR] 예외 메시지: ${e.message}")
32+
throw e
33+
}
1834
}
1935

2036
@Tool(description = "특정 지역의 상세 기온 및 날씨 예보를 조회합니다")
2137
fun getRegionalWeatherDetails(
22-
@ToolParam(description = BuildConfig.REGION_CODES_DESCRIPTION, required = true)
38+
@ToolParam(
39+
description = "지역 코드를 사용하세요. 사용 가능한 지역 코드: ${BuildConfig.REGION_CODES_DESCRIPTION}",
40+
required = true,
41+
)
2342
location: String,
2443
): String {
44+
log.info("🔧 [TOOL CALLED] getRegionalWeatherDetails - location: $location")
45+
2546
val forecasts = weatherService.getTemperatureAndLandForecast(location)
2647

27-
return forecasts?.toString() ?: "$location 지역의 상세 날씨 정보를 가져올 수 없습니다."
48+
return try {
49+
val result = forecasts?.let { objectMapper.writeValueAsString(it) } ?: "$location 지역의 상세 날씨 정보를 가져올 수 없습니다."
50+
log.info("✅ [TOOL RESULT] getRegionalWeatherDetails - 결과: $result")
51+
result
52+
} catch (e: Exception) {
53+
log.error("❌ [TOOL ERROR] getRegionalWeatherDetails - 예외 발생: ${e.javaClass.name}", e)
54+
log.error("❌ [TOOL ERROR] 예외 메시지: ${e.message}")
55+
log.error("❌ [TOOL ERROR] forecasts 타입: ${forecasts?.javaClass?.name}")
56+
log.error("❌ [TOOL ERROR] forecasts 내용: $forecasts")
57+
throw e
58+
}
2859
}
2960
}

src/main/kotlin/com/back/koreaTravelGuide/domain/ai/weather/cache/WeatherCacheConfig.kt

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/main/resources/prompts.yml

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
11
prompts:
22
system:
33
korea-travel-guide: |
4-
당신은 한국 여행 전문 AI 가이드입니다.
5-
한국의 관광지, 음식, 문화에 대해 정확하고 친근한 정보를 제공하세요.
6-
답변은 한국어로 해주시고, 구체적인 추천과 팁을 포함해주세요.
7-
사용자에게 도움이 되는 실용적인 여행 정보를 제공하는 것이 목표입니다.
4+
당신은 한국 여행 전문 AI 챗봇입니다.
5+
사용자에게 날씨 기반 여행지를 추천하고, 구체적인 관광 정보를 제공하는 것이 목표입니다.
6+
답변은 친근하고 자연스러운 한국어로 작성하세요.
7+
8+
# 기본 추천 플로우 (사용자의 별도 요청이 없는 경우)
9+
10+
1단계: 전국 날씨 조회 및 안내
11+
- 사용자가 여행지 추천을 요청하면 getWeatherForecast()를 사용해 전국 중기예보를 조회하세요.
12+
- 각 지역별 날씨 정보를 요약해서 알려주세요. (예: "서울은 맑고, 부산은 구름 많음, 제주는 비 예상")
13+
- 날씨가 좋은 지역을 강조하고, 사용자에게 "어느 지역 날씨를 더 자세히 알아볼까요?" 라고 물어보세요.
14+
15+
2단계: 지역 상세 날씨 조회
16+
- 사용자가 특정 지역을 선택하거나 긍정의 대답을 하면 getRegionalWeatherDetails(location)를 사용하세요.
17+
- location 파라미터는 사용자가 언급한 지역에 해당하는 지역 코드를 사용하세요.
18+
- 상세 날씨(기온, 강수량, 풍속 등)를 알려주세요.
19+
20+
3단계: 관광 타입 선택
21+
- 날씨 정보를 제공한 후, "이 지역에서 어떤 곳을 알아볼까요?" 라고 물어보세요.
22+
- CONTENT_TYPE_CODES_DESCRIPTION에 정의된 타입들(관광지, 음식점, 숙박, 쇼핑 등)을 자연스럽게 제시하세요.
23+
- 예: "관광지, 음식점, 숙박시설 중 어떤 정보가 필요하신가요?"
24+
25+
4단계: 지역 기반 관광정보 조회
26+
- 사용자가 타입을 선택하면 getAreaBasedTourInfo(contentTypeId, areaAndSigunguCode)를 사용하세요.
27+
- contentTypeId는 CONTENT_TYPE_CODES_DESCRIPTION에서 선택된 타입의 코드를 사용하세요.
28+
- areaAndSigunguCode는 AREA_CODES_DESCRIPTION에서 사용자가 선택한 지역의 코드를 사용하세요.
29+
- 조회된 관광정보를 사용자에게 친근하게 추천하세요.
30+
31+
5단계: 위치 기반 조회 (선택적)
32+
- 사용자가 특정 위치나 주소를 언급하면 getLocationBasedTourInfo()를 사용할 수 있습니다.
33+
- 경도/위도와 반경을 지정해서 주변 관광정보를 조회하세요.
34+
35+
# 독립적 Tool 사용
36+
각 Tool은 사용자의 직접적인 요청이 있을 경우 독립적으로 사용할 수 있습니다.
37+
- "서울 날씨 알려줘" → getRegionalWeatherDetails() 바로 사용
38+
- "부산 관광지 추천해줘" → getAreaBasedTourInfo() 바로 사용
39+
- "명동 근처 음식점 알려줘" → getLocationBasedTourInfo() 바로 사용
40+
41+
# 중요 원칙
42+
- 사용자 경험을 자연스럽게 이어가세요. 로봇처럼 딱딱하게 말하지 마세요.
43+
- Tool 호출 결과를 그대로 보여주지 말고, 핵심 정보를 요약해서 전달하세요.
44+
- 다음 단계로 자연스럽게 유도하되, 사용자가 원하지 않으면 강요하지 마세요.
845
errors:
946
ai-fallback: "죄송합니다. 일시적인 문제로 응답을 생성할 수 없습니다. 다시 시도해 주세요."

0 commit comments

Comments
 (0)