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
Expand Up @@ -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: (비어있음)")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ class GlobalExceptionHandler {
ex: Exception,
request: HttpServletRequest,
): ResponseEntity<ApiResponse<Void>> {
// 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("서버 내부 오류가 발생했습니다"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<AiChatSession> {
return aiChatSessionRepository.findByUserIdOrderByCreatedAtDesc(userId)
}
Expand Down Expand Up @@ -74,6 +77,7 @@ class AiChatService(
.call()
.content() ?: AI_ERROR_FALLBACK
} catch (e: Exception) {
logger.error("AI 응답 생성 실패: {}", e.message, e)
AI_ERROR_FALLBACK
}

Expand Down
24 changes: 22 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
spring:
profiles:
active: dev
config:
import: "optional:file:.env[.properties]"

# Spring AI 1.0.0-M6 설정 (OpenRouter 사용)
ai:
Expand All @@ -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: ""
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -111,4 +130,5 @@ management:

# JWT 설정
jwt:
secret-key: ${CUSTOM__JWT__SECRET_KEY}
secret-key: ${CUSTOM__JWT__SECRET_KEY:dev-secret-key-for-local-testing-please-change}
access-token-expiration-minutes: ${JWT_ACCESS_TOKEN_EXPIRATION_MINUTES:60}
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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);