diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/config/DevConfig.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/config/DevConfig.kt index 33d8b3a..b962c68 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/config/DevConfig.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/config/DevConfig.kt @@ -44,7 +44,7 @@ class DevConfig { println(" πŸ“Š Actuator Info: $baseUrl/actuator/info") println("\nπŸ”§ λ°μ΄ν„°λ² μ΄μŠ€ 접속 정보 (H2 Console용):") - println(" JDBC URL: jdbc:h2:mem:korea_travel_guide") + println(" JDBC URL: jdbc:h2:mem:testdb") println(" Username: sa") println(" Password: (λΉ„μ–΄μžˆμŒ)") diff --git a/src/main/kotlin/com/back/koreaTravelGuide/common/exception/GlobalExceptionHandler.kt b/src/main/kotlin/com/back/koreaTravelGuide/common/exception/GlobalExceptionHandler.kt index 3a5685c..40610c1 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/exception/GlobalExceptionHandler.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/exception/GlobalExceptionHandler.kt @@ -46,6 +46,12 @@ class GlobalExceptionHandler { ex: Exception, request: HttpServletRequest, ): ResponseEntity> { + // Static resource μ˜ˆμ™ΈλŠ” λ¬΄μ‹œ (favicon.ico, CSS, JS λ“±) + if (ex is org.springframework.web.servlet.resource.NoResourceFoundException) { + logger.debug("Static resource not found: {} at {}", ex.message, request.requestURI) + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResponse("λ¦¬μ†ŒμŠ€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€")) + } + logger.error("μ„œλ²„ 였λ₯˜ - {}: {} at {}", ex::class.simpleName, ex.message, request.requestURI, ex) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse("μ„œλ²„ λ‚΄λΆ€ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€")) } 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 d1c5914..cb687f6 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt @@ -5,6 +5,7 @@ import com.back.koreaTravelGuide.security.CustomOAuth2UserService import com.back.koreaTravelGuide.security.JwtAuthenticationFilter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.http.SessionCreationPolicy @@ -16,35 +17,59 @@ class SecurityConfig( private val customOAuth2UserService: CustomOAuth2UserService, private val customOAuth2LoginSuccessHandler: CustomOAuth2LoginSuccessHandler, private val jwtAuthenticationFilter: JwtAuthenticationFilter, + private val environment: Environment, ) { @Bean fun filterChain(http: HttpSecurity): SecurityFilterChain { + val isDev = environment.activeProfiles.contains("dev") + http { - // κΈ°λ³Έ λ³΄μ•ˆ κΈ°λŠ₯ csrf { disable() } formLogin { disable() } httpBasic { disable() } logout { disable() } + headers { + if (isDev) { + frameOptions { disable() } + } else { + frameOptions { sameOrigin } + } + } + sessionManagement { - sessionCreationPolicy = SessionCreationPolicy.STATELESS + sessionCreationPolicy = + if (isDev) { + SessionCreationPolicy.IF_REQUIRED + } else { + SessionCreationPolicy.STATELESS + } } - oauth2Login { - userInfoEndpoint { - userService = customOAuth2UserService + if (!isDev) { + oauth2Login { + userInfoEndpoint { + userService = customOAuth2UserService + } + authenticationSuccessHandler = customOAuth2LoginSuccessHandler } - authenticationSuccessHandler = customOAuth2LoginSuccessHandler } authorizeHttpRequests { - authorize("/api/auth/**", permitAll) - authorize("/swagger-ui/**", "/v3/api-docs/**", permitAll) authorize("/h2-console/**", permitAll) + authorize("/swagger-ui/**", "/v3/api-docs/**", permitAll) + authorize("/api/auth/**", permitAll) authorize("/favicon.ico", permitAll) - authorize(anyRequest, authenticated) + if (isDev) { + authorize(anyRequest, permitAll) + } else { + authorize(anyRequest, authenticated) + } + } + + if (!isDev) { + addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) } - addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java) } return http.build() diff --git a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/service/AiChatService.kt b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/service/AiChatService.kt index 322a6a9..6209b75 100644 --- a/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/service/AiChatService.kt +++ b/src/main/kotlin/com/back/koreaTravelGuide/domain/ai/aiChat/service/AiChatService.kt @@ -7,6 +7,7 @@ import com.back.koreaTravelGuide.domain.ai.aiChat.entity.AiChatSession import com.back.koreaTravelGuide.domain.ai.aiChat.entity.SenderType import com.back.koreaTravelGuide.domain.ai.aiChat.repository.AiChatMessageRepository import com.back.koreaTravelGuide.domain.ai.aiChat.repository.AiChatSessionRepository +import org.slf4j.LoggerFactory import org.springframework.ai.chat.client.ChatClient import org.springframework.ai.chat.memory.ChatMemory import org.springframework.stereotype.Service @@ -17,6 +18,8 @@ class AiChatService( private val aiChatSessionRepository: AiChatSessionRepository, private val chatClient: ChatClient, ) { + private val logger = LoggerFactory.getLogger(AiChatService::class.java) + fun getSessions(userId: Long): List { return aiChatSessionRepository.findByUserIdOrderByCreatedAtDesc(userId) } @@ -74,6 +77,7 @@ class AiChatService( .call() .content() ?: AI_ERROR_FALLBACK } catch (e: Exception) { + logger.error("AI 응닡 생성 μ‹€νŒ¨: {}", e.message, e) AI_ERROR_FALLBACK } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7a44033..57aa50f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,8 @@ spring: profiles: active: dev + config: + import: "optional:file:.env[.properties]" # Spring AI 1.0.0-M6 μ„€μ • (OpenRouter μ‚¬μš©) ai: @@ -14,7 +16,7 @@ spring: # 개발용 H2 Database μ„€μ • (μ£Όλ‹ˆμ–΄ 개발자용) datasource: - url: jdbc:h2:mem:korea_travel_guide + url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: "" @@ -34,6 +36,11 @@ spring: hibernate: format_sql: true # SQL ν¬λ§·νŒ… + sql: + init: + mode: always + schema-locations: classpath:org/springframework/ai/chat/memory/repository/jdbc/schema-h2.sql + # WebSocket μ„€μ • (Guest-Guide μ±„νŒ…μš©) websocket: allowed-origins: "http://localhost:3000,http://localhost:3001,http://localhost:8080" @@ -63,6 +70,12 @@ spring: session: store-type: none # Redis 없어도 μ‹€ν–‰ κ°€λŠ₯ν•˜λ„λ‘ λ³€κ²½ timeout: 30m + + # Redis μžλ™ μ„€μ • λΉ„ν™œμ„±ν™” (μ„Έμ…˜ λΉ„ν™œμ„±ν™”μš©) + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.session.SessionAutoConfiguration + - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration # Swagger API λ¬Έμ„œ μ„€μ • (μ£Όλ‹ˆμ–΄ 개발자용) springdoc: @@ -79,6 +92,12 @@ weather: key: ${WEATHER_API_KEY} base-url: http://apis.data.go.kr/1360000/MidFcstInfoService +# Tour API μ„€μ • +tour: + api: + key: ${TOUR_API_KEY:dev-tour-api-key-placeholder} + base-url: ${TOUR_API_BASE_URL:http://apis.data.go.kr/B551011/KorService1} + # λ‘œκΉ… μ„€μ • (μ£Όλ‹ˆμ–΄ 개발자 λ””λ²„κΉ…μš©) logging: @@ -111,4 +130,5 @@ management: # JWT μ„€μ • jwt: - secret-key: ${CUSTOM__JWT__SECRET_KEY} \ No newline at end of file + secret-key: ${CUSTOM__JWT__SECRET_KEY:dev-secret-key-for-local-testing-please-change} + access-token-expiration-minutes: ${JWT_ACCESS_TOKEN_EXPIRATION_MINUTES:60} \ No newline at end of file diff --git a/src/main/resources/org/springframework/ai/chat/memory/repository/jdbc/schema-h2.sql b/src/main/resources/org/springframework/ai/chat/memory/repository/jdbc/schema-h2.sql new file mode 100644 index 0000000..0bcb8d3 --- /dev/null +++ b/src/main/resources/org/springframework/ai/chat/memory/repository/jdbc/schema-h2.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( + id VARCHAR(255) DEFAULT RANDOM_UUID() PRIMARY KEY, + conversation_id VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + content CLOB NOT NULL, + metadata CLOB, + "timestamp" TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_spring_ai_chat_memory_conversation_id ON SPRING_AI_CHAT_MEMORY(conversation_id); +CREATE INDEX IF NOT EXISTS idx_spring_ai_chat_memory_timestamp ON SPRING_AI_CHAT_MEMORY("timestamp"); \ No newline at end of file diff --git a/src/main/resources/org/springframework/ai/chat/memory/repository/jdbc/schema-postgresql.sql b/src/main/resources/org/springframework/ai/chat/memory/repository/jdbc/schema-postgresql.sql new file mode 100644 index 0000000..63253c9 --- /dev/null +++ b/src/main/resources/org/springframework/ai/chat/memory/repository/jdbc/schema-postgresql.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY ( + id VARCHAR(255) PRIMARY KEY, + conversation_id VARCHAR(255) NOT NULL, + message_type VARCHAR(50) NOT NULL, + content TEXT NOT NULL, + metadata TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_spring_ai_chat_memory_conversation_id ON SPRING_AI_CHAT_MEMORY(conversation_id); +CREATE INDEX IF NOT EXISTS idx_spring_ai_chat_memory_created_at ON SPRING_AI_CHAT_MEMORY(created_at); \ No newline at end of file