diff --git a/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java b/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java index 08e9b2e..dd39aca 100644 --- a/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java +++ b/ai-service/src/main/java/com/kt/ai/config/SecurityConfig.java @@ -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,22 @@ 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 -> 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() ); @@ -50,11 +52,14 @@ public class SecurityConfig { /** * CORS 설정 + * - 모든 Origin 허용 (Swagger UI 테스트를 위해) + * - 모든 HTTP Method 허용 + * - 모든 Header 허용 */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:8080")); + configuration.setAllowedOriginPatterns(List.of("*")); // 모든 Origin 허용 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); @@ -64,4 +69,13 @@ public class SecurityConfig { source.registerCorsConfiguration("/**", configuration); return source; } + + /** + * Chrome DevTools 요청 등 정적 리소스 요청을 Spring Security에서 제외 + */ + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring() + .requestMatchers("/.well-known/**"); + } } diff --git a/ai-service/src/main/resources/application.yml b/ai-service/src/main/resources/application.yml index 5bddf3a..fa3f33d 100644 --- a/ai-service/src/main/resources/application.yml +++ b/ai-service/src/main/resources/application.yml @@ -53,7 +53,7 @@ jwt: # CORS Configuration cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*} + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:*,http://kt-event-marketing.20.214.196.128.nip.io} allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS,PATCH} allowed-headers: ${CORS_ALLOWED_HEADERS:*} allow-credentials: ${CORS_ALLOW_CREDENTIALS:true} diff --git a/develop/dev/ai-service-workflow.md b/develop/dev/ai-service-workflow.md new file mode 100644 index 0000000..c8796a3 --- /dev/null +++ b/develop/dev/ai-service-workflow.md @@ -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
(ai-event-generation-job topic) + + %% 2. Kafka Consumer가 메시지 수신 + Kafka->>Consumer: consume(AIJobMessage) + Note over Consumer: @KafkaListener
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
System: "트렌드 분석 전문가"
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
System: "이벤트 기획 전문가"
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 +``` diff --git a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/DebugController.java b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/DebugController.java deleted file mode 100644 index a186d0f..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/DebugController.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.kt.event.participation.presentation.controller; - -import com.kt.event.participation.domain.participant.Participant; -import com.kt.event.participation.domain.participant.ParticipantRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * 디버깅용 컨트롤러 - */ -@Slf4j -@CrossOrigin(origins = "http://localhost:3000") -@RestController -@RequestMapping("/debug") -@RequiredArgsConstructor -public class DebugController { - - private final ParticipantRepository participantRepository; - - /** - * 중복 참여 체크 테스트 - */ - @GetMapping("/exists/{eventId}/{phoneNumber}") - public String testExists(@PathVariable String eventId, @PathVariable String phoneNumber) { - try { - log.info("디버그: 중복 체크 시작 - eventId: {}, phoneNumber: {}", eventId, phoneNumber); - - boolean exists = participantRepository.existsByEventIdAndPhoneNumber(eventId, phoneNumber); - - log.info("디버그: 중복 체크 결과 - exists: {}", exists); - - long totalCount = participantRepository.count(); - long eventCount = participantRepository.countByEventId(eventId); - - return String.format( - "eventId: %s, phoneNumber: %s, exists: %s, totalCount: %d, eventCount: %d", - eventId, phoneNumber, exists, totalCount, eventCount - ); - - } catch (Exception e) { - log.error("디버그: 예외 발생", e); - return "ERROR: " + e.getMessage(); - } - } - - /** - * 모든 참여자 데이터 조회 - */ - @GetMapping("/participants") - public String getAllParticipants() { - try { - List participants = participantRepository.findAll(); - - StringBuilder sb = new StringBuilder(); - sb.append("Total participants: ").append(participants.size()).append("\n\n"); - - for (Participant p : participants) { - sb.append(String.format("ID: %s, EventID: %s, Phone: %s, Name: %s\n", - p.getParticipantId(), p.getEventId(), p.getPhoneNumber(), p.getName())); - } - - return sb.toString(); - - } catch (Exception e) { - log.error("디버그: 참여자 조회 예외 발생", e); - return "ERROR: " + e.getMessage(); - } - } - - /** - * 특정 전화번호의 참여 이력 조회 - */ - @GetMapping("/phone/{phoneNumber}") - public String getByPhoneNumber(@PathVariable String phoneNumber) { - try { - List participants = participantRepository.findAll(); - - StringBuilder sb = new StringBuilder(); - sb.append("Participants with phone: ").append(phoneNumber).append("\n\n"); - - int count = 0; - for (Participant p : participants) { - if (phoneNumber.equals(p.getPhoneNumber())) { - sb.append(String.format("ID: %s, EventID: %s, Name: %s\n", - p.getParticipantId(), p.getEventId(), p.getName())); - count++; - } - } - - if (count == 0) { - sb.append("No participants found with this phone number."); - } - - return sb.toString(); - - } catch (Exception e) { - log.error("디버그: 전화번호별 조회 예외 발생", e); - return "ERROR: " + e.getMessage(); - } - } -} \ No newline at end of file diff --git a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java index 078f913..39f8b58 100644 --- a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java +++ b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java @@ -35,9 +35,9 @@ public class ParticipationController { /** * 이벤트 참여 - * POST /events/{eventId}/participate + * POST /participations/{eventId}/participate */ - @PostMapping("/events/{eventId}/participate") + @PostMapping("/participations/{eventId}/participate") public ResponseEntity> participate( @PathVariable String eventId, @Valid @RequestBody ParticipationRequest request) { @@ -61,14 +61,15 @@ public class ParticipationController { /** * 참여자 목록 조회 - * GET /events/{eventId}/participants + * GET /participations/{eventId}/participants + * GET /events/{eventId}/participants (프론트엔드 호환) */ @Operation( summary = "참여자 목록 조회", description = "이벤트의 참여자 목록을 페이징하여 조회합니다. " + "정렬 가능한 필드: createdAt(기본값), participantId, name, phoneNumber, bonusEntries, isWinner, wonAt" ) - @GetMapping("/events/{eventId}/participants") + @GetMapping({"/participations/{eventId}/participants", "/events/{eventId}/participants"}) public ResponseEntity>> getParticipants( @Parameter(description = "이벤트 ID", example = "evt_20250124_001") @PathVariable String eventId, @@ -89,9 +90,10 @@ public class ParticipationController { /** * 참여자 상세 조회 - * GET /events/{eventId}/participants/{participantId} + * GET /participations/{eventId}/participants/{participantId} + * GET /events/{eventId}/participants/{participantId} (프론트엔드 호환) */ - @GetMapping("/events/{eventId}/participants/{participantId}") + @GetMapping({"/participations/{eventId}/participants/{participantId}", "/events/{eventId}/participants/{participantId}"}) public ResponseEntity> getParticipant( @PathVariable String eventId, @PathVariable String participantId) { diff --git a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java index 3adf1fe..4a42e61 100644 --- a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java +++ b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java @@ -35,9 +35,9 @@ public class WinnerController { /** * 당첨자 추첨 - * POST /events/{eventId}/draw-winners + * POST /participations/{eventId}/draw-winners */ - @PostMapping("/events/{eventId}/draw-winners") + @PostMapping("/participations/{eventId}/draw-winners") public ResponseEntity> drawWinners( @PathVariable String eventId, @Valid @RequestBody DrawWinnersRequest request) { @@ -50,14 +50,14 @@ public class WinnerController { /** * 당첨자 목록 조회 - * GET /events/{eventId}/winners + * GET /participations/{eventId}/winners */ @Operation( summary = "당첨자 목록 조회", description = "이벤트의 당첨자 목록을 페이징하여 조회합니다. " + "정렬 가능한 필드: winnerRank(기본값), wonAt, participantId, name, phoneNumber, bonusEntries" ) - @GetMapping("/events/{eventId}/winners") + @GetMapping("/participations/{eventId}/winners") public ResponseEntity>> getWinners( @Parameter(description = "이벤트 ID", example = "evt_20250124_001") @PathVariable String eventId, diff --git a/participation-service/src/main/resources/application.yml b/participation-service/src/main/resources/application.yml index 44011c7..2f35890 100644 --- a/participation-service/src/main/resources/application.yml +++ b/participation-service/src/main/resources/application.yml @@ -56,7 +56,7 @@ jwt: # CORS 설정 cors: - allowed-origins: ${CORS_ALLOWED_ORIGINS:*} + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:8081,http://localhost:8082,http://localhost:8083,http://localhost:8084,http://kt-event-marketing.20.214.196.128.nip.io} allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS,PATCH} allowed-headers: ${CORS_ALLOWED_HEADERS:*} allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}