This commit is contained in:
Hyowon Yang 2025-10-30 17:50:19 +09:00
commit 2663baf615
7 changed files with 428 additions and 126 deletions

View File

@ -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/**");
}
}

View File

@ -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}

View 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
```

View File

@ -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<Participant> 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<Participant> 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();
}
}
}

View File

@ -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<ApiResponse<ParticipationResponse>> 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<ApiResponse<PageResponse<ParticipationResponse>>> 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<ApiResponse<ParticipationResponse>> getParticipant(
@PathVariable String eventId,
@PathVariable String participantId) {

View File

@ -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<ApiResponse<DrawWinnersResponse>> 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<ApiResponse<PageResponse<ParticipationResponse>>> getWinners(
@Parameter(description = "이벤트 ID", example = "evt_20250124_001")
@PathVariable String eventId,

View File

@ -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}