diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/config/RedisConfig.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/config/RedisConfig.kt index 4885ac3..d60a2a6 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/config/RedisConfig.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/config/RedisConfig.kt @@ -1,7 +1,10 @@ package com.back.koreaTravelGuide.common.config +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailResponse +import com.back.koreaTravelGuide.domain.ai.tour.dto.TourResponse +import com.back.koreaTravelGuide.domain.ai.weather.dto.MidForecastDto +import com.back.koreaTravelGuide.domain.ai.weather.dto.TemperatureAndLandForecastDto import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator import com.fasterxml.jackson.module.kotlin.KotlinModule import org.springframework.cache.CacheManager import org.springframework.context.annotation.Bean @@ -10,13 +13,20 @@ import org.springframework.data.redis.cache.RedisCacheConfiguration import org.springframework.data.redis.cache.RedisCacheManager import org.springframework.data.redis.connection.RedisConnectionFactory import org.springframework.data.redis.core.RedisTemplate -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer import org.springframework.data.redis.serializer.RedisSerializationContext import org.springframework.data.redis.serializer.StringRedisSerializer import java.time.Duration @Configuration class RedisConfig { + @Bean + fun objectMapper(): ObjectMapper = + ObjectMapper().apply { + // Kotlin 모듈 등록 (data class 생성자 인식) + registerModule(KotlinModule.Builder().build()) + } + @Bean fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate { val template = RedisTemplate() @@ -33,35 +43,76 @@ class RedisConfig { } @Bean - fun cacheManager(connectionFactory: RedisConnectionFactory): CacheManager { - // Kotlin data class 역직렬화 지원을 위한 ObjectMapper 생성 - val objectMapper = - ObjectMapper().apply { - // Kotlin 모듈 등록 (data class 생성자 인식) - registerModule(KotlinModule.Builder().build()) - // 타입 정보 보존을 위한 검증기 설정 - activateDefaultTyping( - BasicPolymorphicTypeValidator.builder() - .allowIfBaseType(Any::class.java) - .build(), - ObjectMapper.DefaultTyping.NON_FINAL, + fun cacheManager( + connectionFactory: RedisConnectionFactory, + objectMapper: ObjectMapper, + ): CacheManager { + + // 각 캐시 타입별 Serializer 생성 + val tourResponseSerializer = Jackson2JsonRedisSerializer(objectMapper, TourResponse::class.java) + val tourDetailResponseSerializer = Jackson2JsonRedisSerializer(objectMapper, TourDetailResponse::class.java) + + // List 타입을 위한 JavaType 생성 + val midForecastListType = + objectMapper.typeFactory.constructCollectionType( + List::class.java, + MidForecastDto::class.java, + ) + val midForecastListSerializer = Jackson2JsonRedisSerializer>(objectMapper, midForecastListType) + + // List 타입을 위한 JavaType 생성 + val tempAndLandListType = + objectMapper.typeFactory.constructCollectionType( + List::class.java, + TemperatureAndLandForecastDto::class.java, + ) + val tempAndLandListSerializer = Jackson2JsonRedisSerializer>(objectMapper, tempAndLandListType) + + // 공통 키 Serializer + val keySerializer = RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()) + + // Tour 관련 캐시 설정 (TourResponse 타입) + val tourResponseConfig = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(keySerializer) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(tourResponseSerializer), ) - } + .entryTtl(Duration.ofHours(12)) - val redisCacheConfiguration = + // Tour 상세 캐시 설정 (TourDetailResponse 타입) + val tourDetailConfig = RedisCacheConfiguration.defaultCacheConfig() - .serializeKeysWith( - RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer()), + .serializeKeysWith(keySerializer) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(tourDetailResponseSerializer), ) + .entryTtl(Duration.ofHours(12)) + + // 중기예보 캐시 설정 (List 타입) + val midForecastConfig = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(keySerializer) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(midForecastListSerializer), + ) + .entryTtl(Duration.ofHours(12)) + + // 기온/육상 예보 캐시 설정 (List 타입) + val tempAndLandConfig = + RedisCacheConfiguration.defaultCacheConfig() + .serializeKeysWith(keySerializer) .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer( - GenericJackson2JsonRedisSerializer(objectMapper), - ), + RedisSerializationContext.SerializationPair.fromSerializer(tempAndLandListSerializer), ) .entryTtl(Duration.ofHours(12)) return RedisCacheManager.builder(connectionFactory) - .cacheDefaults(redisCacheConfiguration) + .withCacheConfiguration("tourAreaBased", tourResponseConfig) + .withCacheConfiguration("tourLocationBased", tourResponseConfig) + .withCacheConfiguration("tourDetail", tourDetailConfig) + .withCacheConfiguration("weatherMidFore", midForecastConfig) + .withCacheConfiguration("weatherTempAndLandFore", tempAndLandConfig) .build() } } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt index 3d123ec..5e785fb 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt @@ -21,7 +21,8 @@ class SecurityConfig( ) { @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { - val isDev = environment.activeProfiles.contains("dev") + val isDev = environment.getProperty("spring.profiles.active")?.contains("dev") == true || + environment.activeProfiles.contains("dev") http { csrf { disable() } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/controller/AiChatController.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/controller/AiChatController.kt index bfbf1ba..c537f4f 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/controller/AiChatController.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/controller/AiChatController.kt @@ -1,6 +1,7 @@ package com.back.koreaTravelGuide.domain.ai.aiChat.controller import com.back.koreaTravelGuide.common.ApiResponse +import com.back.koreaTravelGuide.common.security.getUserId import com.back.koreaTravelGuide.domain.ai.aiChat.dto.AiChatRequest import com.back.koreaTravelGuide.domain.ai.aiChat.dto.AiChatResponse import com.back.koreaTravelGuide.domain.ai.aiChat.dto.SessionMessagesResponse @@ -9,6 +10,7 @@ import com.back.koreaTravelGuide.domain.ai.aiChat.dto.UpdateSessionTitleRequest import com.back.koreaTravelGuide.domain.ai.aiChat.dto.UpdateSessionTitleResponse import com.back.koreaTravelGuide.domain.ai.aiChat.service.AiChatService import org.springframework.http.ResponseEntity +import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping @@ -16,7 +18,6 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -26,8 +27,9 @@ class AiChatController( ) { @GetMapping("/sessions") fun getSessions( - @RequestParam userId: Long, + authentication: Authentication?, ): ResponseEntity>> { + val userId = authentication?.getUserId() ?: 1L // dev 모드: 기본 userId=1 val sessions = aiChatService.getSessions(userId).map { SessionsResponse.from(it) @@ -37,8 +39,9 @@ class AiChatController( @PostMapping("/sessions") fun createSession( - @RequestParam userId: Long, + authentication: Authentication?, ): ResponseEntity> { + val userId = authentication?.getUserId() ?: 1L // dev 모드: 기본 userId=1 val session = aiChatService.createSession(userId) val response = SessionsResponse.from(session) return ResponseEntity.ok(ApiResponse("채팅방이 성공적으로 생성되었습니다.", response)) @@ -47,8 +50,9 @@ class AiChatController( @DeleteMapping("/sessions/{sessionId}") fun deleteSession( @PathVariable sessionId: Long, - @RequestParam userId: Long, + authentication: Authentication?, ): ResponseEntity> { + val userId = authentication?.getUserId() ?: 1L // dev 모드: 기본 userId=1 aiChatService.deleteSession(sessionId, userId) return ResponseEntity.ok(ApiResponse("채팅방이 성공적으로 삭제되었습니다.")) } @@ -56,8 +60,9 @@ class AiChatController( @GetMapping("/sessions/{sessionId}/messages") fun getSessionMessages( @PathVariable sessionId: Long, - @RequestParam userId: Long, + authentication: Authentication?, ): ResponseEntity>> { + val userId = authentication?.getUserId() ?: 1L // dev 모드: 기본 userId=1 val messages = aiChatService.getSessionMessages(sessionId, userId) val response = messages.map { @@ -69,9 +74,10 @@ class AiChatController( @PostMapping("/sessions/{sessionId}/messages") fun sendMessage( @PathVariable sessionId: Long, - @RequestParam userId: Long, + authentication: Authentication?, @RequestBody request: AiChatRequest, ): ResponseEntity> { + val userId = authentication?.getUserId() ?: 1L // dev 모드: 기본 userId=1 val (userMessage, aiMessage) = aiChatService.sendMessage(sessionId, userId, request.message) val response = AiChatResponse( @@ -84,9 +90,10 @@ class AiChatController( @PatchMapping("/sessions/{sessionId}/title") fun updateSessionTitle( @PathVariable sessionId: Long, - @RequestParam userId: Long, + authentication: Authentication?, @RequestBody request: UpdateSessionTitleRequest, ): ResponseEntity> { + val userId = authentication?.getUserId() ?: 1L // dev 모드: 기본 userId=1 val updatedSession = aiChatService.updateSessionTitle(sessionId, userId, request.newTitle) val response = UpdateSessionTitleResponse( diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt index fa792f0..7dc3194 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/tool/TourTool.kt @@ -5,6 +5,7 @@ import com.back.koreaTravelGuide.common.logging.log import com.back.koreaTravelGuide.domain.ai.tour.dto.TourDetailParams import com.back.koreaTravelGuide.domain.ai.tour.dto.TourLocationBasedParams import com.back.koreaTravelGuide.domain.ai.tour.service.TourService +import com.fasterxml.jackson.databind.ObjectMapper import org.springframework.ai.tool.annotation.Tool import org.springframework.ai.tool.annotation.ToolParam import org.springframework.stereotype.Component @@ -12,6 +13,7 @@ import org.springframework.stereotype.Component @Component class TourTool( private val tourService: TourService, + private val objectMapper: ObjectMapper, ) { /** * fetchTours - 지역기반 관광정보 조회 @@ -47,8 +49,14 @@ class TourTool( val tourParams = tourService.parseParams(contentTypeId, areaAndSigunguCode) val tourInfo = tourService.fetchTours(tourParams) - log.info("✅ [TOOL RESULT] getAreaBasedTourInfo - 결과: ${tourInfo.toString().take(100)}...") - return tourInfo.toString() ?: "지역기반 관광정보 조회를 가져올 수 없습니다." + return try { + val result = tourInfo.let { objectMapper.writeValueAsString(it) } + log.info("✅ [TOOL RESULT] getAreaBasedTourInfo - 결과: ${result.take(100)}...") + result + } catch (e: Exception) { + log.error("❌ [TOOL ERROR] getAreaBasedTourInfo - 예외 발생", e) + "지역기반 관광정보 조회를 가져올 수 없습니다." + } } /** @@ -98,8 +106,14 @@ class TourTool( val locationBasedParams = TourLocationBasedParams(mapX, mapY, radius) val tourLocationBasedInfo = tourService.fetchLocationBasedTours(tourParams, locationBasedParams) - log.info("✅ [TOOL RESULT] getLocationBasedTourInfo - 결과: ${tourLocationBasedInfo.toString().take(100)}...") - return tourLocationBasedInfo.toString() ?: "위치기반 관광정보 조회를 가져올 수 없습니다." + return try { + val result = tourLocationBasedInfo.let { objectMapper.writeValueAsString(it) } + log.info("✅ [TOOL RESULT] getLocationBasedTourInfo - 결과: ${result.take(100)}...") + result + } catch (e: Exception) { + log.error("❌ [TOOL ERROR] getLocationBasedTourInfo - 예외 발생", e) + "위치기반 관광정보 조회를 가져올 수 없습니다." + } } /** @@ -123,7 +137,13 @@ class TourTool( val tourDetailParams = TourDetailParams(contentId) val tourDetailInfo = tourService.fetchTourDetail(tourDetailParams) - log.info("✅ [TOOL RESULT] getTourDetailInfo - 결과: ${tourDetailInfo.toString().take(100)}...") - return tourDetailInfo.toString() ?: "관광정보 상세조회를 가져올 수 없습니다." + return try { + val result = tourDetailInfo.let { objectMapper.writeValueAsString(it) } + log.info("✅ [TOOL RESULT] getTourDetailInfo - 결과: ${result.take(100)}...") + result + } catch (e: Exception) { + log.error("❌ [TOOL ERROR] getTourDetailInfo - 예외 발생", e) + "관광정보 상세조회를 가져올 수 없습니다." + } } } diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt index f058aaa..c250e6d 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/tour/dto/TourResponse.kt @@ -1,5 +1,7 @@ package com.back.koreaTravelGuide.domain.ai.tour.dto +import com.fasterxml.jackson.annotation.JsonProperty + /** * 9.27 양현준 * 관광 정보 응답 DTO @@ -41,7 +43,9 @@ data class TourItem( // 시군구코드 val sigunguCode: String?, // 법정동 시도 코드 + @get:JsonProperty("lDongRegnCd") val lDongRegnCd: String?, // 법정동 시군구 코드 + @get:JsonProperty("lDongSignguCd") val lDongSignguCd: String?, )