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}