mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 10:46:23 +00:00
Merge branch 'feature/ai' into develop
This commit is contained in:
commit
a23b4eb505
@ -4,6 +4,7 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
@ -27,21 +28,19 @@ import java.util.List;
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* Security Filter Chain 설정
|
||||
* - 모든 요청 허용 (내부 API)
|
||||
* - CSRF 비활성화
|
||||
* - Stateless 세션
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// CSRF 비활성화 (REST API는 CSRF 불필요)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 세션 사용 안 함 (JWT 기반 인증)
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
|
||||
// 모든 요청 허용 (테스트용)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/health", "/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/internal/**").permitAll() // Internal API
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
|
||||
@ -49,19 +48,11 @@ public class SecurityConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 설정
|
||||
* Chrome DevTools 요청 등 정적 리소스 요청을 Spring Security에서 제외
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080"));
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||
return (web) -> web.ignoring()
|
||||
.requestMatchers("/.well-known/**");
|
||||
}
|
||||
}
|
||||
|
||||
390
develop/dev/ai-service-workflow.md
Normal file
390
develop/dev/ai-service-workflow.md
Normal file
@ -0,0 +1,390 @@
|
||||
# AI Service 전체 메서드 워크플로우
|
||||
|
||||
## 1. AI 추천 생성 워크플로우 (Kafka 기반 비동기)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ES as Event Service
|
||||
participant Kafka as Kafka Topic
|
||||
participant Consumer as AIJobConsumer
|
||||
participant ARS as AIRecommendationService
|
||||
participant JSS as JobStatusService
|
||||
participant TAS as TrendAnalysisService
|
||||
participant CS as CacheService
|
||||
participant CAC as ClaudeApiClient
|
||||
participant Claude as Claude API
|
||||
participant Redis as Redis
|
||||
|
||||
%% 1. Event Service가 Kafka 메시지 발행
|
||||
ES->>Kafka: Publish AIJobMessage<br/>(ai-event-generation-job topic)
|
||||
|
||||
%% 2. Kafka Consumer가 메시지 수신
|
||||
Kafka->>Consumer: consume(AIJobMessage)
|
||||
Note over Consumer: @KafkaListener<br/>groupId: ai-service-consumers
|
||||
|
||||
%% 3. AI 추천 생성 시작
|
||||
Consumer->>ARS: generateRecommendations(message)
|
||||
activate ARS
|
||||
|
||||
%% 4. Job 상태: PROCESSING (10%)
|
||||
ARS->>JSS: updateJobStatus(jobId, PROCESSING, "트렌드 분석 중")
|
||||
JSS->>CS: saveJobStatus(jobId, status)
|
||||
CS->>Redis: SET ai:job:status:{jobId}
|
||||
|
||||
%% 5. 트렌드 분석
|
||||
ARS->>ARS: analyzeTrend(message)
|
||||
ARS->>CS: getTrend(industry, region)
|
||||
CS->>Redis: GET ai:trend:{industry}:{region}
|
||||
|
||||
alt 캐시 HIT
|
||||
Redis-->>CS: TrendAnalysis (cached)
|
||||
CS-->>ARS: TrendAnalysis
|
||||
else 캐시 MISS
|
||||
ARS->>TAS: analyzeTrend(industry, region)
|
||||
activate TAS
|
||||
|
||||
%% Circuit Breaker 적용
|
||||
TAS->>TAS: circuitBreakerManager.executeWithCircuitBreaker()
|
||||
TAS->>CAC: sendMessage(apiKey, version, request)
|
||||
CAC->>Claude: POST /v1/messages
|
||||
Note over Claude: Model: claude-sonnet-4-5<br/>System: "트렌드 분석 전문가"<br/>Prompt: 업종/지역/계절 트렌드
|
||||
Claude-->>CAC: ClaudeResponse
|
||||
CAC-->>TAS: ClaudeResponse
|
||||
|
||||
TAS->>TAS: parseResponse(responseText)
|
||||
TAS-->>ARS: TrendAnalysis
|
||||
deactivate TAS
|
||||
|
||||
%% 트렌드 캐싱
|
||||
ARS->>CS: saveTrend(industry, region, analysis)
|
||||
CS->>Redis: SET ai:trend:{industry}:{region} (TTL: 1시간)
|
||||
end
|
||||
|
||||
%% 6. Job 상태: PROCESSING (50%)
|
||||
ARS->>JSS: updateJobStatus(jobId, PROCESSING, "이벤트 추천안 생성 중")
|
||||
JSS->>CS: saveJobStatus(jobId, status)
|
||||
CS->>Redis: SET ai:job:status:{jobId}
|
||||
|
||||
%% 7. 이벤트 추천안 생성
|
||||
ARS->>ARS: createRecommendations(message, trendAnalysis)
|
||||
ARS->>ARS: circuitBreakerManager.executeWithCircuitBreaker()
|
||||
ARS->>CAC: sendMessage(apiKey, version, request)
|
||||
CAC->>Claude: POST /v1/messages
|
||||
Note over Claude: Model: claude-sonnet-4-5<br/>System: "이벤트 기획 전문가"<br/>Prompt: 3가지 추천안 생성
|
||||
Claude-->>CAC: ClaudeResponse
|
||||
CAC-->>ARS: ClaudeResponse
|
||||
|
||||
ARS->>ARS: parseRecommendationResponse(responseText)
|
||||
|
||||
%% 8. Job 상태: PROCESSING (90%)
|
||||
ARS->>JSS: updateJobStatus(jobId, PROCESSING, "결과 저장 중")
|
||||
JSS->>CS: saveJobStatus(jobId, status)
|
||||
CS->>Redis: SET ai:job:status:{jobId}
|
||||
|
||||
%% 9. 결과 저장
|
||||
ARS->>CS: saveRecommendation(eventId, result)
|
||||
CS->>Redis: SET ai:recommendation:{eventId} (TTL: 24시간)
|
||||
|
||||
%% 10. Job 상태: COMPLETED (100%)
|
||||
ARS->>JSS: updateJobStatus(jobId, COMPLETED, "AI 추천 완료")
|
||||
JSS->>CS: saveJobStatus(jobId, status)
|
||||
CS->>Redis: SET ai:job:status:{jobId}
|
||||
|
||||
deactivate ARS
|
||||
|
||||
%% 11. Kafka ACK
|
||||
Consumer->>Kafka: acknowledgment.acknowledge()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Job 상태 조회 워크플로우 (동기)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ES as Event Service
|
||||
participant Controller as InternalJobController
|
||||
participant JSS as JobStatusService
|
||||
participant CS as CacheService
|
||||
participant Redis as Redis
|
||||
|
||||
%% 1. Event Service가 Job 상태 조회
|
||||
ES->>Controller: GET /api/v1/ai-service/internal/jobs/{jobId}/status
|
||||
|
||||
%% 2. Job 상태 조회
|
||||
Controller->>JSS: getJobStatus(jobId)
|
||||
activate JSS
|
||||
|
||||
JSS->>CS: getJobStatus(jobId)
|
||||
CS->>Redis: GET ai:job:status:{jobId}
|
||||
|
||||
alt 상태 존재
|
||||
Redis-->>CS: JobStatusResponse
|
||||
CS-->>JSS: Object (JobStatusResponse)
|
||||
JSS->>JSS: objectMapper.convertValue()
|
||||
JSS-->>Controller: JobStatusResponse
|
||||
Controller-->>ES: 200 OK + JobStatusResponse
|
||||
else 상태 없음
|
||||
Redis-->>CS: null
|
||||
CS-->>JSS: null
|
||||
JSS-->>Controller: JobNotFoundException
|
||||
Controller-->>ES: 404 Not Found
|
||||
end
|
||||
|
||||
deactivate JSS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. AI 추천 결과 조회 워크플로우 (동기)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant ES as Event Service
|
||||
participant Controller as InternalRecommendationController
|
||||
participant ARS as AIRecommendationService
|
||||
participant CS as CacheService
|
||||
participant Redis as Redis
|
||||
|
||||
%% 1. Event Service가 AI 추천 결과 조회
|
||||
ES->>Controller: GET /api/v1/ai-service/internal/recommendations/{eventId}
|
||||
|
||||
%% 2. 추천 결과 조회
|
||||
Controller->>ARS: getRecommendation(eventId)
|
||||
activate ARS
|
||||
|
||||
ARS->>CS: getRecommendation(eventId)
|
||||
CS->>Redis: GET ai:recommendation:{eventId}
|
||||
|
||||
alt 결과 존재
|
||||
Redis-->>CS: AIRecommendationResult
|
||||
CS-->>ARS: Object (AIRecommendationResult)
|
||||
ARS->>ARS: objectMapper.convertValue()
|
||||
ARS-->>Controller: AIRecommendationResult
|
||||
Controller-->>ES: 200 OK + AIRecommendationResult
|
||||
else 결과 없음
|
||||
Redis-->>CS: null
|
||||
CS-->>ARS: null
|
||||
ARS-->>Controller: RecommendationNotFoundException
|
||||
Controller-->>ES: 404 Not Found
|
||||
end
|
||||
|
||||
deactivate ARS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 헬스체크 워크플로우 (동기)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as Client/Actuator
|
||||
participant Controller as HealthController
|
||||
participant Redis as Redis
|
||||
|
||||
%% 1. 헬스체크 요청
|
||||
Client->>Controller: GET /api/v1/ai-service/health
|
||||
|
||||
%% 2. Redis 상태 확인
|
||||
Controller->>Controller: checkRedis()
|
||||
|
||||
alt RedisTemplate 존재
|
||||
Controller->>Redis: PING
|
||||
alt Redis 정상
|
||||
Redis-->>Controller: PONG
|
||||
Controller->>Controller: redisStatus = UP
|
||||
else Redis 오류
|
||||
Redis-->>Controller: Exception
|
||||
Controller->>Controller: redisStatus = DOWN
|
||||
end
|
||||
else RedisTemplate 없음
|
||||
Controller->>Controller: redisStatus = UNKNOWN
|
||||
end
|
||||
|
||||
%% 3. 전체 상태 판단
|
||||
alt Redis DOWN
|
||||
Controller->>Controller: overallStatus = DEGRADED
|
||||
else Redis UP/UNKNOWN
|
||||
Controller->>Controller: overallStatus = UP
|
||||
end
|
||||
|
||||
%% 4. 응답
|
||||
Controller-->>Client: 200 OK + HealthCheckResponse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 주요 컴포넌트 메서드 목록
|
||||
|
||||
### 5.1 Controller Layer
|
||||
|
||||
#### InternalJobController
|
||||
| 메서드 | HTTP | 엔드포인트 | 설명 |
|
||||
|--------|------|-----------|------|
|
||||
| `getJobStatus(jobId)` | GET | `/api/v1/ai-service/internal/jobs/{jobId}/status` | Job 상태 조회 |
|
||||
| `createTestJob(jobId)` | GET | `/api/v1/ai-service/internal/jobs/debug/create-test-job/{jobId}` | 테스트 Job 생성 (디버그) |
|
||||
|
||||
#### InternalRecommendationController
|
||||
| 메서드 | HTTP | 엔드포인트 | 설명 |
|
||||
|--------|------|-----------|------|
|
||||
| `getRecommendation(eventId)` | GET | `/api/v1/ai-service/internal/recommendations/{eventId}` | AI 추천 결과 조회 |
|
||||
| `debugRedisKeys()` | GET | `/api/v1/ai-service/internal/recommendations/debug/redis-keys` | Redis 모든 키 조회 |
|
||||
| `debugRedisKey(key)` | GET | `/api/v1/ai-service/internal/recommendations/debug/redis-key/{key}` | Redis 특정 키 조회 |
|
||||
| `searchAllDatabases()` | GET | `/api/v1/ai-service/internal/recommendations/debug/search-all-databases` | 전체 DB 검색 |
|
||||
| `createTestData(eventId)` | GET | `/api/v1/ai-service/internal/recommendations/debug/create-test-data/{eventId}` | 테스트 데이터 생성 |
|
||||
|
||||
#### HealthController
|
||||
| 메서드 | HTTP | 엔드포인트 | 설명 |
|
||||
|--------|------|-----------|------|
|
||||
| `healthCheck()` | GET | `/api/v1/ai-service/health` | 서비스 헬스체크 |
|
||||
| `checkRedis()` | - | (내부) | Redis 연결 확인 |
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Service Layer
|
||||
|
||||
#### AIRecommendationService
|
||||
| 메서드 | 호출자 | 설명 |
|
||||
|--------|-------|------|
|
||||
| `getRecommendation(eventId)` | Controller | Redis에서 추천 결과 조회 |
|
||||
| `generateRecommendations(message)` | AIJobConsumer | AI 추천 생성 (전체 프로세스) |
|
||||
| `analyzeTrend(message)` | 내부 | 트렌드 분석 (캐시 확인 포함) |
|
||||
| `createRecommendations(message, trendAnalysis)` | 내부 | 이벤트 추천안 생성 |
|
||||
| `callClaudeApiForRecommendations(message, trendAnalysis)` | 내부 | Claude API 호출 (추천안) |
|
||||
| `buildRecommendationPrompt(message, trendAnalysis)` | 내부 | 추천안 프롬프트 생성 |
|
||||
| `parseRecommendationResponse(responseText)` | 내부 | 추천안 응답 파싱 |
|
||||
| `parseEventRecommendation(node)` | 내부 | EventRecommendation 파싱 |
|
||||
| `parseRange(node)` | 내부 | Range 객체 파싱 |
|
||||
| `extractJsonFromMarkdown(text)` | 내부 | Markdown에서 JSON 추출 |
|
||||
|
||||
#### TrendAnalysisService
|
||||
| 메서드 | 호출자 | 설명 |
|
||||
|--------|-------|------|
|
||||
| `analyzeTrend(industry, region)` | AIRecommendationService | 트렌드 분석 수행 |
|
||||
| `callClaudeApi(industry, region)` | 내부 | Claude API 호출 (트렌드) |
|
||||
| `buildPrompt(industry, region)` | 내부 | 트렌드 분석 프롬프트 생성 |
|
||||
| `parseResponse(responseText)` | 내부 | 트렌드 응답 파싱 |
|
||||
| `extractJsonFromMarkdown(text)` | 내부 | Markdown에서 JSON 추출 |
|
||||
| `parseTrendKeywords(arrayNode)` | 내부 | TrendKeyword 리스트 파싱 |
|
||||
|
||||
#### JobStatusService
|
||||
| 메서드 | 호출자 | 설명 |
|
||||
|--------|-------|------|
|
||||
| `getJobStatus(jobId)` | Controller | Job 상태 조회 |
|
||||
| `updateJobStatus(jobId, status, message)` | AIRecommendationService | Job 상태 업데이트 |
|
||||
| `calculateProgress(status)` | 내부 | 상태별 진행률 계산 |
|
||||
|
||||
#### CacheService
|
||||
| 메서드 | 호출자 | 설명 |
|
||||
|--------|-------|------|
|
||||
| `set(key, value, ttlSeconds)` | 내부 | 범용 캐시 저장 |
|
||||
| `get(key)` | 내부 | 범용 캐시 조회 |
|
||||
| `delete(key)` | 외부 | 캐시 삭제 |
|
||||
| `saveJobStatus(jobId, status)` | JobStatusService | Job 상태 저장 |
|
||||
| `getJobStatus(jobId)` | JobStatusService | Job 상태 조회 |
|
||||
| `saveRecommendation(eventId, recommendation)` | AIRecommendationService | AI 추천 결과 저장 |
|
||||
| `getRecommendation(eventId)` | AIRecommendationService | AI 추천 결과 조회 |
|
||||
| `saveTrend(industry, region, trend)` | AIRecommendationService | 트렌드 분석 결과 저장 |
|
||||
| `getTrend(industry, region)` | AIRecommendationService | 트렌드 분석 결과 조회 |
|
||||
|
||||
---
|
||||
|
||||
### 5.3 Consumer Layer
|
||||
|
||||
#### AIJobConsumer
|
||||
| 메서드 | 트리거 | 설명 |
|
||||
|--------|-------|------|
|
||||
| `consume(message, topic, offset, ack)` | Kafka Message | Kafka 메시지 수신 및 처리 |
|
||||
|
||||
---
|
||||
|
||||
### 5.4 Client Layer
|
||||
|
||||
#### ClaudeApiClient (Feign)
|
||||
| 메서드 | 호출자 | 설명 |
|
||||
|--------|-------|------|
|
||||
| `sendMessage(apiKey, anthropicVersion, request)` | TrendAnalysisService, AIRecommendationService | Claude API 호출 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Redis 캐시 키 구조
|
||||
|
||||
| 키 패턴 | 설명 | TTL |
|
||||
|--------|------|-----|
|
||||
| `ai:job:status:{jobId}` | Job 상태 정보 | 24시간 (86400초) |
|
||||
| `ai:recommendation:{eventId}` | AI 추천 결과 | 24시간 (86400초) |
|
||||
| `ai:trend:{industry}:{region}` | 트렌드 분석 결과 | 1시간 (3600초) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Claude API 호출 정보
|
||||
|
||||
### 7.1 트렌드 분석
|
||||
- **URL**: `https://api.anthropic.com/v1/messages`
|
||||
- **Model**: `claude-sonnet-4-5-20250929`
|
||||
- **Max Tokens**: 4096
|
||||
- **Temperature**: 0.7
|
||||
- **System Prompt**: "당신은 마케팅 트렌드 분석 전문가입니다. 업종별, 지역별 트렌드를 분석하고 인사이트를 제공합니다."
|
||||
- **응답 형식**: JSON (industryTrends, regionalTrends, seasonalTrends)
|
||||
|
||||
### 7.2 이벤트 추천안 생성
|
||||
- **URL**: `https://api.anthropic.com/v1/messages`
|
||||
- **Model**: `claude-sonnet-4-5-20250929`
|
||||
- **Max Tokens**: 4096
|
||||
- **Temperature**: 0.7
|
||||
- **System Prompt**: "당신은 소상공인을 위한 마케팅 이벤트 기획 전문가입니다. 트렌드 분석을 바탕으로 실행 가능한 이벤트 추천안을 제공합니다."
|
||||
- **응답 형식**: JSON (recommendations: 3가지 옵션)
|
||||
|
||||
---
|
||||
|
||||
## 8. Circuit Breaker 설정
|
||||
|
||||
### 적용 대상
|
||||
- `claudeApi`: 모든 Claude API 호출
|
||||
|
||||
### 설정값
|
||||
```yaml
|
||||
failure-rate-threshold: 50%
|
||||
slow-call-duration-threshold: 60초
|
||||
sliding-window-size: 10
|
||||
minimum-number-of-calls: 5
|
||||
wait-duration-in-open-state: 60초
|
||||
timeout-duration: 300초 (5분)
|
||||
```
|
||||
|
||||
### Fallback 메서드
|
||||
- `AIServiceFallback.getDefaultTrendAnalysis()`: 기본 트렌드 분석
|
||||
- `AIServiceFallback.getDefaultRecommendations()`: 기본 추천안
|
||||
|
||||
---
|
||||
|
||||
## 9. 에러 처리
|
||||
|
||||
### Exception 종류
|
||||
| Exception | HTTP Code | 발생 조건 |
|
||||
|-----------|-----------|---------|
|
||||
| `RecommendationNotFoundException` | 404 | Redis에 추천 결과 없음 |
|
||||
| `JobNotFoundException` | 404 | Redis에 Job 상태 없음 |
|
||||
| `AIServiceException` | 500 | AI 서비스 내부 오류 |
|
||||
|
||||
### 에러 응답 예시
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-10-30T15:30:00",
|
||||
"status": 404,
|
||||
"error": "Not Found",
|
||||
"message": "추천 결과를 찾을 수 없습니다: eventId=evt-123",
|
||||
"path": "/api/v1/ai-service/internal/recommendations/evt-123"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 로깅 레벨
|
||||
|
||||
```yaml
|
||||
com.kt.ai: DEBUG
|
||||
org.springframework.kafka: INFO
|
||||
org.springframework.data.redis: INFO
|
||||
io.github.resilience4j: DEBUG
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user