diff --git a/design/backend/api/API-설계서.md b/design/backend/api/API-설계서.md new file mode 100644 index 0000000..f443d6d --- /dev/null +++ b/design/backend/api/API-설계서.md @@ -0,0 +1,665 @@ +# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - API 설계서 + +## 문서 정보 +- **작성일**: 2025-10-23 +- **버전**: 1.0 +- **작성자**: System Architect +- **관련 문서**: + - [유저스토리](../../userstory.md) + - [논리 아키텍처](../logical/logical-architecture.md) + - [외부 시퀀스 설계](../sequence/outer/) + - [내부 시퀀스 설계](../sequence/inner/) + +--- + +## 목차 +1. [개요](#1-개요) +2. [API 설계 원칙](#2-api-설계-원칙) +3. [서비스별 API 명세](#3-서비스별-api-명세) +4. [API 통합 가이드](#4-api-통합-가이드) +5. [보안 및 인증](#5-보안-및-인증) +6. [에러 처리](#6-에러-처리) +7. [API 테스트 가이드](#7-api-테스트-가이드) + +--- + +## 1. 개요 + +### 1.1 설계 범위 +본 API 설계서는 KT AI 기반 소상공인 이벤트 자동 생성 서비스의 7개 마이크로서비스 API를 정의합니다. + +### 1.2 마이크로서비스 구성 +1. **User Service**: 사용자 인증 및 매장정보 관리 +2. **Event Service**: 이벤트 전체 생명주기 관리 +3. **AI Service**: AI 기반 이벤트 추천 +4. **Content Service**: SNS 콘텐츠 생성 +5. **Distribution Service**: 다중 채널 배포 관리 +6. **Participation Service**: 이벤트 참여 및 당첨자 관리 +7. **Analytics Service**: 실시간 효과 측정 및 통합 대시보드 + +### 1.3 파일 구조 +``` +design/backend/api/ +├── user-service-api.yaml (31KB, 1,011 lines) +├── event-service-api.yaml (41KB, 1,373 lines) +├── ai-service-api.yaml (26KB, 847 lines) +├── content-service-api.yaml (37KB, 1,158 lines) +├── distribution-service-api.yaml (21KB, 653 lines) +├── participation-service-api.yaml (25KB, 820 lines) +├── analytics-service-api.yaml (28KB, 1,050 lines) +└── API-설계서.md (this file) +``` + +--- + +## 2. API 설계 원칙 + +### 2.1 OpenAPI 3.0 표준 준수 +- 모든 API는 OpenAPI 3.0 스펙을 따릅니다 +- Swagger UI/Editor에서 직접 테스트 가능합니다 +- 자동 코드 생성 및 문서화를 지원합니다 + +### 2.2 RESTful 설계 +- **리소스 중심 URL 구조**: `/api/{resource}/{id}` +- **HTTP 메서드**: GET (조회), POST (생성), PUT (수정), DELETE (삭제) +- **상태 코드**: 200 (성공), 201 (생성), 400 (잘못된 요청), 401 (인증 실패), 403 (권한 없음), 404 (리소스 없음), 500 (서버 오류) + +### 2.3 유저스토리 기반 설계 +- 각 API 엔드포인트는 유저스토리와 매핑됩니다 +- **x-user-story** 필드로 유저스토리 ID를 명시합니다 +- **x-controller** 필드로 담당 컨트롤러를 명시합니다 + +### 2.4 서비스 독립성 +- 각 서비스는 독립적인 OpenAPI 명세를 가집니다 +- 공통 스키마는 각 서비스에서 필요에 따라 정의합니다 +- 서비스 간 통신은 REST API, Kafka 이벤트, Redis 캐시를 통해 이루어집니다 + +### 2.5 Example 데이터 제공 +- 모든 스키마에 example 데이터가 포함됩니다 +- Swagger UI에서 즉시 테스트 가능합니다 +- 성공/실패 시나리오 모두 포함합니다 + +--- + +## 3. 서비스별 API 명세 + +### 3.1 User Service (사용자 인증 및 매장정보 관리) + +**파일**: `user-service-api.yaml` +**관련 유저스토리**: UFR-USER-010, 020, 030, 040 + +#### API 엔드포인트 (7개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/users/register | 회원가입 | UFR-USER-010 | - | +| POST | /api/users/login | 로그인 | UFR-USER-020 | - | +| POST | /api/users/logout | 로그아웃 | UFR-USER-040 | JWT | +| GET | /api/users/profile | 프로필 조회 | UFR-USER-030 | JWT | +| PUT | /api/users/profile | 프로필 수정 | UFR-USER-030 | JWT | +| PUT | /api/users/password | 비밀번호 변경 | UFR-USER-030 | JWT | +| GET | /api/users/{userId}/store | 매장정보 조회 (서비스 연동용) | - | JWT | + +#### 주요 기능 +- JWT 토큰 기반 인증 (TTL 7일) +- 사업자번호 검증 (국세청 API 연동) +- Redis 세션 관리 +- BCrypt 비밀번호 해싱 +- AES-256-GCM 사업자번호 암호화 + +#### 주요 스키마 +- `UserRegisterRequest`: 회원가입 요청 +- `UserLoginRequest`: 로그인 요청 +- `UserProfileResponse`: 프로필 응답 +- `StoreInfoResponse`: 매장정보 응답 + +--- + +### 3.2 Event Service (이벤트 전체 생명주기 관리) + +**파일**: `event-service-api.yaml` +**관련 유저스토리**: UFR-EVENT-010 ~ 070 + +#### API 엔드포인트 (14개) + +**Dashboard & Event List:** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /api/events | 이벤트 목록 조회 | UFR-EVENT-010, 070 | JWT | +| GET | /api/events/{eventId} | 이벤트 상세 조회 | UFR-EVENT-060 | JWT | + +**Event Creation Flow (5 Steps):** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/events/objectives | Step 1: 이벤트 목적 선택 | UFR-EVENT-020 | JWT | +| POST | /api/events/{eventId}/ai-recommendations | Step 2: AI 추천 요청 | UFR-EVENT-030 | JWT | +| PUT | /api/events/{eventId}/recommendations | Step 2-2: AI 추천 선택 | UFR-EVENT-030 | JWT | +| POST | /api/events/{eventId}/images | Step 3: 이미지 생성 요청 | UFR-CONT-010 | JWT | +| PUT | /api/events/{eventId}/images/{imageId}/select | Step 3-2: 이미지 선택 | UFR-CONT-010 | JWT | +| PUT | /api/events/{eventId}/images/{imageId}/edit | Step 3-3: 이미지 편집 | UFR-CONT-020 | JWT | +| PUT | /api/events/{eventId}/channels | Step 4: 배포 채널 선택 | UFR-EVENT-040 | JWT | +| POST | /api/events/{eventId}/publish | Step 5: 최종 승인 및 배포 | UFR-EVENT-050 | JWT | + +**Event Management:** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| PUT | /api/events/{eventId} | 이벤트 수정 | UFR-EVENT-060 | JWT | +| DELETE | /api/events/{eventId} | 이벤트 삭제 | UFR-EVENT-070 | JWT | +| POST | /api/events/{eventId}/end | 이벤트 조기 종료 | UFR-EVENT-060 | JWT | + +**Job Status:** +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /api/jobs/{jobId} | Job 상태 폴링 | UFR-EVENT-030, UFR-CONT-010 | JWT | + +#### 주요 기능 +- 이벤트 생명주기 관리 (DRAFT → PUBLISHED → ENDED) +- Kafka Job 발행 (ai-event-generation-job, image-generation-job) +- Kafka Event 발행 (EventCreated) +- Distribution Service 동기 호출 +- Redis 기반 AI/이미지 데이터 캐싱 +- Job 상태 폴링 메커니즘 (PENDING, PROCESSING, COMPLETED, FAILED) + +#### 주요 스키마 +- `EventObjectiveRequest`: 이벤트 목적 선택 +- `EventResponse`: 이벤트 응답 +- `JobStatusResponse`: Job 상태 응답 +- `AIRecommendationSelection`: AI 추천 선택 +- `ChannelSelectionRequest`: 배포 채널 선택 + +--- + +### 3.3 AI Service (AI 기반 이벤트 추천) + +**파일**: `ai-service-api.yaml` +**관련 유저스토리**: UFR-AI-010 + +#### API 엔드포인트 (3개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /health | 서비스 헬스체크 | - | - | +| GET | /internal/jobs/{jobId}/status | Job 상태 조회 (내부 API) | UFR-AI-010 | JWT | +| GET | /internal/recommendations/{eventId} | AI 추천 결과 조회 (내부 API) | UFR-AI-010 | JWT | + +#### Kafka Consumer (비동기 처리) +- **Topic**: `ai-event-generation-job` +- **Consumer Group**: `ai-service-consumers` +- **처리 시간**: 최대 5분 +- **결과 저장**: Redis (TTL 24시간) + +#### 주요 기능 +- 업종/지역/시즌 트렌드 분석 +- 3가지 차별화된 이벤트 기획안 생성 +- 예상 성과 계산 (참여자 수, ROI, 매출 증가율) +- Circuit Breaker 패턴 (5분 timeout, 캐시 fallback) +- Claude API / GPT-4 API 연동 + +#### 주요 스키마 +- `KafkaAIJobMessage`: Kafka Job 입력 +- `AIRecommendationResult`: AI 추천 결과 (트렌드 분석 + 3가지 옵션) +- `TrendAnalysis`: 업종/지역/시즌 트렌드 +- `EventRecommendation`: 이벤트 기획안 (컨셉, 경품, 참여방법, 예상성과) + +--- + +### 3.4 Content Service (SNS 콘텐츠 생성) + +**파일**: `content-service-api.yaml` +**관련 유저스토리**: UFR-CONT-010, 020 + +#### API 엔드포인트 (6개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/content/images/generate | 이미지 생성 요청 (비동기) | UFR-CONT-010 | JWT | +| GET | /api/content/images/jobs/{jobId} | Job 상태 폴링 | UFR-CONT-010 | JWT | +| GET | /api/content/events/{eventDraftId} | 이벤트 전체 콘텐츠 조회 | UFR-CONT-020 | JWT | +| GET | /api/content/events/{eventDraftId}/images | 이미지 목록 조회 | UFR-CONT-020 | JWT | +| GET | /api/content/images/{imageId} | 이미지 상세 조회 | UFR-CONT-020 | JWT | +| POST | /api/content/images/{imageId}/regenerate | 이미지 재생성 | UFR-CONT-020 | JWT | + +#### Kafka Consumer (비동기 처리) +- **Topic**: `image-generation-job` +- **Consumer Group**: `content-service-consumers` +- **처리 시간**: 최대 5분 +- **결과 저장**: Redis (CDN URL, TTL 7일) + +#### 주요 기능 +- 3가지 스타일 이미지 생성 (SIMPLE, FANCY, TRENDY) +- 플랫폼별 최적화 (Instagram 1080x1080, Naver 800x600, Kakao 800x800) +- Circuit Breaker 패턴 (Stable Diffusion → DALL-E → Default Template) +- Azure Blob Storage (CDN) 연동 +- Redis 기반 AI 데이터 읽기 + +#### 주요 스키마 +- `ImageGenerationJob`: Kafka Job 입력 +- `ImageGenerationRequest`: 이미지 생성 요청 +- `GeneratedImage`: 생성된 이미지 (style, platform, CDN URL) +- `ContentResponse`: 전체 콘텐츠 응답 + +--- + +### 3.5 Distribution Service (다중 채널 배포 관리) + +**파일**: `distribution-service-api.yaml` +**관련 유저스토리**: UFR-DIST-010, 020 + +#### API 엔드포인트 (2개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/distribution/distribute | 다중 채널 배포 (동기) | UFR-DIST-010 | JWT | +| GET | /api/distribution/{eventId}/status | 배포 상태 조회 | UFR-DIST-020 | JWT | + +#### 주요 기능 +- **배포 채널**: 우리동네TV, 링고비즈, 지니TV, Instagram, Naver Blog, Kakao Channel +- **병렬 배포**: 6개 채널 동시 배포 (1분 이내) +- **Resilience 패턴**: + - Circuit Breaker: 채널별 독립 적용 + - Retry: 최대 3회 재시도 (지수 백오프: 1s, 2s, 4s) + - Bulkhead: 채널별 스레드 풀 격리 + - Fallback: 실패 채널 스킵 + 알림 +- **Kafka Event 발행**: DistributionCompleted +- **로깅**: Event DB에 distribution_logs 저장 + +#### 주요 스키마 +- `DistributionRequest`: 배포 요청 +- `DistributionResponse`: 배포 응답 (채널별 결과) +- `DistributionStatusResponse`: 배포 상태 +- `ChannelDistributionResult`: 채널별 배포 결과 + +--- + +### 3.6 Participation Service (이벤트 참여 및 당첨자 관리) + +**파일**: `participation-service-api.yaml` +**관련 유저스토리**: UFR-PART-010, 020, 030 + +#### API 엔드포인트 (5개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| POST | /api/events/{eventId}/participate | 이벤트 참여 | UFR-PART-010 | - | +| GET | /api/events/{eventId}/participants | 참여자 목록 조회 | UFR-PART-020 | JWT | +| GET | /api/events/{eventId}/participants/{participantId} | 참여자 상세 조회 | UFR-PART-020 | JWT | +| POST | /api/events/{eventId}/draw-winners | 당첨자 추첨 | UFR-PART-030 | JWT | +| GET | /api/events/{eventId}/winners | 당첨자 목록 조회 | UFR-PART-030 | JWT | + +#### 주요 기능 +- 중복 참여 체크 (전화번호 기반) +- 매장 방문 고객 가산점 적용 +- 난수 기반 무작위 추첨 +- Kafka Event 발행 (ParticipantRegistered) +- 개인정보 수집/이용 동의 관리 +- 페이지네이션 지원 + +#### 주요 스키마 +- `ParticipationRequest`: 참여 요청 +- `ParticipationResponse`: 참여 응답 (응모번호) +- `ParticipantListResponse`: 참여자 목록 +- `WinnerDrawRequest`: 당첨자 추첨 요청 +- `WinnerResponse`: 당첨자 정보 + +--- + +### 3.7 Analytics Service (실시간 효과 측정 및 통합 대시보드) + +**파일**: `analytics-service-api.yaml` +**관련 유저스토리**: UFR-ANAL-010 + +#### API 엔드포인트 (4개) + +| 메서드 | 경로 | 설명 | 유저스토리 | 인증 | +|--------|------|------|-----------|------| +| GET | /api/events/{eventId}/analytics | 성과 대시보드 조회 | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/channels | 채널별 성과 분석 | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/timeline | 시간대별 참여 추이 | UFR-ANAL-010 | JWT | +| GET | /api/events/{eventId}/analytics/roi | 투자 대비 수익률 상세 | UFR-ANAL-010 | JWT | + +#### Kafka Event 구독 +- **EventCreated**: 이벤트 기본 통계 초기화 +- **ParticipantRegistered**: 실시간 참여자 수 증가 +- **DistributionCompleted**: 배포 채널 통계 업데이트 + +#### 주요 기능 +- **실시간 대시보드**: Redis 캐싱 (TTL 5분) +- **외부 API 통합**: 우리동네TV, 지니TV, SNS APIs (조회수, 노출수, 소셜 인터랙션) +- **Circuit Breaker**: 외부 API 실패 시 캐시 fallback +- **ROI 계산**: 비용 대비 수익률 자동 계산 +- **성과 집계**: 채널별, 시간대별 성과 분석 + +#### 주요 스키마 +- `AnalyticsDashboardResponse`: 대시보드 전체 데이터 +- `ChannelPerformanceResponse`: 채널별 성과 +- `TimelineDataResponse`: 시간대별 참여 추이 +- `RoiDetailResponse`: ROI 상세 분석 + +--- + +## 4. API 통합 가이드 + +### 4.1 이벤트 생성 플로우 (Event-Driven) + +``` +1. 이벤트 목적 선택 (Event Service) + POST /api/events/objectives + → EventCreated 이벤트 발행 (Kafka) + → Analytics Service 구독 (통계 초기화) + +2. AI 추천 요청 (Event Service → AI Service) + POST /api/events/{eventId}/ai-recommendations + → ai-event-generation-job 발행 (Kafka) + → AI Service 구독 및 처리 (비동기) + → Redis에 결과 저장 (TTL 24시간) + → 클라이언트 폴링: GET /api/jobs/{jobId} + +3. AI 추천 선택 (Event Service) + PUT /api/events/{eventId}/recommendations + → Redis에서 AI 추천 데이터 읽기 + → Event DB에 선택된 추천 저장 + +4. 이미지 생성 요청 (Event Service → Content Service) + POST /api/events/{eventId}/images + → image-generation-job 발행 (Kafka) + → Content Service 구독 및 처리 (비동기) + → Redis에서 AI 데이터 읽기 + → CDN에 이미지 업로드 + → Redis에 CDN URL 저장 (TTL 7일) + → 클라이언트 폴링: GET /api/jobs/{jobId} + +5. 이미지 선택 및 편집 (Event Service) + PUT /api/events/{eventId}/images/{imageId}/select + PUT /api/events/{eventId}/images/{imageId}/edit + +6. 배포 채널 선택 (Event Service) + PUT /api/events/{eventId}/channels + +7. 최종 승인 및 배포 (Event Service → Distribution Service) + POST /api/events/{eventId}/publish + → Distribution Service 동기 호출: POST /api/distribution/distribute + → 다중 채널 병렬 배포 (1분 이내) + → DistributionCompleted 이벤트 발행 (Kafka) + → Analytics Service 구독 (배포 통계 업데이트) +``` + +### 4.2 고객 참여 플로우 (Event-Driven) + +``` +1. 이벤트 참여 (Participation Service) + POST /api/events/{eventId}/participate + → 중복 참여 체크 + → Participation DB 저장 + → ParticipantRegistered 이벤트 발행 (Kafka) + → Analytics Service 구독 (참여자 수 실시간 증가) + +2. 당첨자 추첨 (Participation Service) + POST /api/events/{eventId}/draw-winners + → 난수 기반 무작위 추첨 + → Winners DB 저장 +``` + +### 4.3 성과 분석 플로우 (Event-Driven) + +``` +1. 실시간 대시보드 조회 (Analytics Service) + GET /api/events/{eventId}/analytics + → Redis 캐시 확인 (TTL 5분) + → 캐시 HIT: 즉시 반환 + → 캐시 MISS: + - Analytics DB 조회 (이벤트/참여 통계) + - 외부 APIs 조회 (우리동네TV, 지니TV, SNS) [Circuit Breaker] + - Redis 캐싱 후 반환 + +2. Kafka 이벤트 구독 (Analytics Service Background) + - EventCreated 구독 → 이벤트 기본 정보 초기화 + - ParticipantRegistered 구독 → 참여자 수 실시간 증가 + - DistributionCompleted 구독 → 배포 채널 통계 업데이트 + - 캐시 무효화 → 다음 조회 시 최신 데이터 갱신 +``` + +### 4.4 서비스 간 통신 패턴 + +| 패턴 | 사용 시나리오 | 통신 방식 | 예시 | +|------|-------------|----------|------| +| **동기 REST API** | 즉시 응답 필요 | HTTP/JSON | Distribution Service 배포 요청 | +| **Kafka Job Topics** | 장시간 비동기 작업 | Kafka 메시지 큐 | AI 추천, 이미지 생성 | +| **Kafka Event Topics** | 상태 변경 알림 | Kafka Pub/Sub | EventCreated, ParticipantRegistered | +| **Redis Cache** | 데이터 공유 | Redis Get/Set | AI 결과, 이미지 URL | + +--- + +## 5. 보안 및 인증 + +### 5.1 JWT 기반 인증 + +**토큰 발급:** +- User Service에서 로그인/회원가입 시 JWT 토큰 발급 +- 토큰 만료 시간: 7일 +- Redis에 세션 정보 저장 (TTL 7일) + +**토큰 검증:** +- API Gateway에서 모든 요청의 JWT 토큰 검증 +- Authorization 헤더: `Bearer {token}` +- 검증 실패 시 401 Unauthorized 응답 + +**보호된 엔드포인트:** +- 모든 API (회원가입, 로그인, 이벤트 참여 제외) + +### 5.2 민감 정보 암호화 + +- **비밀번호**: BCrypt 해싱 (Cost Factor: 10) +- **사업자번호**: AES-256-GCM 암호화 +- **개인정보**: 전화번호 마스킹 (010-****-1234) + +### 5.3 API Rate Limiting + +- API Gateway에서 사용자당 100 req/min 제한 +- Redis 기반 Rate Limiting 구현 + +--- + +## 6. 에러 처리 + +### 6.1 표준 에러 응답 포맷 + +```json +{ + "success": false, + "errorCode": "ERROR_CODE", + "message": "사용자 친화적인 에러 메시지", + "details": "상세 에러 정보 (선택)", + "timestamp": "2025-10-23T16:30:00Z" +} +``` + +### 6.2 HTTP 상태 코드 + +| 상태 코드 | 설명 | 사용 예시 | +|----------|------|----------| +| 200 OK | 성공 | GET 요청 성공 | +| 201 Created | 생성 성공 | POST 요청으로 리소스 생성 | +| 400 Bad Request | 잘못된 요청 | 유효성 검증 실패 | +| 401 Unauthorized | 인증 실패 | JWT 토큰 없음/만료 | +| 403 Forbidden | 권한 없음 | 접근 권한 부족 | +| 404 Not Found | 리소스 없음 | 존재하지 않는 이벤트 조회 | +| 409 Conflict | 충돌 | 중복 참여, 동시성 문제 | +| 500 Internal Server Error | 서버 오류 | 서버 내부 오류 | +| 503 Service Unavailable | 서비스 불가 | Circuit Breaker Open | + +### 6.3 서비스별 주요 에러 코드 + +**User Service:** +- `USER_001`: 중복 사용자 +- `USER_002`: 사업자번호 검증 실패 +- `USER_003`: 사용자 없음 +- `AUTH_001`: 인증 실패 +- `AUTH_002`: 유효하지 않은 토큰 + +**Event Service:** +- `EVENT_001`: 이벤트 없음 +- `EVENT_002`: 유효하지 않은 상태 전환 +- `EVENT_003`: 필수 데이터 누락 (AI 추천, 이미지) +- `JOB_001`: Job 없음 +- `JOB_002`: Job 실패 + +**Participation Service:** +- `PART_001`: 중복 참여 +- `PART_002`: 이벤트 기간 아님 +- `PART_003`: 참여자 없음 + +**Distribution Service:** +- `DIST_001`: 배포 실패 +- `DIST_002`: Circuit Breaker Open + +**Analytics Service:** +- `ANALYTICS_001`: 데이터 없음 +- `EXTERNAL_API_ERROR`: 외부 API 장애 + +--- + +## 7. API 테스트 가이드 + +### 7.1 Swagger UI를 통한 테스트 + +**방법 1: Swagger Editor** +1. https://editor.swagger.io/ 접속 +2. 각 서비스의 YAML 파일 내용 붙여넣기 +3. 우측 Swagger UI에서 API 테스트 + +**방법 2: SwaggerHub** +1. 각 API 명세의 `servers` 섹션에 SwaggerHub Mock Server URL 포함 +2. Mock Server를 통한 즉시 테스트 가능 + +**방법 3: Redocly** +```bash +# 각 API 명세 검증 +npx @redocly/cli lint design/backend/api/*.yaml + +# 문서 HTML 생성 +npx @redocly/cli build-docs design/backend/api/user-service-api.yaml \ + --output docs/user-service-api.html +``` + +### 7.2 테스트 시나리오 예시 + +**1. 회원가입 → 로그인 → 이벤트 생성 플로우** +```bash +# 1. 회원가입 +POST /api/users/register +{ + "name": "김사장", + "phoneNumber": "010-1234-5678", + "email": "owner@example.com", + "password": "SecurePass123!", + "store": { + "name": "맛있는 고깃집", + "industry": "RESTAURANT", + "address": "서울시 강남구 테헤란로 123", + "businessNumber": "123-45-67890" + } +} + +# 2. 로그인 +POST /api/users/login +{ + "phoneNumber": "010-1234-5678", + "password": "SecurePass123!" +} +# → JWT 토큰 수신 + +# 3. 이벤트 목적 선택 +POST /api/events/objectives +Authorization: Bearer {token} +{ + "objective": "NEW_CUSTOMER_ACQUISITION" +} +# → eventId 수신 + +# 4. AI 추천 요청 +POST /api/events/{eventId}/ai-recommendations +Authorization: Bearer {token} +# → jobId 수신 + +# 5. Job 상태 폴링 (5초 간격) +GET /api/jobs/{jobId} +Authorization: Bearer {token} +# → status: COMPLETED 확인 + +# 6. AI 추천 선택 +PUT /api/events/{eventId}/recommendations +Authorization: Bearer {token} +{ + "selectedOption": 1, + "customization": { + "title": "봄맞이 삼겹살 50% 할인 이벤트", + "prizeName": "삼겹살 1인분 무료" + } +} + +# ... (이미지 생성, 배포 채널 선택, 최종 승인) +``` + +### 7.3 Mock 데이터 활용 + +- 모든 API 명세에 example 데이터 포함 +- Swagger UI의 "Try it out" 기능으로 즉시 테스트 +- 성공/실패 시나리오 모두 example 제공 + +### 7.4 통합 테스트 도구 + +**Postman Collection 생성:** +```bash +# OpenAPI 명세를 Postman Collection으로 변환 +npx openapi-to-postmanv2 -s design/backend/api/user-service-api.yaml \ + -o postman/user-service-collection.json +``` + +**Newman (CLI 테스트 실행):** +```bash +# Postman Collection 실행 +newman run postman/user-service-collection.json \ + --environment postman/dev-environment.json +``` + +--- + +## 부록 + +### A. 파일 통계 + +| 서비스 | 파일명 | 크기 | 라인 수 | API 수 | +|--------|--------|------|--------|--------| +| User | user-service-api.yaml | 31KB | 1,011 | 7 | +| Event | event-service-api.yaml | 41KB | 1,373 | 14 | +| AI | ai-service-api.yaml | 26KB | 847 | 3 | +| Content | content-service-api.yaml | 37KB | 1,158 | 6 | +| Distribution | distribution-service-api.yaml | 21KB | 653 | 2 | +| Participation | participation-service-api.yaml | 25KB | 820 | 5 | +| Analytics | analytics-service-api.yaml | 28KB | 1,050 | 4 | +| **합계** | - | **209KB** | **6,912** | **41** | + +### B. 주요 의사결정 + +1. **OpenAPI 3.0 표준 채택**: 업계 표준 준수, 자동 코드 생성 지원 +2. **서비스별 독립 명세**: 서비스 독립성 보장, 독립 배포 가능 +3. **유저스토리 기반 설계**: x-user-story 필드로 추적성 확보 +4. **Example 데이터 포함**: Swagger UI 즉시 테스트 가능 +5. **JWT 인증 표준화**: 모든 서비스에서 일관된 인증 방식 +6. **에러 응답 표준화**: 일관된 에러 응답 포맷 +7. **Kafka + Redis 통합**: Event-Driven 아키텍처 지원 +8. **Circuit Breaker 패턴**: 외부 API 장애 대응 + +### C. 다음 단계 + +1. **외부 시퀀스 설계**: 서비스 간 API 호출 흐름 상세 설계 +2. **내부 시퀀스 설계**: 서비스 내부 컴포넌트 간 호출 흐름 설계 +3. **클래스 설계**: 서비스별 클래스 다이어그램 작성 +4. **데이터 설계**: 서비스별 데이터베이스 스키마 설계 +5. **백엔드 개발**: OpenAPI 명세 기반 코드 생성 및 구현 + +--- + +**문서 버전**: 1.0 +**최종 수정일**: 2025-10-23 +**작성자**: System Architect diff --git a/design/backend/api/ai-service-api.yaml b/design/backend/api/ai-service-api.yaml index af13b25..ea2583f 100644 --- a/design/backend/api/ai-service-api.yaml +++ b/design/backend/api/ai-service-api.yaml @@ -2,1035 +2,846 @@ openapi: 3.0.3 info: title: AI Service API description: | - AI 기반 트렌드 분석 및 이벤트 추천 서비스 API + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - AI Service - ## 주요 기능 - - 업종/지역/시즌별 트렌드 분석 - - AI 기반 이벤트 기획안 추천 (3가지 옵션) - - 비동기 Job 처리 및 폴링 기반 결과 조회 + ## 서비스 개요 + - Kafka를 통한 비동기 AI 추천 처리 + - Claude API / GPT-4 API 연동 + - Redis 기반 결과 캐싱 (TTL 24시간) + + ## 처리 흐름 + 1. Event Service가 Kafka Topic에 Job 메시지 발행 + 2. AI Service가 메시지 구독 및 처리 + 3. 트렌드 분석 수행 (Claude/GPT-4 API) + 4. 3가지 이벤트 추천안 생성 + 5. 결과를 Redis에 저장 (TTL 24시간) + 6. Job 상태를 Redis에 업데이트 + + ## 외부 API 통합 + - **Claude API / GPT-4 API**: 트렌드 분석 및 이벤트 추천 + - **Circuit Breaker**: 5분 타임아웃, Fallback to 캐시 + - **Retry**: 최대 3회, Exponential Backoff - ## 기술 스택 - - AI Engine: Claude API / GPT-4 API - - 캐싱: Redis (트렌드 1시간, 추천안 24시간) - - 메시지 큐: Kafka (비동기 Job 처리) - - 안정성: Circuit Breaker 패턴 version: 1.0.0 contact: - name: AI Service Team - email: ai-team@kt.com + name: Backend Architect + email: architect@kt.com servers: - - url: https://api.kt-event.com/ai/v1 - description: 프로덕션 서버 - - url: https://dev-api.kt-event.com/ai/v1 - description: 개발 서버 - - url: http://localhost:8083/ai/v1 - description: 로컬 개발 서버 + - url: http://localhost:8083 + description: Local Development Server + - url: http://ai-service:8083 + description: Kubernetes Internal Service tags: - - name: AI Analysis - description: AI 기반 트렌드 분석 및 추천 엔드포인트 - - name: Job Status - description: 비동기 Job 상태 조회 엔드포인트 + - name: Health Check + description: 서비스 상태 확인 + - name: Internal API + description: 내부 서비스 간 통신용 API + - name: Kafka Consumer + description: 비동기 작업 처리 (문서화만) paths: - /analyze-trends: - post: - tags: - - AI Analysis - summary: 트렌드 분석 요청 - description: | - 업종, 지역, 시즌을 기반으로 트렌드 분석을 수행합니다. - - ## 처리 방식 - - **비동기 처리**: Kafka를 통한 비동기 Job 생성 - - **응답 시간**: 즉시 Job ID 반환 (< 100ms) - - **실제 처리 시간**: 5~30초 이내 (AI API 응답 시간 포함) - - ## 캐싱 전략 - - 캐시 키: `trend:{업종}:{지역}` - - TTL: 1시간 - - 캐시 히트 시 즉시 응답 - - ## Circuit Breaker - - Failure Rate Threshold: 50% - - Timeout: 30초 - - Half-Open Wait Duration: 30초 - operationId: analyzeTrends - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/TrendAnalysisRequest" - examples: - restaurant: - summary: 음식점 트렌드 분석 - value: - eventDraftId: "evt_draft_001" - industry: "음식점" - region: "서울 강남구" - purpose: "신규 고객 유치" - storeInfo: - storeName: "맛있는 고깃집" - storeSize: "중형" - monthlyRevenue: 30000000 - cafe: - summary: 카페 트렌드 분석 - value: - eventDraftId: "evt_draft_002" - industry: "카페" - region: "서울 홍대" - purpose: "재방문 유도" - storeInfo: - storeName: "커피스토리" - storeSize: "소형" - monthlyRevenue: 15000000 - responses: - "202": - description: | - 트렌드 분석 Job이 생성되었습니다. - - Job ID를 사용하여 `/jobs/{jobId}` 엔드포인트로 결과 폴링 - - 예상 처리 시간: 5~30초 - content: - application/json: - schema: - $ref: "#/components/schemas/JobCreatedResponse" - example: - jobId: "job_ai_20250122_001" - status: "PROCESSING" - message: "트렌드 분석이 진행 중입니다" - estimatedCompletionTime: "2025-01-22T10:05:30Z" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "429": - $ref: "#/components/responses/TooManyRequests" - "500": - $ref: "#/components/responses/InternalServerError" - - /recommend-events: - post: - tags: - - AI Analysis - summary: 이벤트 추천 요청 - description: | - 트렌드 분석 결과를 기반으로 3가지 차별화된 이벤트 기획안을 생성합니다. - - ## 처리 방식 - - **비동기 처리**: Kafka를 통한 비동기 Job 생성 - - **응답 시간**: 즉시 Job ID 반환 (< 100ms) - - **실제 처리 시간**: 5~30초 이내 - - ## 3가지 추천 옵션 - 1. **저비용 옵션**: 높은 참여율 중심 - 2. **중비용 옵션**: 균형잡힌 ROI - 3. **고비용 옵션**: 높은 매출 증대 효과 - - ## 병렬 처리 - - 3가지 옵션 동시 생성 (병렬) - - 전체 처리 시간: 단일 요청 시간과 동일 - - ## 캐싱 전략 - - 캐시 키: `ai:recommendation:{eventDraftId}` - - TTL: 24시간 - operationId: recommendEvents - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/EventRecommendationRequest" - examples: - newCustomer: - summary: 신규 고객 유치 이벤트 - value: - eventDraftId: "evt_draft_001" - purpose: "신규 고객 유치" - industry: "음식점" - region: "서울 강남구" - storeInfo: - storeName: "맛있는 고깃집" - storeSize: "중형" - monthlyRevenue: 30000000 - trendAnalysisJobId: "job_ai_20250122_001" - responses: - "202": - description: | - 이벤트 추천 Job이 생성되었습니다. - - Job ID를 사용하여 `/jobs/{jobId}` 엔드포인트로 결과 폴링 - - 예상 처리 시간: 5~30초 - content: - application/json: - schema: - $ref: "#/components/schemas/JobCreatedResponse" - example: - jobId: "job_ai_20250122_002" - status: "PROCESSING" - message: "이벤트 추천이 진행 중입니다" - estimatedCompletionTime: "2025-01-22T10:05:30Z" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "429": - $ref: "#/components/responses/TooManyRequests" - "500": - $ref: "#/components/responses/InternalServerError" - - /jobs/{jobId}: + /health: get: tags: - - Job Status - summary: Job 상태 조회 - description: | - 비동기 Job의 처리 상태 및 결과를 조회합니다. + - Health Check + summary: 서비스 헬스체크 + description: AI Service 상태 및 외부 연동 확인 + operationId: healthCheck + x-user-story: System + x-controller: HealthController + responses: + '200': + description: 서비스 정상 + content: + application/json: + schema: + $ref: '#/components/schemas/HealthCheckResponse' + example: + status: UP + timestamp: "2025-10-23T10:30:00Z" + services: + kafka: UP + redis: UP + claude_api: UP + gpt4_api: UP + circuit_breaker: CLOSED - ## 폴링 전략 - - **초기 폴링**: 1초 간격 (처음 5회) - - **장기 폴링**: 3초 간격 (이후) - - **최대 대기 시간**: 60초 - - ## Job 상태 - - `PENDING`: 대기 중 - - `PROCESSING`: 처리 중 - - `COMPLETED`: 완료 - - `FAILED`: 실패 - - ## 캐싱 - - Redis를 통한 Job 상태 저장 - - 키: `job:{jobId}` + /internal/jobs/{jobId}/status: + get: + tags: + - Internal API + summary: 작업 상태 조회 + description: Redis에 저장된 AI 추천 작업 상태 조회 (Event Service에서 호출) operationId: getJobStatus - security: - - bearerAuth: [] + x-user-story: UFR-AI-010 + x-controller: InternalJobController parameters: - name: jobId in: path required: true - description: Job ID schema: type: string - example: "job_ai_20250122_001" + description: Job ID + example: "job-ai-evt001-20251023103000" responses: - "200": - description: Job 상태 조회 성공 + '200': + description: 작업 상태 조회 성공 content: application/json: schema: - oneOf: - - $ref: "#/components/schemas/TrendAnalysisJobResponse" - - $ref: "#/components/schemas/EventRecommendationJobResponse" + $ref: '#/components/schemas/JobStatusResponse' examples: processing: summary: 처리 중 value: - jobId: "job_ai_20250122_001" + jobId: "job-ai-evt001-20251023103000" status: "PROCESSING" - message: "트렌드 분석 중입니다" progress: 50 - createdAt: "2025-01-22T10:05:00Z" - estimatedCompletionTime: "2025-01-22T10:05:30Z" - trendCompleted: - summary: 트렌드 분석 완료 + message: "AI 추천 생성 중" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + completed: + summary: 완료 value: - jobId: "job_ai_20250122_001" + jobId: "job-ai-evt001-20251023103000" status: "COMPLETED" - message: "트렌드 분석이 완료되었습니다" - result: - industryTrends: - successfulEventTypes: - - type: "할인 이벤트" - successRate: 85 - - type: "경품 추첨" - successRate: 78 - popularPrizes: - - prize: "커피 쿠폰" - preferenceScore: 92 - - prize: "현금 할인" - preferenceScore: 88 - effectiveParticipationMethods: - - method: "간단한 설문조사" - engagementRate: 75 - regionalCharacteristics: - successRate: 82 - demographicProfile: - ageGroups: - - range: "20-29" - percentage: 35 - - range: "30-39" - percentage: 40 - genderDistribution: - male: 45 - female: 55 - seasonalPatterns: - currentSeason: "겨울" - recommendedEventTypes: - - "따뜻한 음료 할인" - - "연말 감사 이벤트" - specialOccasions: - - occasion: "설날" - daysUntil: 15 - completedAt: "2025-01-22T10:05:25Z" - recommendationCompleted: - summary: 이벤트 추천 완료 - value: - jobId: "job_ai_20250122_002" - status: "COMPLETED" - message: "이벤트 추천이 완료되었습니다" - result: - recommendations: - - option: 1 - title: "신규 고객 환영 커피 쿠폰 증정" - budget: "low" - prize: - name: "아메리카노 쿠폰" - quantity: 100 - estimatedCost: 300000 - participationMethod: - type: "간단한 설문조사" - difficulty: "low" - description: "매장 방문 후 QR 코드 스캔 및 간단한 설문" - estimatedParticipants: 150 - estimatedROI: 250 - promotionalTexts: - - "따뜻한 커피 한 잔으로 시작하는 하루!" - - "신규 방문 고객님께 특별한 선물" - hashtags: - - "#강남맛집" - - "#커피쿠폰" - - "#신규고객환영" - - option: 2 - title: "런치 세트 20% 할인 이벤트" - budget: "medium" - prize: - name: "런치 세트 할인권" - quantity: 50 - estimatedCost: 500000 - participationMethod: - type: "재방문 미션" - difficulty: "medium" - description: "첫 방문 후 리뷰 작성 시 할인권 제공" - estimatedParticipants: 80 - estimatedROI: 320 - promotionalTexts: - - "점심 시간, 특별한 할인 기회!" - - "리뷰 남기고 할인받자" - hashtags: - - "#강남맛집" - - "#런치할인" - - "#점심특가" - - option: 3 - title: "디너 코스 무료 업그레이드 추첨" - budget: "high" - prize: - name: "디너 코스 업그레이드권" - quantity: 10 - estimatedCost: 1000000 - participationMethod: - type: "바이럴 확산" - difficulty: "high" - description: "SNS 공유 및 친구 태그 3명 이상" - estimatedParticipants: 200 - estimatedROI: 450 - promotionalTexts: - - "프리미엄 디너 코스로 업그레이드!" - - "친구와 함께 즐기는 특별한 저녁" - hashtags: - - "#강남맛집" - - "#디너코스" - - "#프리미엄디너" - completedAt: "2025-01-22T10:05:35Z" + progress: 100 + message: "AI 추천 완료" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + completedAt: "2025-10-23T10:35:00Z" + processingTimeMs: 295000 failed: - summary: 처리 실패 + summary: 실패 value: - jobId: "job_ai_20250122_003" + jobId: "job-ai-evt001-20251023103000" status: "FAILED" - message: "AI API 오류로 인해 처리에 실패했습니다" - error: - code: "AI_API_ERROR" - detail: "External AI API timeout after 30 seconds" - failedAt: "2025-01-22T10:05:45Z" - "404": - $ref: "#/components/responses/NotFound" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + progress: 0 + message: "Claude API timeout" + errorMessage: "Claude API timeout after 5 minutes" + createdAt: "2025-10-23T10:30:00Z" + startedAt: "2025-10-23T10:30:05Z" + failedAt: "2025-10-23T10:35:05Z" + retryCount: 3 + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /internal/recommendations/{eventId}: + get: + tags: + - Internal API + summary: AI 추천 결과 조회 + description: Redis에 캐시된 AI 추천 결과 조회 (Event Service에서 호출) + operationId: getRecommendation + x-user-story: UFR-AI-010 + x-controller: InternalRecommendationController + parameters: + - name: eventId + in: path + required: true + schema: + type: string + description: 이벤트 ID + example: "evt-001" + responses: + '200': + description: 추천 결과 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/AIRecommendationResult' + example: + eventId: "evt-001" + trendAnalysis: + industryTrends: + - keyword: "프리미엄 디저트" + relevance: 0.85 + description: "고급 디저트 카페 트렌드 증가" + regionalTrends: + - keyword: "핫플레이스" + relevance: 0.78 + description: "강남 신논현역 주변 유동인구 증가" + seasonalTrends: + - keyword: "가을 시즌" + relevance: 0.92 + description: "가을 시즌 한정 메뉴 선호도 증가" + recommendations: + - optionNumber: 1 + concept: "프리미엄 경험형" + title: "가을 한정 시그니처 디저트 페어링 이벤트" + description: "가을 제철 재료를 활용한 시그니처 디저트와 음료 페어링 체험" + targetAudience: "20-30대 여성, SNS 활동적인 고객" + duration: + recommendedDays: 14 + recommendedPeriod: "10월 중순 ~ 11월 초" + mechanics: + type: "EXPERIENCE" + details: "디저트+음료 페어링 세트 주문 시 인스타그램 업로드 고객에게 다음 방문 시 사용 가능한 10% 할인권 제공" + promotionChannels: + - "Instagram" + - "카카오톡 채널" + - "네이버 플레이스" + estimatedCost: + min: 300000 + max: 500000 + breakdown: + material: 200000 + promotion: 150000 + discount: 150000 + expectedMetrics: + newCustomers: + min: 50 + max: 80 + repeatVisits: + min: 30 + max: 50 + revenueIncrease: + min: 15.0 + max: 25.0 + roi: + min: 120.0 + max: 180.0 + socialEngagement: + estimatedPosts: 100 + estimatedReach: 5000 + differentiator: "프리미엄 경험 제공으로 고객 만족도와 SNS 바이럴 효과 극대화" + generatedAt: "2025-10-23T10:35:00Z" + expiresAt: "2025-10-24T10:35:00Z" + aiProvider: "CLAUDE" + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: | - JWT 토큰 기반 인증 - - User Service에서 발급한 JWT 토큰 사용 - - 헤더: `Authorization: Bearer {token}` - schemas: - TrendAnalysisRequest: + # ==================== Health Check ==================== + HealthCheckResponse: type: object + description: 서비스 헬스체크 응답 required: - - eventDraftId + - status + - timestamp + - services + properties: + status: + type: string + enum: [UP, DOWN, DEGRADED] + description: 전체 서비스 상태 + example: UP + timestamp: + type: string + format: date-time + description: 체크 시각 + example: "2025-10-23T10:30:00Z" + services: + type: object + description: 개별 서비스 상태 + required: + - kafka + - redis + - claude_api + - circuit_breaker + properties: + kafka: + type: string + enum: [UP, DOWN] + description: Kafka 연결 상태 + example: UP + redis: + type: string + enum: [UP, DOWN] + description: Redis 연결 상태 + example: UP + claude_api: + type: string + enum: [UP, DOWN, CIRCUIT_OPEN] + description: Claude API 상태 + example: UP + gpt4_api: + type: string + enum: [UP, DOWN, CIRCUIT_OPEN] + description: GPT-4 API 상태 (선택) + example: UP + circuit_breaker: + type: string + enum: [CLOSED, OPEN, HALF_OPEN] + description: Circuit Breaker 상태 + example: CLOSED + + # ==================== Kafka Job Message (문서화만) ==================== + KafkaAIJobMessage: + type: object + description: | + **Kafka Topic**: `ai-event-generation-job` + **Consumer Group**: `ai-service-consumers` + **처리 방식**: 비동기 + **최대 처리 시간**: 5분 + + AI 이벤트 생성 요청 메시지 + required: + - jobId + - eventId + - objective - industry - region - - purpose properties: - eventDraftId: + jobId: type: string - description: 이벤트 초안 ID (Event Service에서 생성) - example: "evt_draft_001" + description: Job 고유 ID + example: "job-ai-evt001-20251023103000" + eventId: + type: string + description: 이벤트 ID (Event Service에서 생성) + example: "evt-001" + objective: + type: string + description: 이벤트 목적 + enum: + - "신규 고객 유치" + - "재방문 유도" + - "매출 증대" + - "브랜드 인지도 향상" + example: "신규 고객 유치" industry: type: string description: 업종 - enum: - - 음식점 - - 카페 - - 소매점 - - 뷰티/미용 - - 의료/헬스케어 - - 기타 example: "음식점" region: type: string description: 지역 (시/구/동) example: "서울 강남구" - purpose: - type: string - description: 이벤트 목적 - enum: - - 신규 고객 유치 - - 재방문 유도 - - 매출 증대 - - 인지도 향상 - example: "신규 고객 유치" - storeInfo: - $ref: "#/components/schemas/StoreInfo" - - EventRecommendationRequest: - type: object - required: - - eventDraftId - - purpose - - industry - - region - - storeInfo - properties: - eventDraftId: - type: string - description: 이벤트 초안 ID - example: "evt_draft_001" - purpose: - type: string - description: 이벤트 목적 - enum: - - 신규 고객 유치 - - 재방문 유도 - - 매출 증대 - - 인지도 향상 - example: "신규 고객 유치" - industry: - type: string - description: 업종 - example: "음식점" - region: - type: string - description: 지역 - example: "서울 강남구" - storeInfo: - $ref: "#/components/schemas/StoreInfo" - trendAnalysisJobId: - type: string - description: | - 트렌드 분석 Job ID (선택) - - 제공 시 해당 트렌드 분석 결과 재사용 - - 미제공 시 새로운 트렌드 분석 수행 - example: "job_ai_20250122_001" - - StoreInfo: - type: object - required: - - storeName - properties: storeName: type: string - description: 매장명 + description: 매장명 (선택) example: "맛있는 고깃집" - storeSize: + targetAudience: type: string - description: 매장 크기 - enum: - - 소형 - - 중형 - - 대형 - example: "중형" - monthlyRevenue: + description: 목표 고객층 (선택) + example: "20-30대 여성" + budget: type: integer - format: int64 - description: 월 평균 매출 (원) - minimum: 0 - example: 30000000 + description: 예산 (원) (선택) + example: 500000 + requestedAt: + type: string + format: date-time + description: 요청 시각 + example: "2025-10-23T10:30:00Z" - JobCreatedResponse: + # ==================== AI Recommendation Result ==================== + AIRecommendationResult: type: object - required: - - jobId - - status - - message - properties: - jobId: - type: string - description: Job ID (폴링 조회용) - example: "job_ai_20250122_001" - status: - type: string - enum: - - PENDING - - PROCESSING - description: Job 상태 - example: "PROCESSING" - message: - type: string - description: 상태 메시지 - example: "트렌드 분석이 진행 중입니다" - estimatedCompletionTime: - type: string - format: date-time - description: 예상 완료 시간 (ISO 8601) - example: "2025-01-22T10:05:30Z" + description: | + **Redis Key**: `ai:recommendation:{eventId}` + **TTL**: 86400초 (24시간) - TrendAnalysisJobResponse: - type: object - required: - - jobId - - status - - message - - createdAt - properties: - jobId: - type: string - description: Job ID - example: "job_ai_20250122_001" - status: - type: string - enum: - - PENDING - - PROCESSING - - COMPLETED - - FAILED - description: Job 상태 - example: "COMPLETED" - message: - type: string - description: 상태 메시지 - example: "트렌드 분석이 완료되었습니다" - progress: - type: integer - minimum: 0 - maximum: 100 - description: 진행률 (%) - example: 100 - result: - $ref: "#/components/schemas/TrendAnalysisResult" - error: - $ref: "#/components/schemas/JobError" - createdAt: - type: string - format: date-time - description: Job 생성 시간 - example: "2025-01-22T10:05:00Z" - estimatedCompletionTime: - type: string - format: date-time - description: 예상 완료 시간 (PROCESSING 상태일 때만) - example: "2025-01-22T10:05:30Z" - completedAt: - type: string - format: date-time - description: Job 완료 시간 (COMPLETED 상태일 때만) - example: "2025-01-22T10:05:25Z" - failedAt: - type: string - format: date-time - description: Job 실패 시간 (FAILED 상태일 때만) - example: "2025-01-22T10:05:45Z" - - EventRecommendationJobResponse: - type: object - required: - - jobId - - status - - message - - createdAt - properties: - jobId: - type: string - description: Job ID - example: "job_ai_20250122_002" - status: - type: string - enum: - - PENDING - - PROCESSING - - COMPLETED - - FAILED - description: Job 상태 - example: "COMPLETED" - message: - type: string - description: 상태 메시지 - example: "이벤트 추천이 완료되었습니다" - progress: - type: integer - minimum: 0 - maximum: 100 - description: 진행률 (%) - example: 100 - result: - $ref: "#/components/schemas/EventRecommendationResult" - error: - $ref: "#/components/schemas/JobError" - createdAt: - type: string - format: date-time - description: Job 생성 시간 - example: "2025-01-22T10:05:00Z" - estimatedCompletionTime: - type: string - format: date-time - description: 예상 완료 시간 (PROCESSING 상태일 때만) - example: "2025-01-22T10:05:30Z" - completedAt: - type: string - format: date-time - description: Job 완료 시간 (COMPLETED 상태일 때만) - example: "2025-01-22T10:05:35Z" - failedAt: - type: string - format: date-time - description: Job 실패 시간 (FAILED 상태일 때만) - example: "2025-01-22T10:05:45Z" - - TrendAnalysisResult: - type: object - required: - - industryTrends - - regionalCharacteristics - - seasonalPatterns - properties: - industryTrends: - $ref: "#/components/schemas/IndustryTrends" - regionalCharacteristics: - $ref: "#/components/schemas/RegionalCharacteristics" - seasonalPatterns: - $ref: "#/components/schemas/SeasonalPatterns" - - IndustryTrends: - type: object - required: - - successfulEventTypes - - popularPrizes - - effectiveParticipationMethods - properties: - successfulEventTypes: - type: array - description: 최근 성공한 이벤트 유형 (최대 5개) - items: - type: object - required: - - type - - successRate - properties: - type: - type: string - description: 이벤트 유형 - example: "할인 이벤트" - successRate: - type: integer - minimum: 0 - maximum: 100 - description: 성공률 (%) - example: 85 - popularPrizes: - type: array - description: 고객 선호 경품 Top 5 - items: - type: object - required: - - prize - - preferenceScore - properties: - prize: - type: string - description: 경품명 - example: "커피 쿠폰" - preferenceScore: - type: integer - minimum: 0 - maximum: 100 - description: 선호도 점수 - example: 92 - effectiveParticipationMethods: - type: array - description: 효과적인 참여 방법 - items: - type: object - required: - - method - - engagementRate - properties: - method: - type: string - description: 참여 방법 - example: "간단한 설문조사" - engagementRate: - type: integer - minimum: 0 - maximum: 100 - description: 참여율 (%) - example: 75 - - RegionalCharacteristics: - type: object - required: - - successRate - - demographicProfile - properties: - successRate: - type: integer - minimum: 0 - maximum: 100 - description: 해당 지역 이벤트 성공률 (%) - example: 82 - demographicProfile: - type: object - required: - - ageGroups - - genderDistribution - properties: - ageGroups: - type: array - description: 연령대별 분포 - items: - type: object - required: - - range - - percentage - properties: - range: - type: string - description: 연령대 - example: "20-29" - percentage: - type: integer - minimum: 0 - maximum: 100 - description: 비율 (%) - example: 35 - genderDistribution: - type: object - required: - - male - - female - properties: - male: - type: integer - minimum: 0 - maximum: 100 - description: 남성 비율 (%) - example: 45 - female: - type: integer - minimum: 0 - maximum: 100 - description: 여성 비율 (%) - example: 55 - - SeasonalPatterns: - type: object - required: - - currentSeason - - recommendedEventTypes - properties: - currentSeason: - type: string - description: 현재 계절 - enum: - - 봄 - - 여름 - - 가을 - - 겨울 - example: "겨울" - recommendedEventTypes: - type: array - description: 계절별 추천 이벤트 유형 - items: - type: string - example: - - "따뜻한 음료 할인" - - "연말 감사 이벤트" - specialOccasions: - type: array - description: 다가오는 특별 이벤트 (명절, 기념일 등) - items: - type: object - required: - - occasion - - daysUntil - properties: - occasion: - type: string - description: 특별 이벤트명 - example: "설날" - daysUntil: - type: integer - description: 남은 일수 - example: 15 - - EventRecommendationResult: - type: object + AI 이벤트 추천 결과 required: + - eventId + - trendAnalysis - recommendations + - generatedAt + - aiProvider properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-001" + trendAnalysis: + $ref: '#/components/schemas/TrendAnalysis' recommendations: type: array - description: 3가지 이벤트 추천안 + description: 추천 이벤트 기획안 (3개) minItems: 3 maxItems: 3 items: - $ref: "#/components/schemas/EventRecommendation" + $ref: '#/components/schemas/EventRecommendation' + generatedAt: + type: string + format: date-time + description: 생성 시각 + example: "2025-10-23T10:35:00Z" + expiresAt: + type: string + format: date-time + description: 캐시 만료 시각 (생성 시각 + 24시간) + example: "2025-10-24T10:35:00Z" + aiProvider: + type: string + enum: [CLAUDE, GPT4] + description: 사용된 AI 제공자 + example: "CLAUDE" + + TrendAnalysis: + type: object + description: 트렌드 분석 결과 (업종/지역/시즌) + required: + - industryTrends + - regionalTrends + - seasonalTrends + properties: + industryTrends: + type: array + description: 업종 트렌드 키워드 (최대 5개) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + description: 트렌드 키워드 + example: "프리미엄 디저트" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + description: 연관도 (0-1) + example: 0.85 + description: + type: string + description: 트렌드 설명 + example: "고급 디저트 카페 트렌드 증가" + regionalTrends: + type: array + description: 지역 트렌드 키워드 (최대 5개) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + example: "핫플레이스" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.78 + description: + type: string + example: "강남 신논현역 주변 유동인구 증가" + seasonalTrends: + type: array + description: 시즌 트렌드 키워드 (최대 5개) + maxItems: 5 + items: + type: object + required: + - keyword + - relevance + - description + properties: + keyword: + type: string + example: "가을 시즌" + relevance: + type: number + format: float + minimum: 0 + maximum: 1 + example: 0.92 + description: + type: string + example: "가을 시즌 한정 메뉴 선호도 증가" EventRecommendation: type: object + description: 이벤트 추천안 (차별화된 3가지 옵션) required: - - option + - optionNumber + - concept - title - - budget - - prize - - participationMethod - - estimatedParticipants - - estimatedROI - - promotionalTexts - - hashtags + - description + - targetAudience + - duration + - mechanics + - promotionChannels + - estimatedCost + - expectedMetrics + - differentiator properties: - option: + optionNumber: type: integer - description: 옵션 번호 (1, 2, 3) - enum: [1, 2, 3] + description: 옵션 번호 (1-3) + minimum: 1 + maximum: 3 example: 1 + concept: + type: string + description: 이벤트 컨셉 + example: "프리미엄 경험형" title: type: string - description: 이벤트 제목 (수정 가능) - maxLength: 50 - example: "신규 고객 환영 커피 쿠폰 증정" - budget: + description: 이벤트 제목 + maxLength: 100 + example: "가을 한정 시그니처 디저트 페어링 이벤트" + description: type: string - description: 예산 수준 - enum: - - low - - medium - - high - example: "low" - prize: + description: 이벤트 설명 + maxLength: 500 + example: "가을 제철 재료를 활용한 시그니처 디저트와 음료 페어링 체험" + targetAudience: + type: string + description: 목표 고객층 + example: "20-30대 여성, SNS 활동적인 고객" + duration: type: object + description: 이벤트 기간 required: - - name - - quantity - - estimatedCost + - recommendedDays properties: - name: - type: string - description: 경품명 (수정 가능) - example: "아메리카노 쿠폰" - quantity: + recommendedDays: type: integer + description: 권장 진행 일수 minimum: 1 - description: 경품 수량 - example: 100 - estimatedCost: - type: integer - minimum: 0 - description: 예상 비용 (원) - example: 300000 - participationMethod: + example: 14 + recommendedPeriod: + type: string + description: 권장 진행 시기 + example: "10월 중순 ~ 11월 초" + mechanics: type: object + description: 이벤트 메커니즘 required: - type - - difficulty - - description + - details properties: type: type: string - description: 참여 방법 유형 - example: "간단한 설문조사" - difficulty: + enum: [DISCOUNT, GIFT, STAMP, EXPERIENCE, LOTTERY, COMBO] + description: 이벤트 유형 + example: "EXPERIENCE" + details: type: string - description: 난이도 - enum: - - low - - medium - - high - example: "low" - description: - type: string - description: 참여 방법 상세 설명 - example: "매장 방문 후 QR 코드 스캔 및 간단한 설문" - estimatedParticipants: - type: integer - minimum: 0 - description: 예상 참여자 수 - example: 150 - estimatedROI: - type: integer - minimum: 0 - description: 예상 투자 대비 수익률 (%) - example: 250 - promotionalTexts: + description: 상세 메커니즘 + maxLength: 500 + example: "디저트+음료 페어링 세트 주문 시 인스타그램 업로드 고객에게 다음 방문 시 사용 가능한 10% 할인권 제공" + promotionChannels: type: array - description: 홍보 문구 (5개) - minItems: 5 + description: 추천 홍보 채널 (최대 5개) maxItems: 5 items: type: string example: - - "따뜻한 커피 한 잔으로 시작하는 하루!" - - "신규 방문 고객님께 특별한 선물" - - "강남 최고의 고깃집에서 만나요" - - "지금 바로 참여하세요!" - - "한정 수량! 서두르세요!" - hashtags: - type: array - description: SNS 해시태그 (자동 생성) - items: - type: string - example: - - "#강남맛집" - - "#커피쿠폰" - - "#신규고객환영" + - "Instagram" + - "카카오톡 채널" + - "네이버 플레이스" + estimatedCost: + type: object + description: 예상 비용 + required: + - min + - max + properties: + min: + type: integer + description: 최소 비용 (원) + minimum: 0 + example: 300000 + max: + type: integer + description: 최대 비용 (원) + minimum: 0 + example: 500000 + breakdown: + type: object + description: 비용 구성 + properties: + material: + type: integer + description: 재료비 (원) + example: 200000 + promotion: + type: integer + description: 홍보비 (원) + example: 150000 + discount: + type: integer + description: 할인 비용 (원) + example: 150000 + expectedMetrics: + $ref: '#/components/schemas/ExpectedMetrics' + differentiator: + type: string + description: 다른 옵션과의 차별점 + maxLength: 500 + example: "프리미엄 경험 제공으로 고객 만족도와 SNS 바이럴 효과 극대화, 브랜드 이미지 향상에 집중" - JobError: + ExpectedMetrics: type: object + description: 예상 성과 지표 + required: + - newCustomers + - revenueIncrease + - roi + properties: + newCustomers: + type: object + description: 신규 고객 수 + required: + - min + - max + properties: + min: + type: integer + minimum: 0 + example: 50 + max: + type: integer + minimum: 0 + example: 80 + repeatVisits: + type: object + description: 재방문 고객 수 (선택) + properties: + min: + type: integer + minimum: 0 + example: 30 + max: + type: integer + minimum: 0 + example: 50 + revenueIncrease: + type: object + description: 매출 증가율 (%) + required: + - min + - max + properties: + min: + type: number + format: float + minimum: 0 + example: 15.0 + max: + type: number + format: float + minimum: 0 + example: 25.0 + roi: + type: object + description: ROI - 투자 대비 수익률 (%) + required: + - min + - max + properties: + min: + type: number + format: float + minimum: 0 + example: 120.0 + max: + type: number + format: float + minimum: 0 + example: 180.0 + socialEngagement: + type: object + description: SNS 참여도 (선택) + properties: + estimatedPosts: + type: integer + description: 예상 게시물 수 + minimum: 0 + example: 100 + estimatedReach: + type: integer + description: 예상 도달 수 + minimum: 0 + example: 5000 + + # ==================== Job Status ==================== + JobStatusResponse: + type: object + description: | + **Redis Key**: `ai:job:status:{jobId}` + **TTL**: 86400초 (24시간) + + 작업 상태 응답 + required: + - jobId + - status + - progress + - message + - createdAt + properties: + jobId: + type: string + description: Job ID + example: "job-ai-evt001-20251023103000" + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: 작업 상태 + example: "COMPLETED" + progress: + type: integer + minimum: 0 + maximum: 100 + description: 진행률 (%) + example: 100 + message: + type: string + description: 상태 메시지 + example: "AI 추천 완료" + eventId: + type: string + description: 이벤트 ID + example: "evt-001" + createdAt: + type: string + format: date-time + description: 작업 생성 시각 + example: "2025-10-23T10:30:00Z" + startedAt: + type: string + format: date-time + description: 작업 시작 시각 + example: "2025-10-23T10:30:05Z" + completedAt: + type: string + format: date-time + description: 작업 완료 시각 (완료 시) + example: "2025-10-23T10:35:00Z" + failedAt: + type: string + format: date-time + description: 작업 실패 시각 (실패 시) + example: "2025-10-23T10:35:05Z" + errorMessage: + type: string + description: 에러 메시지 (실패 시) + example: "Claude API timeout after 5 minutes" + retryCount: + type: integer + description: 재시도 횟수 + minimum: 0 + example: 0 + processingTimeMs: + type: integer + description: 처리 시간 (밀리초) + minimum: 0 + example: 295000 + + # ==================== Error Response ==================== + ErrorResponse: + type: object + description: 에러 응답 required: - code - - detail + - message + - timestamp properties: code: type: string description: 에러 코드 enum: - - AI_API_ERROR - - TIMEOUT + - AI_SERVICE_ERROR + - JOB_NOT_FOUND + - RECOMMENDATION_NOT_FOUND + - REDIS_ERROR + - KAFKA_ERROR - CIRCUIT_BREAKER_OPEN - - INVALID_PARAMETERS - INTERNAL_ERROR - example: "AI_API_ERROR" - detail: - type: string - description: 에러 상세 메시지 - example: "External AI API timeout after 30 seconds" - - ErrorResponse: - type: object - required: - - error - - message - - timestamp - properties: - error: - type: string - description: 에러 타입 - example: "BAD_REQUEST" + example: "JOB_NOT_FOUND" message: type: string description: 에러 메시지 - example: "필수 파라미터가 누락되었습니다" - details: - type: array - description: 상세 에러 정보 - items: - type: object - properties: - field: - type: string - description: 에러 필드 - example: "industry" - message: - type: string - description: 필드별 에러 메시지 - example: "업종은 필수 입력 항목입니다" + example: "작업을 찾을 수 없습니다" timestamp: type: string format: date-time - description: 에러 발생 시간 - example: "2025-01-22T10:05:00Z" + description: 에러 발생 시각 + example: "2025-10-23T10:30:00Z" + details: + type: object + description: 추가 에러 상세 + additionalProperties: true + example: + jobId: "job-ai-evt001-20251023103000" responses: - BadRequest: - description: 잘못된 요청 - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - example: - error: "BAD_REQUEST" - message: "필수 파라미터가 누락되었습니다" - details: - - field: "industry" - message: "업종은 필수 입력 항목입니다" - timestamp: "2025-01-22T10:05:00Z" - - Unauthorized: - description: 인증 실패 - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - example: - error: "UNAUTHORIZED" - message: "유효하지 않은 인증 토큰입니다" - timestamp: "2025-01-22T10:05:00Z" - NotFound: description: 리소스를 찾을 수 없음 content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" + $ref: '#/components/schemas/ErrorResponse' example: - error: "NOT_FOUND" - message: "Job을 찾을 수 없습니다" - timestamp: "2025-01-22T10:05:00Z" - - TooManyRequests: - description: 요청 제한 초과 - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" - example: - error: "TOO_MANY_REQUESTS" - message: "요청 제한을 초과했습니다. 잠시 후 다시 시도해주세요" - timestamp: "2025-01-22T10:05:00Z" - headers: - Retry-After: - description: 재시도 가능 시간 (초) - schema: - type: integer - example: 60 + code: "JOB_NOT_FOUND" + message: "작업을 찾을 수 없습니다" + timestamp: "2025-10-23T10:30:00Z" InternalServerError: description: 서버 내부 오류 content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" + $ref: '#/components/schemas/ErrorResponse' example: - error: "INTERNAL_SERVER_ERROR" + code: "INTERNAL_ERROR" message: "서버 내부 오류가 발생했습니다" - timestamp: "2025-01-22T10:05:00Z" + timestamp: "2025-10-23T10:30:00Z" + +# ==================== 기술 구성 문서화 ==================== +x-technical-specifications: + circuit-breaker: + claude-api: + failureThreshold: 5 + successThreshold: 2 + timeout: 300000 + resetTimeout: 60000 + fallbackStrategy: CACHED_RECOMMENDATION + gpt4-api: + failureThreshold: 5 + successThreshold: 2 + timeout: 300000 + resetTimeout: 60000 + fallbackStrategy: CACHED_RECOMMENDATION + + redis-cache: + patterns: + recommendation: "ai:recommendation:{eventId}" + jobStatus: "ai:job:status:{jobId}" + fallback: "ai:fallback:{industry}:{region}" + ttl: + recommendation: 86400 + jobStatus: 86400 + fallback: 604800 + + kafka: + topics: + input: "ai-event-generation-job" + consumer: + groupId: "ai-service-consumers" + maxRetries: 3 + retryBackoffMs: 5000 + maxPollRecords: 10 + sessionTimeoutMs: 30000 + + external-apis: + claude: + endpoint: "https://api.anthropic.com/v1/messages" + model: "claude-3-5-sonnet-20241022" + maxTokens: 4096 + timeout: 300000 + gpt4: + endpoint: "https://api.openai.com/v1/chat/completions" + model: "gpt-4-turbo-preview" + maxTokens: 4096 + timeout: 300000 diff --git a/design/backend/api/analytics-service-api.yaml b/design/backend/api/analytics-service-api.yaml index 42124bb..6fb198a 100644 --- a/design/backend/api/analytics-service-api.yaml +++ b/design/backend/api/analytics-service-api.yaml @@ -2,941 +2,1049 @@ openapi: 3.0.3 info: title: Analytics Service API description: | - KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Analytics Service API - - 이벤트 성과 분석 및 통합 대시보드를 제공하는 서비스입니다. + 실시간 효과 측정 및 통합 대시보드를 제공하는 Analytics Service API **주요 기능:** - - 실시간 성과 분석 대시보드 (UFR-ANAL-010) - - 채널별 성과 추적 - - ROI 계산 및 비교 분석 - - 참여자 프로필 분석 + - 이벤트 성과 대시보드 실시간 조회 + - 채널별 성과 분석 및 비교 + - 시간대별 참여 추이 분석 + - 투자 대비 수익률(ROI) 상세 분석 - **데이터 소스:** - - Participation Service: 참여자 데이터 - - Distribution Service: 채널별 노출 수 - - 외부 API: 우리동네TV, 지니TV, SNS 통계 - - POS 시스템: 매출 데이터 (연동 시) + **Kafka Event Subscriptions:** + - EventCreated: 이벤트 통계 초기화 + - ParticipantRegistered: 실시간 참여자 수 업데이트 + - DistributionCompleted: 배포 통계 업데이트 + + **External API Integration:** + - 우리동네TV API (조회수) + - 지니TV API (광고 노출 수) + - SNS APIs (좋아요, 댓글, 공유 수) + - Circuit Breaker with fallback to cached data + + **Caching Strategy:** + - Redis cache with 5-minute TTL + - Cache-Aside pattern for dashboard data + - Real-time updates via Kafka event subscription version: 1.0.0 contact: name: Analytics Service Team - email: analytics@kt-event-service.com + email: analytics@kt-event.com servers: - - url: https://api.kt-event-service.com/analytics/v1 - description: Production Server - - url: https://dev-api.kt-event-service.com/analytics/v1 - description: Development Server - - url: http://localhost:8084/api + - url: http://localhost:8086 description: Local Development Server + - url: https://api-dev.kt-event.com/analytics + description: Development Server + - url: https://api.kt-event.com/analytics + description: Production Server tags: - name: Analytics - description: 성과 분석 대시보드 API - - name: Health - description: 서비스 헬스 체크 + description: 이벤트 성과 분석 및 대시보드 API + - name: Channels + description: 채널별 성과 분석 API + - name: Timeline + description: 시간대별 분석 API + - name: ROI + description: 투자 대비 수익률 분석 API paths: - /health: - get: - tags: - - Health - summary: 헬스 체크 - description: Analytics Service의 상태를 확인합니다. - operationId: healthCheck - responses: - '200': - description: 서비스 정상 - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "UP" - service: - type: string - example: "analytics-service" - timestamp: - type: string - format: date-time - example: "2025-10-22T10:00:00Z" - - /events/{eventId}/analytics: + /api/events/{eventId}/analytics: get: tags: - Analytics - summary: 이벤트 성과 분석 대시보드 조회 + summary: 성과 대시보드 조회 description: | - 특정 이벤트의 실시간 성과 분석 데이터를 조회합니다. - - **유저스토리:** UFR-ANAL-010 - 성과 분석 대시보드 - - **주요 기능:** - - 4개 요약 카드 (참여자 수, 노출 수, ROI, 매출 증가율) - - 채널별 성과 분석 - - 시간대별 참여 추이 - - 참여자 프로필 분석 - - 비교 분석 (업종 평균, 이전 이벤트) - - **캐싱 전략:** - - Redis Cache-Aside 패턴 - - TTL: 300초 (5분) - - Cache HIT 시 응답 시간: 약 0.5초 - - Cache MISS 시 응답 시간: 약 3초 - - **데이터 업데이트:** - - 실시간 업데이트: Kafka 이벤트 구독 - - EventCreated: 통계 초기화 - - ParticipantRegistered: 참여자 수 증가 - - DistributionCompleted: 배포 통계 업데이트 + 이벤트의 전체 성과를 통합하여 조회합니다. + - 실시간 참여자 수 + - 총 도달 수 (조회수, 노출 수) + - 참여율, 전환율 + - 투자 대비 수익률 (ROI) + - 채널별 성과 요약 operationId: getEventAnalytics - security: - - BearerAuth: [] + x-user-story: UFR-ANAL-010 + x-controller: AnalyticsDashboardController parameters: - name: eventId in: path required: true - description: 이벤트 ID (UUID) + description: 이벤트 ID schema: type: string - format: uuid - example: "550e8400-e29b-41d4-a716-446655440000" + example: "evt_2025012301" + - name: startDate + in: query + required: false + description: 조회 시작 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + - name: endDate + in: query + required: false + description: 조회 종료 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + - name: refresh + in: query + required: false + description: 캐시 갱신 여부 (true인 경우 외부 API 호출) + schema: + type: boolean + default: false responses: '200': - description: 대시보드 데이터 조회 성공 + description: 성과 대시보드 조회 성공 content: application/json: schema: - $ref: '#/components/schemas/DashboardResponse' - examples: - success: - summary: 성공 응답 예시 - value: - eventId: "550e8400-e29b-41d4-a716-446655440000" - storeId: "660e8400-e29b-41d4-a716-446655440001" - eventTitle: "신규 고객 환영 이벤트" - summaryCards: - totalParticipants: - count: 1234 - targetGoal: 1000 - achievementRate: 123.4 - dailyChange: 150 - totalViews: - count: 17200 - yesterdayChange: 5.2 - channelBreakdown: - - channel: "우리동네TV" - views: 5000 - - channel: "지니TV" - views: 10000 - - channel: "Instagram" - views: 2000 - - channel: "Naver Blog" - views: 200 - roi: - value: 250.0 - industryAverage: 180.0 - comparisonRate: 138.9 - totalCost: 1000000 - totalRevenue: 3500000 - breakEvenStatus: "ACHIEVED" - salesGrowth: - rate: 15.2 - beforeEventSales: 5000000 - afterEventSales: 5760000 - periodComparison: "이벤트 전후 7일 비교" - channelPerformance: - - channel: "우리동네TV" - views: 5000 - participants: 400 - conversionRate: 8.0 - costPerAcquisition: 2500 - status: "SUCCESS" - - channel: "지니TV" - views: 10000 - participants: 500 - conversionRate: 5.0 - costPerAcquisition: 4000 - status: "SUCCESS" - - channel: "Instagram" - views: 2000 - participants: 200 - conversionRate: 10.0 - costPerAcquisition: 1000 - status: "SUCCESS" - - channel: "Naver Blog" - views: 200 - participants: 100 - conversionRate: 50.0 - costPerAcquisition: 500 - status: "SUCCESS" - - channel: "Kakao Channel" - views: 0 - participants: 34 - conversionRate: 0 - costPerAcquisition: 0 - status: "SUCCESS" - participationTrend: - timeUnit: "DAILY" - dataPoints: - - timestamp: "2025-10-15T00:00:00Z" - participantCount: 100 - - timestamp: "2025-10-16T00:00:00Z" - participantCount: 250 - - timestamp: "2025-10-17T00:00:00Z" - participantCount: 400 - - timestamp: "2025-10-18T00:00:00Z" - participantCount: 600 - - timestamp: "2025-10-19T00:00:00Z" - participantCount: 850 - - timestamp: "2025-10-20T00:00:00Z" - participantCount: 1050 - - timestamp: "2025-10-21T00:00:00Z" - participantCount: 1234 - peakTime: - timestamp: "2025-10-20T18:00:00Z" - participantCount: 200 - roiAnalysis: - totalCost: - prizeCost: 500000 - channelCosts: - - channel: "우리동네TV" - cost: 100000 - - channel: "지니TV" - cost: 200000 - - channel: "Instagram" - cost: 50000 - - channel: "Naver Blog" - cost: 50000 - - channel: "Kakao Channel" - cost: 0 - otherCosts: 100000 - total: 1000000 - expectedRevenue: - salesIncrease: 760000 - newCustomerLTV: 2740000 - total: 3500000 - roiCalculation: - formula: "(수익 - 비용) / 비용 × 100" - roi: 250.0 - breakEvenPoint: - required: 1000000 - achieved: 3500000 - status: "ACHIEVED" - participantProfile: - ageDistribution: - - ageGroup: "10대" - count: 50 - percentage: 4.1 - - ageGroup: "20대" - count: 300 - percentage: 24.3 - - ageGroup: "30대" - count: 450 - percentage: 36.5 - - ageGroup: "40대" - count: 300 - percentage: 24.3 - - ageGroup: "50대 이상" - count: 134 - percentage: 10.8 - genderDistribution: - - gender: "남성" - count: 600 - percentage: 48.6 - - gender: "여성" - count: 634 - percentage: 51.4 - regionDistribution: - - region: "서울" - count: 500 - percentage: 40.5 - - region: "경기" - count: 400 - percentage: 32.4 - - region: "기타" - count: 334 - percentage: 27.1 - timeDistribution: - - timeSlot: "오전 (06:00-12:00)" - count: 200 - percentage: 16.2 - - timeSlot: "오후 (12:00-18:00)" - count: 500 - percentage: 40.5 - - timeSlot: "저녁 (18:00-24:00)" - count: 500 - percentage: 40.5 - - timeSlot: "새벽 (00:00-06:00)" - count: 34 - percentage: 2.8 - comparativeAnalysis: - industryComparison: - - metric: "참여율" - myValue: 7.2 - industryAverage: 5.5 - percentageDifference: 30.9 - - metric: "ROI" - myValue: 250.0 - industryAverage: 180.0 - percentageDifference: 38.9 - - metric: "전환율" - myValue: 8.5 - industryAverage: 6.0 - percentageDifference: 41.7 - previousEventComparison: - - metric: "참여자 수" - currentValue: 1234 - previousBest: 1000 - improvementRate: 23.4 - - metric: "ROI" - currentValue: 250.0 - previousBest: 200.0 - improvementRate: 25.0 - - metric: "매출 증가율" - currentValue: 15.2 - previousBest: 12.0 - improvementRate: 26.7 - lastUpdated: "2025-10-22T10:30:00Z" - cacheStatus: "HIT" - '400': - description: 잘못된 요청 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - invalidEventId: - summary: 잘못된 이벤트 ID - value: - error: "INVALID_REQUEST" - message: "유효하지 않은 이벤트 ID 형식입니다." - timestamp: "2025-10-22T10:00:00Z" - '401': - description: 인증 실패 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - unauthorized: - summary: 인증되지 않은 요청 - value: - error: "UNAUTHORIZED" - message: "인증이 필요합니다." - timestamp: "2025-10-22T10:00:00Z" - '403': - description: 권한 없음 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - forbidden: - summary: 권한 없음 - value: - error: "FORBIDDEN" - message: "해당 이벤트의 통계를 조회할 권한이 없습니다." - timestamp: "2025-10-22T10:00:00Z" + $ref: '#/components/schemas/AnalyticsDashboard' '404': description: 이벤트를 찾을 수 없음 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - notFound: - summary: 이벤트 미존재 - value: - error: "EVENT_NOT_FOUND" - message: "해당 이벤트를 찾을 수 없습니다." - timestamp: "2025-10-22T10:00:00Z" '500': - description: 서버 내부 오류 + description: 서버 오류 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - internalError: - summary: 서버 오류 - value: - error: "INTERNAL_SERVER_ERROR" - message: "서버 내부 오류가 발생했습니다." - timestamp: "2025-10-22T10:00:00Z" - '503': - description: 외부 API 서비스 이용 불가 + + /api/events/{eventId}/analytics/channels: + get: + tags: + - Channels + summary: 채널별 성과 분석 + description: | + 각 배포 채널별 성과를 상세하게 분석합니다. + - 우리동네TV 조회수 + - 지니TV 광고 노출 수 + - SNS 반응 수 (좋아요, 댓글, 공유) + - 채널별 참여율 및 전환율 + - 채널별 ROI + operationId: getChannelAnalytics + x-user-story: UFR-ANAL-010 + x-controller: ChannelAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: channels + in: query + required: false + description: 조회할 채널 목록 (쉼표로 구분, 미지정 시 전체) + schema: + type: string + example: "우리동네TV,지니TV,SNS" + - name: sortBy + in: query + required: false + description: 정렬 기준 + schema: + type: string + enum: + - views + - participants + - engagement_rate + - conversion_rate + - roi + default: roi + - name: order + in: query + required: false + description: 정렬 순서 + schema: + type: string + enum: + - asc + - desc + default: desc + responses: + '200': + description: 채널별 성과 분석 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ChannelAnalyticsResponse' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/events/{eventId}/analytics/timeline: + get: + tags: + - Timeline + summary: 시간대별 참여 추이 + description: | + 이벤트 기간 동안의 시간대별 참여 추이를 분석합니다. + - 시간대별 참여자 수 + - 시간대별 조회수 + - 피크 타임 분석 + - 추세 분석 (증가/감소) + operationId: getTimelineAnalytics + x-user-story: UFR-ANAL-010 + x-controller: TimelineAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: interval + in: query + required: false + description: 시간 간격 단위 + schema: + type: string + enum: + - hourly + - daily + - weekly + default: daily + - name: startDate + in: query + required: false + description: 조회 시작 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + - name: endDate + in: query + required: false + description: 조회 종료 날짜 (ISO 8601 format) + schema: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + - name: metrics + in: query + required: false + description: 조회할 지표 목록 (쉼표로 구분) + schema: + type: string + example: "participants,views,engagement" + responses: + '200': + description: 시간대별 참여 추이 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/TimelineAnalyticsResponse' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/events/{eventId}/analytics/roi: + get: + tags: + - ROI + summary: 투자 대비 수익률 상세 + description: | + 이벤트의 투자 대비 수익률을 상세하게 분석합니다. + - 총 투자 비용 (제작비, 배포비, 운영비) + - 예상 매출 증대 + - ROI 계산 + - 비용 대비 참여자 수 (CPA) + - 비용 대비 전환 수 (CPC) + operationId: getRoiAnalytics + x-user-story: UFR-ANAL-010 + x-controller: RoiAnalyticsController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_2025012301" + - name: includeProjection + in: query + required: false + description: 예상 수익 포함 여부 + schema: + type: boolean + default: true + responses: + '200': + description: ROI 상세 분석 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RoiAnalyticsResponse' + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 서버 오류 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' - examples: - externalServiceUnavailable: - summary: 외부 서비스 장애 - value: - error: "EXTERNAL_SERVICE_UNAVAILABLE" - message: "일부 채널 데이터를 불러올 수 없습니다. Fallback 데이터를 사용합니다." - timestamp: "2025-10-22T10:00:00Z" components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: JWT 토큰 기반 인증. Authorization 헤더에 "Bearer {token}" 형식으로 전달합니다. - schemas: - DashboardResponse: + AnalyticsDashboard: type: object - required: - - eventId - - storeId - - eventTitle - - summaryCards - - channelPerformance - - participationTrend - - roiAnalysis - - participantProfile - - comparativeAnalysis - - lastUpdated + description: 이벤트 성과 대시보드 properties: eventId: type: string - format: uuid description: 이벤트 ID - example: "550e8400-e29b-41d4-a716-446655440000" - storeId: - type: string - format: uuid - description: 매장 ID - example: "660e8400-e29b-41d4-a716-446655440001" + example: "evt_2025012301" eventTitle: type: string description: 이벤트 제목 - example: "신규 고객 환영 이벤트" - summaryCards: - $ref: '#/components/schemas/SummaryCards' + example: "신년맞이 20% 할인 이벤트" + period: + $ref: '#/components/schemas/PeriodInfo' + summary: + $ref: '#/components/schemas/AnalyticsSummary' channelPerformance: type: array - description: 채널별 성과 분석 + description: 채널별 성과 요약 items: - $ref: '#/components/schemas/ChannelPerformance' - participationTrend: - $ref: '#/components/schemas/ParticipationTrend' - roiAnalysis: - $ref: '#/components/schemas/ROIAnalysis' - participantProfile: - $ref: '#/components/schemas/ParticipantProfile' - comparativeAnalysis: - $ref: '#/components/schemas/ComparativeAnalysis' - lastUpdated: + $ref: '#/components/schemas/ChannelSummary' + roi: + $ref: '#/components/schemas/RoiSummary' + lastUpdatedAt: type: string format: date-time - description: 마지막 업데이트 시각 - example: "2025-10-22T10:30:00Z" - cacheStatus: + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + dataSource: type: string - enum: [HIT, MISS] - description: 캐시 상태 (HIT/MISS) - example: "HIT" + description: 데이터 출처 + enum: + - real-time + - cached + - fallback + example: "cached" + required: + - eventId + - eventTitle + - period + - summary + - lastUpdatedAt - SummaryCards: + PeriodInfo: type: object - description: 4개 요약 카드 + description: 조회 기간 정보 + properties: + startDate: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + endDate: + type: string + format: date-time + example: "2025-01-31T23:59:59Z" + durationDays: + type: integer + description: 기간 (일) + example: 30 + required: + - startDate + - endDate + + AnalyticsSummary: + type: object + description: 성과 요약 + properties: + totalParticipants: + type: integer + description: 총 참여자 수 + example: 15420 + totalViews: + type: integer + description: 총 조회수 + example: 125300 + totalReach: + type: integer + description: 총 도달 수 + example: 98500 + engagementRate: + type: number + format: double + description: 참여율 (%) + example: 12.3 + conversionRate: + type: number + format: double + description: 전환율 (%) + example: 3.8 + averageEngagementTime: + type: integer + description: 평균 참여 시간 (초) + example: 145 + socialInteractions: + $ref: '#/components/schemas/SocialInteractionStats' required: - totalParticipants - totalViews - - roi - - salesGrowth - properties: - totalParticipants: - $ref: '#/components/schemas/TotalParticipantsCard' - totalViews: - $ref: '#/components/schemas/TotalViewsCard' - roi: - $ref: '#/components/schemas/ROICard' - salesGrowth: - $ref: '#/components/schemas/SalesGrowthCard' - - TotalParticipantsCard: - type: object - description: 총 참여자 수 카드 - required: - - count - - targetGoal - - achievementRate - properties: - count: - type: integer - description: 현재 참여자 수 - example: 1234 - targetGoal: - type: integer - description: 목표 참여자 수 - example: 1000 - achievementRate: - type: number - format: float - description: 목표 대비 달성률 (%) - example: 123.4 - dailyChange: - type: integer - description: 전일 대비 증가 수 - example: 150 - - TotalViewsCard: - type: object - description: 총 노출 수 카드 - required: - - count - - yesterdayChange - properties: - count: - type: integer - description: 총 노출 수 (채널별 노출 합계) - example: 17200 - yesterdayChange: - type: number - format: float - description: 전일 대비 증감률 (%) - example: 5.2 - channelBreakdown: - type: array - description: 채널별 노출 수 분포 - items: - type: object - properties: - channel: - type: string - description: 채널명 - example: "우리동네TV" - views: - type: integer - description: 노출 수 - example: 5000 - - ROICard: - type: object - description: 예상 투자 대비 수익률 카드 - required: - - value - - industryAverage - - comparisonRate - - totalCost - - totalRevenue - - breakEvenStatus - properties: - value: - type: number - format: float - description: 실시간 ROI (%) - example: 250.0 - industryAverage: - type: number - format: float - description: 업종 평균 ROI (%) - example: 180.0 - comparisonRate: - type: number - format: float - description: 업종 평균 대비 비율 (%) - example: 138.9 - totalCost: - type: integer - description: 총 비용 (원) - example: 1000000 - totalRevenue: - type: integer - description: 총 수익 (원) - example: 3500000 - breakEvenStatus: - type: string - enum: [ACHIEVED, NOT_ACHIEVED] - description: 손익분기점 달성 여부 - example: "ACHIEVED" - - SalesGrowthCard: - type: object - description: 매출 증가율 카드 - required: - - rate - properties: - rate: - type: number - format: float - description: 매출 증가율 (%) - example: 15.2 - beforeEventSales: - type: integer - description: 이벤트 전 매출 (원) - example: 5000000 - afterEventSales: - type: integer - description: 이벤트 후 매출 (원) - example: 5760000 - periodComparison: - type: string - description: 비교 기간 설명 - example: "이벤트 전후 7일 비교" - - ChannelPerformance: - type: object - description: 채널별 성과 분석 - required: - - channel - - views - - participants + - totalReach + - engagementRate - conversionRate - - costPerAcquisition - - status + + SocialInteractionStats: + type: object + description: SNS 반응 통계 properties: - channel: + likes: + type: integer + description: 좋아요 수 + example: 3450 + comments: + type: integer + description: 댓글 수 + example: 890 + shares: + type: integer + description: 공유 수 + example: 1250 + required: + - likes + - comments + - shares + + ChannelSummary: + type: object + description: 채널별 성과 요약 + properties: + channelName: type: string description: 채널명 example: "우리동네TV" views: type: integer - description: 노출 수 - example: 5000 + description: 조회수 + example: 45000 participants: type: integer description: 참여자 수 - example: 400 + example: 5500 + engagementRate: + type: number + format: double + description: 참여율 (%) + example: 12.2 conversionRate: type: number - format: float + format: double description: 전환율 (%) - example: 8.0 - costPerAcquisition: - type: integer - description: 비용 대비 효율 (CPA, 원) - example: 2500 - status: - type: string - enum: [SUCCESS, FAILED, PENDING] - description: 배포 상태 - example: "SUCCESS" - - ParticipationTrend: - type: object - description: 시간대별 참여 추이 - required: - - timeUnit - - dataPoints - properties: - timeUnit: - type: string - enum: [HOURLY, DAILY] - description: 시간 단위 (시간별/일별) - example: "DAILY" - dataPoints: - type: array - description: 시간대별 데이터 포인트 - items: - type: object - properties: - timestamp: - type: string - format: date-time - description: 시각 - example: "2025-10-15T00:00:00Z" - participantCount: - type: integer - description: 참여자 수 - example: 100 - peakTime: - type: object - description: 피크 시간대 - properties: - timestamp: - type: string - format: date-time - description: 피크 시각 - example: "2025-10-20T18:00:00Z" - participantCount: - type: integer - description: 피크 참여자 수 - example: 200 - - ROIAnalysis: - type: object - description: 투자 대비 수익률 상세 분석 - required: - - totalCost - - expectedRevenue - - roiCalculation - - breakEvenPoint - properties: - totalCost: - $ref: '#/components/schemas/TotalCost' - expectedRevenue: - $ref: '#/components/schemas/ExpectedRevenue' - roiCalculation: - $ref: '#/components/schemas/ROICalculation' - breakEvenPoint: - $ref: '#/components/schemas/BreakEvenPoint' - - TotalCost: - type: object - description: 총 비용 산출 - required: - - prizeCost - - channelCosts - - otherCosts - - total - properties: - prizeCost: - type: integer - description: 경품 비용 (원) - example: 500000 - channelCosts: - type: array - description: 채널별 플랫폼 비용 - items: - type: object - properties: - channel: - type: string - description: 채널명 - example: "우리동네TV" - cost: - type: integer - description: 비용 (원) - example: 100000 - otherCosts: - type: integer - description: 기타 비용 (원) - example: 100000 - total: - type: integer - description: 총 비용 (원) - example: 1000000 - - ExpectedRevenue: - type: object - description: 예상 수익 산출 - required: - - salesIncrease - - newCustomerLTV - - total - properties: - salesIncrease: - type: integer - description: 매출 증가액 (원) - example: 760000 - newCustomerLTV: - type: integer - description: 신규 고객 LTV (원) - example: 2740000 - total: - type: integer - description: 총 예상 수익 (원) - example: 3500000 - - ROICalculation: - type: object - description: ROI 계산 - required: - - formula - - roi - properties: - formula: - type: string - description: ROI 계산 공식 - example: "(수익 - 비용) / 비용 × 100" + example: 4.1 roi: type: number - format: float + format: double description: ROI (%) - example: 250.0 - - BreakEvenPoint: - type: object - description: 손익분기점 + example: 280.5 required: - - required - - achieved - - status + - channelName + - views + - participants + + RoiSummary: + type: object + description: ROI 요약 properties: - required: + totalInvestment: + type: number + format: double + description: 총 투자 비용 (원) + example: 5000000 + expectedRevenue: + type: number + format: double + description: 예상 매출 증대 (원) + example: 19025000 + netProfit: + type: number + format: double + description: 순이익 (원) + example: 14025000 + roi: + type: number + format: double + description: ROI (%) + example: 280.5 + costPerAcquisition: + type: number + format: double + description: 고객 획득 비용 (CPA, 원) + example: 324.35 + required: + - totalInvestment + - expectedRevenue + - roi + + ChannelAnalyticsResponse: + type: object + description: 채널별 성과 분석 응답 + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_2025012301" + channels: + type: array + description: 채널별 상세 분석 + items: + $ref: '#/components/schemas/ChannelAnalytics' + comparison: + $ref: '#/components/schemas/ChannelComparison' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + required: + - eventId + - channels + - lastUpdatedAt + + ChannelAnalytics: + type: object + description: 채널별 상세 분석 + properties: + channelName: + type: string + description: 채널명 + example: "우리동네TV" + channelType: + type: string + description: 채널 유형 + enum: + - LOCAL_TV + - CABLE_TV + - SNS + - MOBILE_APP + example: "LOCAL_TV" + metrics: + $ref: '#/components/schemas/ChannelMetrics' + performance: + $ref: '#/components/schemas/ChannelPerformance' + costs: + $ref: '#/components/schemas/ChannelCosts' + externalApiStatus: + type: string + description: 외부 API 연동 상태 + enum: + - success + - fallback + - failed + example: "success" + required: + - channelName + - channelType + - metrics + - performance + + ChannelMetrics: + type: object + description: 채널 지표 + properties: + impressions: type: integer - description: 손익분기점 (원) - example: 1000000 - achieved: + description: 노출 수 + example: 120000 + views: type: integer - description: 달성 금액 (원) - example: 3500000 - status: - type: string - enum: [ACHIEVED, NOT_ACHIEVED] - description: 달성 여부 - example: "ACHIEVED" - - ParticipantProfile: - type: object - description: 참여자 프로필 분석 + description: 조회수 + example: 45000 + clicks: + type: integer + description: 클릭 수 + example: 8900 + participants: + type: integer + description: 참여자 수 + example: 5500 + conversions: + type: integer + description: 전환 수 + example: 1850 + socialInteractions: + $ref: '#/components/schemas/SocialInteractionStats' required: - - ageDistribution - - genderDistribution - - regionDistribution - - timeDistribution - properties: - ageDistribution: - type: array - description: 연령대별 분포 - items: - type: object - properties: - ageGroup: - type: string - description: 연령대 - example: "20대" - count: - type: integer - description: 인원 수 - example: 300 - percentage: - type: number - format: float - description: 비율 (%) - example: 24.3 - genderDistribution: - type: array - description: 성별 분포 - items: - type: object - properties: - gender: - type: string - description: 성별 - example: "남성" - count: - type: integer - description: 인원 수 - example: 600 - percentage: - type: number - format: float - description: 비율 (%) - example: 48.6 - regionDistribution: - type: array - description: 지역별 분포 - items: - type: object - properties: - region: - type: string - description: 지역 - example: "서울" - count: - type: integer - description: 인원 수 - example: 500 - percentage: - type: number - format: float - description: 비율 (%) - example: 40.5 - timeDistribution: - type: array - description: 참여 시간대 분석 - items: - type: object - properties: - timeSlot: - type: string - description: 시간대 - example: "오전 (06:00-12:00)" - count: - type: integer - description: 참여자 수 - example: 200 - percentage: - type: number - format: float - description: 비율 (%) - example: 16.2 + - views + - participants - ComparativeAnalysis: + ChannelPerformance: type: object - description: 비교 분석 - required: - - industryComparison - - previousEventComparison + description: 채널 성과 지표 properties: - industryComparison: - type: array - description: 업종 평균과 비교 - items: - type: object - properties: - metric: - type: string - description: 지표명 - example: "참여율" - myValue: - type: number - format: float - description: 내 값 - example: 7.2 - industryAverage: - type: number - format: float - description: 업종 평균 - example: 5.5 - percentageDifference: - type: number - format: float - description: 차이율 (%) - example: 30.9 - previousEventComparison: - type: array - description: 내 이전 이벤트와 비교 - items: - type: object - properties: - metric: - type: string - description: 지표명 - example: "참여자 수" - currentValue: - type: number - format: float - description: 현재 값 - example: 1234 - previousBest: - type: number - format: float - description: 이전 최고 기록 - example: 1000 - improvementRate: - type: number - format: float - description: 개선율 (%) - example: 23.4 + clickThroughRate: + type: number + format: double + description: 클릭률 (CTR, %) + example: 7.4 + engagementRate: + type: number + format: double + description: 참여율 (%) + example: 12.2 + conversionRate: + type: number + format: double + description: 전환율 (%) + example: 4.1 + averageEngagementTime: + type: integer + description: 평균 참여 시간 (초) + example: 165 + bounceRate: + type: number + format: double + description: 이탈율 (%) + example: 35.8 + required: + - engagementRate + - conversionRate - ErrorResponse: + ChannelCosts: type: object - description: 에러 응답 - required: - - error - - message - - timestamp + description: 채널별 비용 properties: - error: + distributionCost: + type: number + format: double + description: 배포 비용 (원) + example: 1500000 + costPerView: + type: number + format: double + description: 조회당 비용 (CPV, 원) + example: 33.33 + costPerClick: + type: number + format: double + description: 클릭당 비용 (CPC, 원) + example: 168.54 + costPerAcquisition: + type: number + format: double + description: 고객 획득 비용 (CPA, 원) + example: 272.73 + roi: + type: number + format: double + description: ROI (%) + example: 295.3 + required: + - distributionCost + - roi + + ChannelComparison: + type: object + description: 채널 간 비교 분석 + properties: + bestPerforming: + type: object + description: 최고 성과 채널 + properties: + byViews: + type: string + example: "우리동네TV" + byEngagement: + type: string + example: "지니TV" + byRoi: + type: string + example: "SNS" + averageMetrics: + type: object + description: 전체 채널 평균 지표 + properties: + engagementRate: + type: number + format: double + example: 11.5 + conversionRate: + type: number + format: double + example: 3.9 + roi: + type: number + format: double + example: 275.8 + + TimelineAnalyticsResponse: + type: object + description: 시간대별 참여 추이 응답 + properties: + eventId: type: string - description: 에러 코드 - example: "INVALID_REQUEST" - message: + description: 이벤트 ID + example: "evt_2025012301" + interval: type: string - description: 에러 메시지 - example: "유효하지 않은 요청입니다." + description: 시간 간격 + enum: + - hourly + - daily + - weekly + example: "daily" + dataPoints: + type: array + description: 시간대별 데이터 + items: + $ref: '#/components/schemas/TimelineDataPoint' + trends: + $ref: '#/components/schemas/TrendAnalysis' + peakTimes: + type: array + description: 피크 타임 정보 + items: + $ref: '#/components/schemas/PeakTimeInfo' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + required: + - eventId + - interval + - dataPoints + - lastUpdatedAt + + TimelineDataPoint: + type: object + description: 시간대별 데이터 포인트 + properties: timestamp: type: string format: date-time - description: 에러 발생 시각 - example: "2025-10-22T10:00:00Z" + description: 시간 + example: "2025-01-15T00:00:00Z" + participants: + type: integer + description: 참여자 수 + example: 450 + views: + type: integer + description: 조회수 + example: 3500 + engagement: + type: integer + description: 참여 행동 수 + example: 280 + conversions: + type: integer + description: 전환 수 + example: 45 + cumulativeParticipants: + type: integer + description: 누적 참여자 수 + example: 5450 + required: + - timestamp + - participants + - views + + TrendAnalysis: + type: object + description: 추세 분석 + properties: + overallTrend: + type: string + description: 전체 추세 + enum: + - increasing + - stable + - decreasing + example: "increasing" + growthRate: + type: number + format: double + description: 증가율 (%) + example: 15.3 + projectedParticipants: + type: integer + description: 예상 참여자 수 (기간 종료 시점) + example: 18500 + peakPeriod: + type: string + description: 피크 기간 + example: "2025-01-15 ~ 2025-01-18" + required: + - overallTrend + + PeakTimeInfo: + type: object + description: 피크 타임 정보 + properties: + timestamp: + type: string + format: date-time + description: 피크 시간 + example: "2025-01-15T14:00:00Z" + metric: + type: string + description: 피크 지표 + enum: + - participants + - views + - engagement + - conversions + example: "participants" + value: + type: integer + description: 피크 값 + example: 1250 + description: + type: string + description: 피크 설명 + example: "주말 오후 최대 참여" + + RoiAnalyticsResponse: + type: object + description: ROI 상세 분석 응답 + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_2025012301" + investment: + $ref: '#/components/schemas/InvestmentDetails' + revenue: + $ref: '#/components/schemas/RevenueDetails' + roi: + $ref: '#/components/schemas/RoiCalculation' + costEfficiency: + $ref: '#/components/schemas/CostEfficiency' + projection: + $ref: '#/components/schemas/RevenueProjection' + lastUpdatedAt: + type: string + format: date-time + description: 마지막 업데이트 시간 + example: "2025-01-23T10:30:00Z" + required: + - eventId + - investment + - revenue + - roi + - lastUpdatedAt + + InvestmentDetails: + type: object + description: 투자 비용 상세 + properties: + contentCreation: + type: number + format: double + description: 콘텐츠 제작비 (원) + example: 2000000 + distribution: + type: number + format: double + description: 배포 비용 (원) + example: 2500000 + operation: + type: number + format: double + description: 운영 비용 (원) + example: 500000 + total: + type: number + format: double + description: 총 투자 비용 (원) + example: 5000000 + breakdown: + type: array + description: 채널별 비용 상세 + items: + type: object + properties: + channelName: + type: string + example: "우리동네TV" + cost: + type: number + format: double + example: 1500000 + required: + - total + + RevenueDetails: + type: object + description: 수익 상세 + properties: + directSales: + type: number + format: double + description: 직접 매출 (원) + example: 12500000 + expectedSales: + type: number + format: double + description: 예상 추가 매출 (원) + example: 6525000 + brandValue: + type: number + format: double + description: 브랜드 가치 향상 추정액 (원) + example: 3000000 + total: + type: number + format: double + description: 총 수익 (원) + example: 19025000 + required: + - total + + RoiCalculation: + type: object + description: ROI 계산 + properties: + netProfit: + type: number + format: double + description: 순이익 (원) + example: 14025000 + roiPercentage: + type: number + format: double + description: ROI (%) + example: 280.5 + breakEvenPoint: + type: string + format: date-time + description: 손익분기점 도달 시점 + example: "2025-01-10T15:30:00Z" + paybackPeriod: + type: integer + description: 투자 회수 기간 (일) + example: 9 + required: + - netProfit + - roiPercentage + + CostEfficiency: + type: object + description: 비용 효율성 + properties: + costPerParticipant: + type: number + format: double + description: 참여자당 비용 (원) + example: 324.35 + costPerConversion: + type: number + format: double + description: 전환당 비용 (원) + example: 850.34 + costPerView: + type: number + format: double + description: 조회당 비용 (원) + example: 39.90 + revenuePerParticipant: + type: number + format: double + description: 참여자당 수익 (원) + example: 1234.25 + required: + - costPerParticipant + + RevenueProjection: + type: object + description: 수익 예측 + properties: + currentRevenue: + type: number + format: double + description: 현재 누적 수익 (원) + example: 12500000 + projectedFinalRevenue: + type: number + format: double + description: 예상 최종 수익 (원) + example: 21000000 + confidenceLevel: + type: number + format: double + description: 예측 신뢰도 (%) + example: 85.5 + basedOn: + type: string + description: 예측 기반 + example: "현재 추세 및 과거 유사 이벤트 데이터" + + ErrorResponse: + type: object + description: 오류 응답 + properties: + timestamp: + type: string + format: date-time + description: 오류 발생 시간 + example: "2025-01-23T10:30:00Z" + status: + type: integer + description: HTTP 상태 코드 + example: 404 + error: + type: string + description: 오류 유형 + example: "Not Found" + message: + type: string + description: 오류 메시지 + example: "이벤트를 찾을 수 없습니다." + path: + type: string + description: 요청 경로 + example: "/api/events/evt_2025012301/analytics" + errorCode: + type: string + description: 내부 오류 코드 + example: "ANAL_001" + required: + - timestamp + - status + - error + - message + - path + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰 기반 인증 + +security: + - bearerAuth: [] diff --git a/design/backend/api/content-service-api.yaml b/design/backend/api/content-service-api.yaml index e171d60..46cada2 100644 --- a/design/backend/api/content-service-api.yaml +++ b/design/backend/api/content-service-api.yaml @@ -1,61 +1,91 @@ openapi: 3.0.3 info: title: Content Service API + version: 1.0.0 description: | - KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Content Service API + # KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Content Service API ## 주요 기능 - - SNS 이미지 생성 (UFR-CONT-010) - - 3가지 스타일 이미지 자동 생성 (심플, 화려한, 트렌디) - - AI 기반 이미지 생성 (Stable Diffusion / DALL-E) - - Circuit Breaker 및 Fallback 패턴 적용 - - Redis 캐싱 (TTL 7일) + - **SNS 이미지 생성** (UFR-CONT-010): AI 기반 이벤트 이미지 자동 생성 + - **콘텐츠 편집** (UFR-CONT-020): 생성된 이미지 조회, 재생성, 삭제 + - **3가지 스타일**: 심플(SIMPLE), 화려한(FANCY), 트렌디(TRENDY) + - **3개 플랫폼 최적화**: Instagram (1080x1080), Naver (800x600), Kakao (800x800) + - **Redis 캐싱**: TTL 7일, 동일 eventDraftId 재요청 시 캐시 반환 + - **CDN 이미지 저장**: Azure Blob Storage 기반 - ## 비동기 처리 방식 - - Kafka 기반 Job 처리 - - 폴링 방식으로 결과 조회 - - Event Service와 느슨한 결합 + ## 비동기 처리 아키텍처 + + ### Kafka Job Consumer + **Topic**: `image-generation-job` + + **처리 흐름**: + 1. Kafka에서 이미지 생성 Job 수신 (EventService에서 발행) + 2. Redis에서 AI Service 이벤트 데이터 조회 + 3. Redis 캐시에서 기존 이미지 확인 (동일 eventDraftId) + 4. 외부 이미지 생성 API 호출 (Stable Diffusion / DALL-E) + - **Circuit Breaker**: 5분 타임아웃, 실패율 50% 초과 시 Open + - **Fallback**: Stable Diffusion → DALL-E → 기본 템플릿 이미지 + 5. 생성된 이미지 CDN(Azure Blob) 업로드 + 6. Redis에 이미지 URL 저장 (TTL 7일) + 7. Job 상태 업데이트 (PENDING → PROCESSING → COMPLETED/FAILED) + + **Job Payload Schema**: `ImageGenerationJob` (components/schemas 참조) + + ## 외부 API 연동 + - **Image Generation API**: Stable Diffusion / DALL-E + - **Circuit Breaker**: 5분 타임아웃, 50% 실패율 임계값 + - **CDN**: Azure Blob Storage (이미지 업로드) + + ## 출력 형식 + - 스타일별 3개 × 플랫폼별 3개 = **총 9개 이미지** 생성 (현재는 Instagram만) + - CDN URL 반환 - version: 1.0.0 contact: name: Content Service Team email: content-team@kt.com servers: - - url: https://api.kt-event.com/v1 - description: Production Server - - url: https://api-dev.kt-event.com/v1 + - url: http://localhost:8083 + description: Content Service Local Development Server + - url: https://api-dev.kt-event.com description: Development Server + - url: https://api.kt-event.com + description: Production Server tags: - - name: Images - description: SNS 이미지 생성 및 조회 + - name: Job Status + description: 이미지 생성 작업 상태 조회 (비동기 폴링) + - name: Content Management + description: 생성된 콘텐츠 조회 및 관리 (UFR-CONT-020) + - name: Image Management + description: 이미지 재생성 및 삭제 (UFR-CONT-020) paths: /api/content/images/generate: post: tags: - - Images - summary: SNS 이미지 생성 요청 + - Job Status + summary: SNS 이미지 생성 요청 (비동기) description: | 이벤트 정보를 기반으로 3가지 스타일의 SNS 이미지 생성을 비동기로 요청합니다. ## 처리 방식 - - **비동기 처리**: Kafka image-job 토픽에 Job 발행 - - **폴링 조회**: jobId로 생성 상태 조회 (GET /api/content/images/{jobId}) + - **비동기 처리**: Kafka `image-generation-job` 토픽에 Job 발행 + - **폴링 조회**: jobId로 생성 상태 조회 (GET /api/content/images/jobs/{jobId}) - **캐싱**: 동일한 eventDraftId 재요청 시 캐시 반환 (TTL 7일) ## 생성 스타일 - 1. **심플 스타일**: 깔끔한 디자인, 텍스트 중심 - 2. **화려한 스타일**: 눈에 띄는 디자인, 풍부한 색상 - 3. **트렌디 스타일**: 최신 트렌드, MZ세대 타겟 + 1. **심플 스타일 (SIMPLE)**: 깔끔한 디자인, 텍스트 중심 + 2. **화려한 스타일 (FANCY)**: 눈에 띄는 디자인, 풍부한 색상 + 3. **트렌디 스타일 (TRENDY)**: 최신 트렌드, MZ세대 타겟 ## Resilience 패턴 - - Circuit Breaker (실패율 50% 초과 시 Open) + - Circuit Breaker (5분 타임아웃, 실패율 50% 초과 시 Open) - Fallback (Stable Diffusion → DALL-E → 기본 템플릿) - - Timeout (20초) operationId: generateImages + x-user-story: UFR-CONT-010 + x-controller: ImageGenerationController.generateImages requestBody: required: true content: @@ -152,11 +182,11 @@ paths: security: - BearerAuth: [] - /api/content/images/{jobId}: + /api/content/images/jobs/{jobId}: get: tags: - - Images - summary: 이미지 생성 상태 및 결과 조회 + - Job Status + summary: 이미지 생성 작업 상태 조회 (폴링) description: | jobId로 이미지 생성 상태를 조회합니다. @@ -169,13 +199,15 @@ paths: - **PENDING**: 대기 중 (Kafka Queue에서 대기) - **PROCESSING**: 생성 중 (AI API 호출 진행) - **COMPLETED**: 완료 (3가지 이미지 URL 반환) - - **FAILED**: 실패 (에러 메시지 포함) + - **FAILED**: 실패 (에러 메시지 포함, Fallback 이미지 제공) ## 캐싱 - COMPLETED 상태는 Redis 캐싱 (TTL 7일) - 동일한 eventDraftId 재요청 시 즉시 반환 operationId: getImageGenerationStatus + x-user-story: UFR-CONT-010 + x-controller: ImageGenerationController.getJobStatus parameters: - name: jobId in: path @@ -307,6 +339,337 @@ paths: security: - BearerAuth: [] + /api/content/events/{eventDraftId}: + get: + tags: + - Content Management + summary: 이벤트의 생성된 콘텐츠 조회 + description: | + 특정 이벤트의 생성된 모든 콘텐츠(이미지) 조회 + - Redis 캐시에서 조회 + - TTL 7일 이내 데이터만 조회 가능 + - 캐시 만료 시 404 반환 + + operationId: getContentByEventId + x-user-story: UFR-CONT-020 + x-controller: ContentController.getContentByEventId + parameters: + - name: eventDraftId + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + example: "evt-draft-12345" + + responses: + '200': + description: 콘텐츠 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ContentResponse' + examples: + success: + summary: 콘텐츠 조회 성공 + value: + eventDraftId: "evt-draft-12345" + images: + - imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + - imageId: "img-12345-fancy" + style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:10Z" + - imageId: "img-12345-trendy" + style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:15Z" + totalCount: 3 + createdAt: "2025-10-22T14:30:00Z" + expiresAt: "2025-10-29T14:30:00Z" + + '404': + description: 콘텐츠를 찾을 수 없음 (생성 중이거나 만료됨) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 콘텐츠 없음 + value: + code: "CONTENT_NOT_FOUND" + message: "해당 이벤트의 콘텐츠를 찾을 수 없습니다." + timestamp: "2025-10-22T14:30:00Z" + + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + security: + - BearerAuth: [] + + /api/content/events/{eventDraftId}/images: + get: + tags: + - Content Management + summary: 이벤트의 이미지 목록 조회 (필터링) + description: | + 특정 이벤트의 모든 생성 이미지 목록 조회 + - 스타일별, 플랫폼별 필터링 지원 + + operationId: getImages + x-user-story: UFR-CONT-020 + x-controller: ContentController.getImages + parameters: + - name: eventDraftId + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + example: "evt-draft-12345" + - name: style + in: query + required: false + description: 이미지 스타일 필터 + schema: + type: string + enum: [SIMPLE, FANCY, TRENDY] + example: "SIMPLE" + - name: platform + in: query + required: false + description: 플랫폼 필터 + schema: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + example: "INSTAGRAM" + + responses: + '200': + description: 이미지 목록 조회 성공 + content: + application/json: + schema: + type: object + properties: + eventDraftId: + type: string + totalCount: + type: integer + images: + type: array + items: + $ref: '#/components/schemas/GeneratedImage' + examples: + allImages: + summary: 전체 이미지 조회 + value: + eventDraftId: "evt-draft-12345" + totalCount: 3 + images: + - imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + + '404': + description: 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + security: + - BearerAuth: [] + + /api/content/images/{imageId}: + get: + tags: + - Image Management + summary: 특정 이미지 상세 조회 + description: 이미지 ID로 특정 이미지의 상세 정보 조회 + + operationId: getImageById + x-user-story: UFR-CONT-020 + x-controller: ContentController.getImageById + parameters: + - name: imageId + in: path + required: true + description: 이미지 ID + schema: + type: string + example: "img-12345-simple" + + responses: + '200': + description: 이미지 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/GeneratedImage' + examples: + success: + summary: 이미지 조회 성공 + value: + imageId: "img-12345-simple" + style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + createdAt: "2025-10-22T14:30:05Z" + + '404': + description: 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + security: + - BearerAuth: [] + + delete: + tags: + - Image Management + summary: 생성된 이미지 삭제 + description: | + 특정 이미지 삭제 + - Redis 캐시에서 제거 + - CDN 이미지는 유지 (비용 고려) + + operationId: deleteImage + x-user-story: UFR-CONT-020 + x-controller: ContentController.deleteImage + parameters: + - name: imageId + in: path + required: true + description: 이미지 ID + schema: + type: string + example: "img-12345-simple" + + responses: + '204': + description: 이미지 삭제 성공 + + '404': + description: 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + security: + - BearerAuth: [] + + /api/content/images/{imageId}/regenerate: + post: + tags: + - Image Management + summary: 이미지 재생성 요청 + description: | + 특정 이미지를 재생성 (콘텐츠 편집) + - 동일한 스타일/플랫폼으로 재생성 + - 프롬프트 수정 가능 + - 비동기 처리 (Kafka Job 발행) + + operationId: regenerateImage + x-user-story: UFR-CONT-020 + x-controller: ContentController.regenerateImage + parameters: + - name: imageId + in: path + required: true + description: 이미지 ID + schema: + type: string + example: "img-12345-simple" + + requestBody: + required: false + description: 재생성 옵션 (선택사항) + content: + application/json: + schema: + $ref: '#/components/schemas/ImageRegenerationRequest' + examples: + modifyPrompt: + summary: 프롬프트 수정 + value: + content: "밝은 분위기로 변경" + changeStyle: + summary: 스타일 변경 + value: + style: "FANCY" + + responses: + '202': + description: 재생성 요청 접수 (비동기 처리) + content: + application/json: + schema: + type: object + required: + - message + - jobId + - estimatedTime + properties: + message: + type: string + example: "이미지 재생성 요청이 접수되었습니다" + jobId: + type: string + example: "job-regen-abc123" + estimatedTime: + type: integer + description: 예상 소요 시간 (초) + example: 10 + + '404': + description: 원본 이미지를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + '503': + description: 이미지 생성 서비스 장애 (Circuit Breaker Open) + content: + application/json: + schema: + $ref: '#/components/schemas/CircuitBreakerErrorResponse' + + security: + - BearerAuth: [] + components: securitySchemes: BearerAuth: @@ -316,6 +679,90 @@ components: description: JWT 토큰을 Authorization 헤더에 포함 (Bearer {token}) schemas: + # ======================================== + # Kafka Job Schema (비동기 처리) + # ======================================== + ImageGenerationJob: + type: object + description: | + **Kafka Topic**: `image-generation-job` + + Event Service에서 발행하여 Content Service가 소비하는 Job Payload + - Content Service의 Kafka Consumer가 처리 + - 비동기 이미지 생성 작업 수행 + required: + - jobId + - eventDraftId + - eventInfo + properties: + jobId: + type: string + description: Job ID (작업 추적용) + example: "job-img-abc123" + + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt-draft-12345" + + eventInfo: + type: object + description: 이벤트 정보 (AI Service에서 생성) + required: + - title + - giftName + properties: + title: + type: string + description: 이벤트 제목 + example: "봄맞이 커피 할인 이벤트" + giftName: + type: string + description: 경품명 + example: "아메리카노 1+1" + brandColor: + type: string + description: 브랜드 컬러 (HEX) + pattern: '^#[0-9A-Fa-f]{6}$' + example: "#FF5733" + logoUrl: + type: string + format: uri + description: 로고 이미지 URL (선택) + example: "https://cdn.example.com/logo.png" + + styles: + type: array + description: 생성할 스타일 목록 (기본값 전체) + items: + type: string + enum: [SIMPLE, FANCY, TRENDY] + example: ["SIMPLE", "FANCY", "TRENDY"] + + platforms: + type: array + description: 생성할 플랫폼 목록 (기본값 Instagram) + items: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + example: ["INSTAGRAM"] + + priority: + type: integer + description: 우선순위 (1-10, 높을수록 우선) + minimum: 1 + maximum: 10 + example: 5 + + requestedAt: + type: string + format: date-time + description: 요청 시각 + example: "2025-10-22T14:00:00Z" + + # ======================================== + # Request Schemas + # ======================================== ImageGenerationRequest: type: object required: @@ -546,3 +993,166 @@ components: type: integer description: 재시도 대기 시간 (초, Rate Limiting 에러에서만) example: 60 + + # ======================================== + # Content Management Schemas (UFR-CONT-020) + # ======================================== + ContentResponse: + type: object + description: 이벤트의 생성된 콘텐츠 전체 정보 + required: + - eventDraftId + - images + - totalCount + - createdAt + - expiresAt + properties: + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt-draft-12345" + + images: + type: array + description: 생성된 이미지 목록 + items: + $ref: '#/components/schemas/ImageDetail' + + totalCount: + type: integer + description: 총 이미지 개수 + example: 3 + + createdAt: + type: string + format: date-time + description: 콘텐츠 생성 시각 + example: "2025-10-22T14:30:00Z" + + expiresAt: + type: string + format: date-time + description: 캐시 만료 시각 (TTL 7일) + example: "2025-10-29T14:30:00Z" + + ImageDetail: + type: object + description: 상세 이미지 정보 (생성 시각 포함) + required: + - imageId + - style + - url + - platform + - size + - createdAt + properties: + imageId: + type: string + description: 이미지 ID + example: "img-12345-simple" + + style: + type: string + enum: [SIMPLE, FANCY, TRENDY] + description: 이미지 스타일 + example: "SIMPLE" + + url: + type: string + format: uri + description: CDN 이미지 URL (Azure Blob Storage) + example: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + + platform: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: 플랫폼 + example: "INSTAGRAM" + + size: + type: object + required: + - width + - height + properties: + width: + type: integer + description: 이미지 너비 (픽셀) + example: 1080 + height: + type: integer + description: 이미지 높이 (픽셀) + example: 1080 + + createdAt: + type: string + format: date-time + description: 이미지 생성 시각 + example: "2025-10-22T14:30:05Z" + + fallbackUsed: + type: boolean + description: Fallback 이미지 사용 여부 + example: false + + ImageRegenerationRequest: + type: object + description: 이미지 재생성 요청 (콘텐츠 편집) + properties: + content: + type: string + description: 수정된 프롬프트 (선택사항) + example: "밝은 분위기로 변경" + + style: + type: string + description: 변경할 스타일 (선택사항) + enum: [SIMPLE, FANCY, TRENDY] + example: "FANCY" + + CircuitBreakerErrorResponse: + type: object + description: Circuit Breaker 오류 응답 (외부 API 장애) + required: + - code + - message + - timestamp + - circuitBreakerState + properties: + code: + type: string + description: 에러 코드 + example: "IMAGE_GENERATION_SERVICE_UNAVAILABLE" + + message: + type: string + description: 에러 메시지 + example: "이미지 생성 서비스가 일시적으로 사용 불가능합니다" + + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-10-22T14:30:00Z" + + circuitBreakerState: + type: string + enum: [OPEN, HALF_OPEN, CLOSED] + description: Circuit Breaker 상태 + example: "OPEN" + + fallbackAvailable: + type: boolean + description: Fallback 이미지 사용 가능 여부 + example: true + + fallbackImageUrl: + type: string + format: uri + description: Fallback 템플릿 이미지 URL (사용 가능한 경우) + example: "https://cdn.kt-event.com/templates/default_event.png" + + retryAfter: + type: integer + description: 재시도 가능 시간 (초) + example: 300 diff --git a/design/backend/api/distribution-service-api.yaml b/design/backend/api/distribution-service-api.yaml index 9790274..9f8a8ae 100644 --- a/design/backend/api/distribution-service-api.yaml +++ b/design/backend/api/distribution-service-api.yaml @@ -2,61 +2,73 @@ openapi: 3.0.3 info: title: Distribution Service API description: | - KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 배포 관리 서비스 API + KT AI 기반 소상공인 이벤트 자동 생성 서비스의 다중 채널 배포 관리 API - **주요 기능:** - - 다중 채널 배포 관리 - - 배포 상태 모니터링 - - 채널별 배포 결과 추적 + ## 주요 기능 + - 다중 채널 동시 배포 (우리동네TV, 링고비즈, 지니TV, SNS) + - 배포 상태 실시간 모니터링 + - Circuit Breaker 기반 장애 격리 + - Retry 패턴 및 Fallback 처리 + + ## 배포 채널 + - **우리동네TV**: 영상 콘텐츠 업로드 + - **링고비즈**: 연결음 업데이트 + - **지니TV**: 광고 등록 + - **SNS**: Instagram, Naver Blog, Kakao Channel + + ## Resilience 패턴 + - Circuit Breaker: 채널별 독립적 장애 격리 + - Retry: 지수 백오프 (1s, 2s, 4s) 최대 3회 + - Bulkhead: 리소스 격리 + - Fallback: 실패 채널 스킵 및 알림 - **지원 배포 채널:** - - 우리동네TV - - 링고비즈 (연결음) - - 지니TV 광고 - - Instagram - - Naver Blog - - Kakao Channel version: 1.0.0 contact: - name: KT Event Marketing Team - email: support@kt-event-marketing.com + name: Backend Development Team + email: backend@kt-event-marketing.com servers: - - url: http://localhost:8085 + - url: http://localhost:8083 description: Local Development Server - - url: https://api-dev.kt-event-marketing.com - description: Development Server - - url: https://api.kt-event-marketing.com - description: Production Server + - url: http://distribution-service:8083 + description: Docker/Kubernetes Service + - url: https://api-dev.kt-event-marketing.com/distribution + description: Development Environment + - url: https://api.kt-event-marketing.com/distribution + description: Production Environment tags: - name: Distribution description: 다중 채널 배포 관리 - - name: Status - description: 배포 상태 조회 및 모니터링 + - name: Monitoring + description: 배포 상태 모니터링 paths: /api/distribution/distribute: post: tags: - Distribution - summary: 다중 채널 배포 실행 + summary: 다중 채널 배포 요청 description: | - 선택된 모든 채널에 동시 배포를 실행합니다. + 이벤트 콘텐츠를 선택된 채널들에 동시 배포합니다. - **배포 프로세스:** - 1. 배포 이력 초기화 (상태: PENDING) - 2. 각 채널별 병렬 배포 처리 - 3. 배포 결과 집계 및 저장 - 4. Kafka 이벤트 발행 (Analytics Service 구독) - 5. Redis 캐시 저장 (TTL: 1시간) + ## 처리 흐름 + 1. 배포 요청 검증 (이벤트 ID, 채널 목록, 콘텐츠 데이터) + 2. 채널별 병렬 배포 실행 (1분 이내 완료 목표) + 3. Circuit Breaker로 장애 채널 격리 + 4. 실패 시 Retry (지수 백오프: 1s, 2s, 4s) + 5. Fallback: 실패 채널 스킵 및 알림 + 6. 배포 결과 집계 및 로그 저장 + 7. DistributionCompleted 이벤트 Kafka 발행 - **Sprint 2 제약사항:** - - 외부 API 호출 없음 (Mock 처리) - - 모든 배포 요청은 성공으로 처리 - - 배포 로그만 DB에 기록 + ## Resilience 처리 + - 각 채널별 독립적인 Circuit Breaker 적용 + - 최대 3회 재시도 (지수 백오프) + - 일부 채널 실패 시에도 성공 채널은 유지 + - 실패 채널 정보는 응답에 포함 - **유저스토리:** UFR-DIST-010 + x-user-story: UFR-DIST-010 + x-controller: DistributionController operationId: distributeToChannels requestBody: required: true @@ -165,23 +177,26 @@ paths: /api/distribution/{eventId}/status: get: tags: - - Status + - Monitoring summary: 배포 상태 조회 description: | - 이벤트의 배포 상태를 조회합니다. + 특정 이벤트의 배포 상태를 실시간으로 조회합니다. - **조회 프로세스:** - 1. Cache-Aside 패턴 적용 (Redis 캐시 우선 조회) - 2. 캐시 MISS 시 DB에서 배포 이력 조회 - 3. 진행중(IN_PROGRESS) 상태일 때만 외부 API로 실시간 상태 확인 - 4. Circuit Breaker 패턴 적용 (외부 API 호출 시) - 5. 배포 상태 캐싱 (TTL: 1시간) + ## 조회 정보 + - 전체 배포 상태 (진행중, 완료, 부분성공, 실패) + - 채널별 배포 상태 및 결과 + - 실패 채널 상세 정보 (오류 유형, 재시도 횟수) + - 배포 시작/완료 시간 및 소요 시간 + - 외부 채널 ID 및 배포 URL - **응답 시간:** - - 캐시 HIT: 0.1초 - - 캐시 MISS: 0.5초 ~ 2초 + ## 상태 값 + - **IN_PROGRESS**: 배포 진행 중 + - **COMPLETED**: 모든 채널 배포 완료 + - **PARTIAL_SUCCESS**: 일부 채널 배포 성공 + - **FAILED**: 모든 채널 배포 실패 - **유저스토리:** UFR-DIST-020 + x-user-story: UFR-DIST-020 + x-controller: DistributionController operationId: getDistributionStatus parameters: - name: eventId @@ -285,80 +300,6 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' - /api/distribution/{eventId}/retry: - post: - tags: - - Distribution - summary: 실패한 채널 재시도 - description: | - 실패한 채널에 대해 배포를 재시도합니다. - - **재시도 프로세스:** - 1. 실패한 채널 목록 검증 - 2. 새로운 배포 시도 로그 생성 - 3. Circuit Breaker 및 Retry 로직 적용 - 4. 캐시 무효화 - 5. 재시도 결과 반환 - - **재시도 제한:** - - 최대 재시도 횟수: 3회 - - Circuit Breaker가 OPEN 상태일 경우 30초 대기 후 시도 - operationId: retryDistribution - parameters: - - name: eventId - in: path - required: true - description: 이벤트 ID - schema: - type: string - example: "evt-12345" - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/RetryRequest' - examples: - retryFailedChannels: - summary: 실패한 채널 재시도 - value: - channels: - - "INSTAGRAM" - - "KAKAO_CHANNEL" - responses: - '200': - description: 재시도 완료 - content: - application/json: - schema: - $ref: '#/components/schemas/RetryResponse' - examples: - success: - summary: 재시도 성공 - value: - eventId: "evt-12345" - retryStatus: "COMPLETED" - retriedAt: "2025-11-01T09:05:00Z" - results: - - channel: "INSTAGRAM" - status: "SUCCESS" - postUrl: "https://instagram.com/p/retry-post-id" - - channel: "KAKAO_CHANNEL" - status: "SUCCESS" - messageId: "kakao-retry-12345" - '400': - description: 잘못된 요청 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: 배포 이력을 찾을 수 없음 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - components: schemas: DistributionRequest: @@ -682,58 +623,6 @@ components: description: 마지막 재시도 시각 example: "2025-11-01T08:59:30Z" - RetryRequest: - type: object - required: - - channels - properties: - channels: - type: array - description: 재시도할 채널 목록 - minItems: 1 - items: - type: string - enum: - - WOORIDONGNE_TV - - RINGO_BIZ - - GENIE_TV - - INSTAGRAM - - NAVER_BLOG - - KAKAO_CHANNEL - example: - - "INSTAGRAM" - - "KAKAO_CHANNEL" - - RetryResponse: - type: object - required: - - eventId - - retryStatus - - results - properties: - eventId: - type: string - description: 이벤트 ID - example: "evt-12345" - retryStatus: - type: string - description: 재시도 전체 상태 - enum: - - COMPLETED - - PARTIAL_FAILURE - - FAILED - example: "COMPLETED" - retriedAt: - type: string - format: date-time - description: 재시도 시각 - example: "2025-11-01T09:05:00Z" - results: - type: array - description: 채널별 재시도 결과 - items: - $ref: '#/components/schemas/ChannelResult' - ErrorResponse: type: object required: diff --git a/design/backend/api/event-service-api.yaml b/design/backend/api/event-service-api.yaml index 1430972..3fe6652 100644 --- a/design/backend/api/event-service-api.yaml +++ b/design/backend/api/event-service-api.yaml @@ -1,346 +1,135 @@ openapi: 3.0.3 info: title: Event Service API - version: 1.0.0 description: | - KT AI 기반 소상공인 이벤트 자동 생성 서비스의 Event Service API 명세입니다. + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Event Service API - **주요 기능:** - - 이벤트 대시보드 조회 - - 이벤트 목적 선택 및 초안 생성 - - AI 이벤트 추천 요청 및 결과 조회 - - 이미지 생성 요청 및 결과 조회 - - 콘텐츠 선택 및 편집 - - 최종 승인 및 배포 - - 이벤트 상세 조회 및 목록 관리 + 이벤트 전체 생명주기 관리 (생성, 조회, 수정, 배포, 종료) + - AI 기반 이벤트 추천 및 커스터마이징 + - 이미지 생성 및 편집 오케스트레이션 + - 배포 채널 관리 및 최종 배포 + - 이벤트 상태 관리 (DRAFT, PUBLISHED, ENDED) + version: 1.0.0 + contact: + name: Event Service Team + email: event-service@kt.com servers: - url: http://localhost:8080 - description: 로컬 개발 서버 - - url: https://api-dev.kt-event-marketing.com - description: 개발 서버 - - url: https://api.kt-event-marketing.com - description: 프로덕션 서버 + description: Local development server + - url: https://api-dev.kt-event.com + description: Development server + - url: https://api.kt-event.com + description: Production server + +security: + - bearerAuth: [] tags: - name: Dashboard - description: 대시보드 관리 - - name: EventDraft - description: 이벤트 초안 관리 - - name: AIRecommendation - description: AI 추천 관리 - - name: ContentGeneration - description: 콘텐츠 생성 관리 - - name: EventPublish - description: 이벤트 발행 관리 - - name: EventManagement - description: 이벤트 조회 및 관리 - - name: Job - description: 비동기 작업 관리 + description: 대시보드 및 이벤트 목록 조회 + - name: Event Creation + description: 이벤트 생성 플로우 + - name: Event Management + description: 이벤트 수정, 삭제, 종료 + - name: Job Status + description: 비동기 작업 상태 조회 paths: - /api/events/dashboard: + /api/events: get: tags: - Dashboard - summary: 대시보드 이벤트 목록 조회 + summary: 이벤트 목록 조회 description: | - UFR-EVENT-010: 소상공인의 대시보드에서 진행중/예정/종료된 이벤트를 조회합니다. - - **비즈니스 로직:** - - 상태별로 최대 5개씩 표시 (최신순) - - Redis 캐시 우선 조회 (TTL 1분) - - 참여자 수, 조회수 등 기본 통계 포함 - operationId: getDashboard - security: - - bearerAuth: [] - responses: - '200': - description: 대시보드 데이터 조회 성공 - content: - application/json: - schema: - $ref: '#/components/schemas/DashboardResponse' - '401': - $ref: '#/components/responses/Unauthorized' - '500': - $ref: '#/components/responses/InternalServerError' - - /api/events/purposes: - post: - tags: - - EventDraft - summary: 이벤트 목적 선택 및 초안 생성 - description: | - UFR-EVENT-020: 이벤트 목적을 선택하고 초안을 생성합니다. - - **비즈니스 로직:** - - 목적 유효성 검증 (신규 고객 유치, 재방문 유도, 매출 증대, 인지도 향상) - - EventDraft 엔티티 생성 및 DB 저장 - - Redis 캐시 저장 (TTL 30분) - - Kafka EventDraftCreated 이벤트 발행 - operationId: createEventDraft - security: - - bearerAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateEventDraftRequest' - responses: - '200': - description: 이벤트 초안 생성 성공 - content: - application/json: - schema: - $ref: '#/components/schemas/EventDraftResponse' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '500': - $ref: '#/components/responses/InternalServerError' - - /api/events/{id}/ai-recommendations: - post: - tags: - - AIRecommendation - summary: AI 이벤트 추천 요청 - description: | - UFR-EVENT-030: AI 트렌드 분석 및 이벤트 추천을 요청합니다. - - **비동기 처리:** - - Kafka ai-job-topic에 Job 발행 - - Redis에 Job 상태 저장 (TTL 1시간) - - 202 Accepted 응답 (jobId 반환) - - 클라이언트는 폴링으로 결과 조회 - operationId: requestAIRecommendation - security: - - bearerAuth: [] + 사용자의 이벤트 목록을 조회합니다 (대시보드, 전체보기). + 필터, 검색, 페이징을 지원합니다. + operationId: getEvents + x-user-story: UFR-EVENT-010, UFR-EVENT-070 + x-controller: EventController.getEvents parameters: - - name: id - in: path - required: true - description: 이벤트 초안 ID + - name: status + in: query + description: 이벤트 상태 필터 (DRAFT, PUBLISHED, ENDED) + required: false schema: type: string - format: uuid + enum: [DRAFT, PUBLISHED, ENDED] + example: PUBLISHED + - name: objective + in: query + description: 이벤트 목적 필터 + required: false + schema: + type: string + example: 신규 고객 유치 + - name: search + in: query + description: 검색어 (이벤트명) + required: false + schema: + type: string + example: 봄맞이 + - name: page + in: query + description: 페이지 번호 (0부터 시작) + required: false + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + - name: size + in: query + description: 페이지 크기 + required: false + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + example: 20 + - name: sort + in: query + description: 정렬 기준 (createdAt, startDate, endDate) + required: false + schema: + type: string + enum: [createdAt, startDate, endDate] + default: createdAt + example: createdAt + - name: order + in: query + description: 정렬 순서 (asc, desc) + required: false + schema: + type: string + enum: [asc, desc] + default: desc + example: desc responses: - '202': - description: AI 추천 요청 접수 + '200': + description: 이벤트 목록 조회 성공 content: application/json: schema: - $ref: '#/components/schemas/JobResponse' - '400': - $ref: '#/components/responses/BadRequest' + $ref: '#/components/schemas/EventListResponse' '401': $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' - /api/jobs/{jobId}/status: + /api/events/{eventId}: get: tags: - - Job - summary: Job 상태 조회 (폴링) - description: | - AI 추천 또는 이미지 생성 Job의 상태를 조회합니다. - - **폴링 패턴:** - - AI 추천: 2초 간격, 최대 30초 (15회) - - 이미지 생성: 3초 간격, 최대 30초 (10회) - - 상태: PENDING, PROCESSING, COMPLETED, FAILED - operationId: getJobStatus - security: - - bearerAuth: [] - parameters: - - name: jobId - in: path - required: true - description: Job ID - schema: - type: string - format: uuid - responses: - '200': - description: Job 상태 조회 성공 - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/AIRecommendationJobResult' - - $ref: '#/components/schemas/ImageGenerationJobResult' - '404': - $ref: '#/components/responses/NotFound' - '500': - $ref: '#/components/responses/InternalServerError' - - /api/events/{id}/content-generation: - post: - tags: - - ContentGeneration - summary: 이미지 생성 요청 - description: | - UFR-CONT-010: SNS용 이미지 생성을 요청합니다. - - **비동기 처리:** - - Kafka image-job-topic에 Job 발행 - - Redis에 Job 상태 저장 (TTL 1시간) - - 202 Accepted 응답 (jobId 반환) - - Content Service가 3가지 스타일 이미지 생성 - operationId: requestImageGeneration - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: 이벤트 초안 ID - schema: - type: string - format: uuid - responses: - '202': - description: 이미지 생성 요청 접수 - content: - application/json: - schema: - $ref: '#/components/schemas/JobResponse' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' - '500': - $ref: '#/components/responses/InternalServerError' - - /api/events/drafts/{id}/content: - put: - tags: - - ContentGeneration - summary: 선택한 콘텐츠 저장 - description: | - UFR-CONT-020: 선택한 이미지와 편집한 콘텐츠를 저장합니다. - - **비즈니스 로직:** - - 선택한 이미지 URL 검증 - - 편집 내용 적용 (텍스트, 색상) - - EventDraft 업데이트 - - Redis 캐시 무효화 - operationId: updateEventContent - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: 이벤트 초안 ID - schema: - type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/UpdateContentRequest' - responses: - '200': - description: 콘텐츠 저장 성공 - content: - application/json: - schema: - $ref: '#/components/schemas/EventContentResponse' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' - '500': - $ref: '#/components/responses/InternalServerError' - - /api/events/{id}/publish: - post: - tags: - - EventPublish - summary: 이벤트 최종 승인 및 배포 - description: | - UFR-EVENT-050: 이벤트를 최종 승인하고 배포를 시작합니다. - - **비즈니스 로직:** - - 발행 준비 검증 (목적, AI 추천, 콘텐츠, 채널 선택) - - 상태 변경: DRAFT → APPROVED → ACTIVE - - Kafka EventCreated 이벤트 발행 - - Distribution Service 동기 호출 (Timeout 70초, Circuit Breaker 적용) - - Redis 캐시 무효화 - operationId: publishEvent - security: - - bearerAuth: [] - parameters: - - name: id - in: path - required: true - description: 이벤트 초안 ID - schema: - type: string - format: uuid - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/PublishEventRequest' - responses: - '200': - description: 이벤트 발행 성공 - content: - application/json: - schema: - $ref: '#/components/schemas/PublishEventResponse' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' - '500': - $ref: '#/components/responses/InternalServerError' - '503': - description: Distribution Service 호출 실패 - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - - /api/events/{id}: - get: - tags: - - EventManagement + - Dashboard summary: 이벤트 상세 조회 - description: | - UFR-EVENT-060: 이벤트의 상세 정보를 조회합니다. - - **비즈니스 로직:** - - Redis 캐시 우선 조회 (TTL 5분) - - 경품 정보 및 배포 이력 JOIN 조회 - - 사용자 권한 검증 - operationId: getEventDetail - security: - - bearerAuth: [] + description: 특정 이벤트의 상세 정보를 조회합니다. + operationId: getEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.getEvent parameters: - - name: id - in: path - required: true - description: 이벤트 ID - schema: - type: string - format: uuid + - $ref: '#/components/parameters/EventId' responses: '200': description: 이벤트 상세 조회 성공 @@ -350,75 +139,521 @@ paths: $ref: '#/components/schemas/EventDetailResponse' '401': $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' - /api/events: - get: + put: tags: - - EventManagement - summary: 이벤트 목록 조회 (필터/검색) + - Event Management + summary: 이벤트 수정 description: | - UFR-EVENT-070: 이벤트 목록을 조회하고 필터링/검색합니다. - - **비즈니스 로직:** - - Redis 캐시 우선 조회 (TTL 1분) - - 상태별 필터링 - - 키워드 검색 (제목, 설명) - - 페이지네이션 (기본 20개/페이지) - - 정렬 (최신순, 참여자순, ROI순) - operationId: getEventList - security: - - bearerAuth: [] + 기존 이벤트의 정보를 수정합니다. + DRAFT 상태의 이벤트만 전체 수정 가능하며, + PUBLISHED 상태에서는 제한적 수정만 가능합니다. + operationId: updateEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.updateEvent parameters: - - name: status - in: query - description: 이벤트 상태 필터 - schema: - type: string - enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] - - name: keyword - in: query - description: 검색 키워드 (제목, 설명) - schema: - type: string - - name: page - in: query - description: 페이지 번호 (0부터 시작) - schema: - type: integer - minimum: 0 - default: 0 - - name: size - in: query - description: 페이지 크기 - schema: - type: integer - minimum: 1 - maximum: 100 - default: 20 - - name: sort - in: query - description: 정렬 기준 - schema: - type: string - enum: [createdAt,desc, participantCount,desc, roi,desc] - default: createdAt,desc + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateEventRequest' responses: '200': - description: 이벤트 목록 조회 성공 + description: 이벤트 수정 성공 content: application/json: schema: - $ref: '#/components/schemas/EventListResponse' + $ref: '#/components/schemas/EventDetailResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태로 인해 수정 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_MODIFIABLE + message: PUBLISHED 상태의 이벤트는 제한적으로만 수정 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + delete: + tags: + - Event Management + summary: 이벤트 삭제 + description: | + 이벤트를 삭제합니다. + DRAFT 상태의 이벤트만 삭제 가능합니다. + operationId: deleteEvent + x-user-story: UFR-EVENT-070 + x-controller: EventController.deleteEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '204': + description: 이벤트 삭제 성공 + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태로 인해 삭제 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_DELETABLE + message: DRAFT 상태의 이벤트만 삭제 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/objectives: + post: + tags: + - Event Creation + summary: 이벤트 목적 선택 (Step 1) + description: | + 이벤트 생성 플로우의 첫 단계입니다. + 사용자가 이벤트 목적을 선택하고 DRAFT 상태의 이벤트를 생성합니다. + operationId: selectObjective + x-user-story: UFR-EVENT-020 + x-controller: EventController.selectObjective + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectObjectiveRequest' + responses: + '201': + description: 이벤트 생성 성공 (DRAFT 상태) + content: + application/json: + schema: + $ref: '#/components/schemas/EventCreatedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/ai-recommendations: + post: + tags: + - Event Creation + summary: AI 추천 요청 (Step 2) + description: | + AI 서비스에 이벤트 추천 생성을 요청합니다. + Kafka Job을 발행하고 jobId를 반환합니다. + Job 상태는 /api/jobs/{jobId}로 폴링하여 확인합니다. + operationId: requestAiRecommendations + x-user-story: UFR-EVENT-030 + x-controller: EventController.requestAiRecommendations + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AiRecommendationRequest' + responses: + '202': + description: AI 추천 요청 접수 + content: + application/json: + schema: + $ref: '#/components/schemas/JobAcceptedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: DRAFT 상태의 이벤트만 AI 추천을 요청할 수 있습니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/recommendations: + put: + tags: + - Event Creation + summary: AI 추천 선택 및 커스터마이징 (Step 2-2) + description: | + AI가 생성한 추천 중 하나를 선택하고, + 필요시 이벤트명, 문구, 기간 등을 커스터마이징합니다. + operationId: selectRecommendation + x-user-story: UFR-EVENT-030 + x-controller: EventController.selectRecommendation + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectRecommendationRequest' + responses: + '200': + description: AI 추천 선택 및 커스터마이징 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: AI 추천이 완료된 DRAFT 상태의 이벤트만 선택 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/images: + post: + tags: + - Event Creation + summary: 이미지 생성 요청 (Step 3) + description: | + Content Service에 이미지 생성을 요청합니다. + Kafka Job을 발행하고 jobId를 반환합니다. + Job 상태는 /api/jobs/{jobId}로 폴링하여 확인합니다. + operationId: requestImageGeneration + x-user-story: UFR-CONT-010 + x-controller: EventController.requestImageGeneration + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageGenerationRequest' + responses: + '202': + description: 이미지 생성 요청 접수 + content: + application/json: + schema: + $ref: '#/components/schemas/JobAcceptedResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: AI 추천이 선택된 DRAFT 상태의 이벤트만 이미지 생성이 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/images/{imageId}/select: + put: + tags: + - Event Creation + summary: 이미지 선택 (Step 3-2) + description: | + 생성된 이미지 중 하나를 선택합니다. + 선택된 이미지는 이벤트의 대표 이미지로 설정됩니다. + operationId: selectImage + x-user-story: UFR-CONT-010 + x-controller: EventController.selectImage + parameters: + - $ref: '#/components/parameters/EventId' + - name: imageId + in: path + description: 이미지 ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440006" + responses: + '200': + description: 이미지 선택 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 또는 이미지 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_IMAGE_STATE + message: 해당 이미지는 이 이벤트에 속하지 않습니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/images/{imageId}/edit: + put: + tags: + - Event Creation + summary: 이미지 편집 (Step 3-3) + description: | + 선택된 이미지를 편집합니다. + Content Service에 편집 요청을 보내고 새로운 이미지 URL을 받습니다. + operationId: editImage + x-user-story: UFR-CONT-020 + x-controller: EventController.editImage + parameters: + - $ref: '#/components/parameters/EventId' + - name: imageId + in: path + description: 이미지 ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440006" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageEditRequest' + responses: + '200': + description: 이미지 편집 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ImageEditResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이미지 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: IMAGE_NOT_EDITABLE + message: 선택된 이미지만 편집 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/channels: + put: + tags: + - Event Creation + summary: 배포 채널 선택 (Step 4) + description: | + 이벤트를 배포할 채널을 선택합니다. + (웹사이트, 카카오톡, Instagram, Facebook 등) + operationId: selectChannels + x-user-story: UFR-EVENT-040 + x-controller: EventController.selectChannels + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SelectChannelsRequest' + responses: + '200': + description: 배포 채널 선택 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: INVALID_EVENT_STATE + message: 이미지가 선택된 DRAFT 상태의 이벤트만 채널 선택이 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{eventId}/publish: + post: + tags: + - Event Creation + summary: 최종 승인 및 배포 (Step 5) + description: | + 이벤트를 최종 승인하고 선택된 채널에 배포합니다. + Distribution Service를 동기 호출하여 배포하고, + 이벤트 상태를 PUBLISHED로 변경합니다. + Kafka Event (EventCreated)를 발행합니다. + operationId: publishEvent + x-user-story: UFR-EVENT-050 + x-controller: EventController.publishEvent + parameters: + - $ref: '#/components/parameters/EventId' + responses: + '200': + description: 이벤트 배포 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventPublishedResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태가 적절하지 않음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_PUBLISHABLE + message: 배포 채널이 선택된 DRAFT 상태의 이벤트만 배포 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + '503': + description: Distribution Service 호출 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: DISTRIBUTION_SERVICE_UNAVAILABLE + message: 배포 서비스를 일시적으로 사용할 수 없습니다. + + /api/events/{eventId}/end: + post: + tags: + - Event Management + summary: 이벤트 조기 종료 + description: | + 진행 중인 이벤트를 조기 종료합니다. + PUBLISHED 상태의 이벤트만 종료 가능하며, + 종료 시 상태가 ENDED로 변경됩니다. + operationId: endEvent + x-user-story: UFR-EVENT-060 + x-controller: EventController.endEvent + parameters: + - $ref: '#/components/parameters/EventId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EndEventRequest' + responses: + '200': + description: 이벤트 종료 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + description: 이벤트 상태로 인해 종료 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + code: EVENT_NOT_ENDABLE + message: PUBLISHED 상태의 이벤트만 종료 가능합니다. + '500': + $ref: '#/components/responses/InternalServerError' + + /api/jobs/{jobId}: + get: + tags: + - Job Status + summary: Job 상태 폴링 + description: | + 비동기 작업(AI 추천 생성, 이미지 생성)의 상태를 조회합니다. + 클라이언트는 COMPLETED 또는 FAILED가 될 때까지 폴링합니다. + COMPLETED 시 Redis에서 결과를 조회할 수 있습니다. + operationId: getJobStatus + x-user-story: UFR-EVENT-030, UFR-CONT-010 + x-controller: JobController.getJobStatus + parameters: + - name: jobId + in: path + description: Job ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440005" + responses: + '200': + description: Job 상태 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' @@ -428,578 +663,656 @@ components: type: http scheme: bearer bearerFormat: JWT - description: JWT 토큰을 사용한 인증 (User Service에서 발급) + description: JWT 토큰 인증 + + parameters: + EventId: + name: eventId + in: path + description: 이벤트 ID + required: true + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" schemas: - # Dashboard Schemas - DashboardResponse: + EventListResponse: type: object - required: - - active - - approved - - completed properties: - active: + content: type: array - description: 진행중 이벤트 목록 (최대 5개) - maxItems: 5 - items: - $ref: '#/components/schemas/EventSummary' - approved: - type: array - description: 배포 대기중 이벤트 목록 (최대 5개) - maxItems: 5 - items: - $ref: '#/components/schemas/EventSummary' - completed: - type: array - description: 종료된 이벤트 목록 (최대 5개) - maxItems: 5 items: $ref: '#/components/schemas/EventSummary' + page: + $ref: '#/components/schemas/PageInfo' + required: + - content + - page EventSummary: type: object - required: - - eventId - - title - - status - - createdAt properties: eventId: type: string format: uuid description: 이벤트 ID - title: + example: "550e8400-e29b-41d4-a716-446655440000" + eventName: type: string - description: 이벤트 제목 - maxLength: 100 - period: - $ref: '#/components/schemas/EventPeriod' + description: 이벤트명 + example: "봄맞이 20% 할인 이벤트" + objective: + type: string + description: 이벤트 목적 + example: "신규 고객 유치" status: type: string - enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] + enum: [DRAFT, PUBLISHED, ENDED] description: 이벤트 상태 - participantCount: - type: integer - description: 참여자 수 - default: 0 - viewCount: - type: integer - description: 조회수 - default: 0 - createdAt: - type: string - format: date-time - description: 생성일시 - - EventPeriod: - type: object - properties: + example: "PUBLISHED" startDate: type: string format: date description: 시작일 + example: "2025-03-01" endDate: type: string format: date description: 종료일 - - # Event Draft Schemas - CreateEventDraftRequest: - type: object - required: - - objective - - storeInfo - properties: - objective: + example: "2025-03-31" + thumbnailUrl: type: string - enum: [NEW_CUSTOMER, REVISIT, SALES_INCREASE, BRAND_AWARENESS] - description: | - 이벤트 목적 - - NEW_CUSTOMER: 신규 고객 유치 - - REVISIT: 재방문 유도 - - SALES_INCREASE: 매출 증대 - - BRAND_AWARENESS: 인지도 향상 - storeInfo: - $ref: '#/components/schemas/StoreInfo' - - StoreInfo: - type: object - required: - - storeName - - industry - - region - properties: - storeName: - type: string - description: 매장명 - maxLength: 50 - industry: - type: string - description: 업종 - maxLength: 30 - region: - type: string - description: 지역 - maxLength: 50 - address: - type: string - description: 주소 - maxLength: 200 - - EventDraftResponse: - type: object - required: - - eventDraftId - - objective - - status - properties: - eventDraftId: - type: string - format: uuid - description: 이벤트 초안 ID - objective: - type: string - enum: [NEW_CUSTOMER, REVISIT, SALES_INCREASE, BRAND_AWARENESS] - description: 이벤트 목적 - status: - type: string - enum: [DRAFT] - description: 초안 상태 + format: uri + description: 썸네일 이미지 URL + example: "https://cdn.kt-event.com/images/event-thumb-001.jpg" createdAt: type: string format: date-time description: 생성일시 + example: "2025-02-15T10:30:00Z" + required: + - eventId + - eventName + - objective + - status + - startDate + - endDate + - createdAt - # Job Schemas - JobResponse: + EventDetailResponse: type: object + properties: + eventId: + type: string + format: uuid + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + userId: + type: string + format: uuid + description: 사용자 ID + example: "550e8400-e29b-41d4-a716-446655440001" + storeId: + type: string + format: uuid + description: 매장 ID + example: "550e8400-e29b-41d4-a716-446655440002" + eventName: + type: string + description: 이벤트명 + example: "봄맞이 20% 할인 이벤트" + objective: + type: string + description: 이벤트 목적 + example: "신규 고객 유치" + description: + type: string + description: 이벤트 설명 + example: "봄을 맞이하여 모든 상품 20% 할인 행사를 진행합니다." + targetAudience: + type: string + description: 타겟 고객 + example: "20-30대 여성" + promotionType: + type: string + description: 프로모션 유형 + example: "할인" + discountRate: + type: integer + description: 할인율 (%) + example: 20 + startDate: + type: string + format: date + description: 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 종료일 + example: "2025-03-31" + status: + type: string + enum: [DRAFT, PUBLISHED, ENDED] + description: 이벤트 상태 + example: "PUBLISHED" + selectedImageId: + type: string + format: uuid + description: 선택된 이미지 ID + example: "550e8400-e29b-41d4-a716-446655440006" + selectedImageUrl: + type: string + format: uri + description: 선택된 이미지 URL + example: "https://cdn.kt-event.com/images/event-img-001.jpg" + generatedImages: + type: array + description: 생성된 이미지 목록 + items: + $ref: '#/components/schemas/GeneratedImage' + channels: + type: array + description: 배포 채널 목록 + items: + type: string + example: "WEBSITE" + aiRecommendations: + type: array + description: AI 추천 목록 + items: + $ref: '#/components/schemas/AiRecommendation' + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-15T10:30:00Z" + updatedAt: + type: string + format: date-time + description: 수정일시 + example: "2025-02-20T14:45:00Z" + required: + - eventId + - userId + - storeId + - eventName + - objective + - status + - startDate + - endDate + - createdAt + + GeneratedImage: + type: object + properties: + imageId: + type: string + format: uuid + description: 이미지 ID + example: "550e8400-e29b-41d4-a716-446655440006" + imageUrl: + type: string + format: uri + description: 이미지 URL + example: "https://cdn.kt-event.com/images/event-img-001.jpg" + isSelected: + type: boolean + description: 선택 여부 + example: true + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-16T11:00:00Z" + required: + - imageId + - imageUrl + - isSelected + - createdAt + + AiRecommendation: + type: object + properties: + recommendationId: + type: string + format: uuid + description: 추천 ID + example: "550e8400-e29b-41d4-a716-446655440007" + eventName: + type: string + description: 추천 이벤트명 + example: "봄맞이 20% 할인 이벤트" + description: + type: string + description: 추천 설명 + example: "봄을 맞이하여 모든 상품 20% 할인 행사를 진행합니다." + promotionType: + type: string + description: 추천 프로모션 유형 + example: "할인" + targetAudience: + type: string + description: 추천 타겟 고객 + example: "20-30대 여성" + isSelected: + type: boolean + description: 선택 여부 + example: true + required: + - recommendationId + - eventName + - description + - isSelected + + SelectObjectiveRequest: + type: object + properties: + objective: + type: string + description: 이벤트 목적 + example: "신규 고객 유치" + required: + - objective + + EventCreatedResponse: + type: object + properties: + eventId: + type: string + format: uuid + description: 생성된 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + status: + type: string + enum: [DRAFT] + description: 이벤트 상태 (항상 DRAFT) + example: "DRAFT" + objective: + type: string + description: 선택된 이벤트 목적 + example: "신규 고객 유치" + createdAt: + type: string + format: date-time + description: 생성일시 + example: "2025-02-15T10:30:00Z" + required: + - eventId + - status + - objective + - createdAt + + AiRecommendationRequest: + type: object + properties: + storeInfo: + type: object + description: 매장 정보 (User Service에서 조회) + properties: + storeId: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440002" + storeName: + type: string + example: "우진네 고깃집" + category: + type: string + example: "음식점" + description: + type: string + example: "신선한 한우를 제공하는 고깃집" + required: + - storeId + - storeName + - category + required: + - storeInfo + + JobAcceptedResponse: + type: object + properties: + jobId: + type: string + format: uuid + description: 생성된 Job ID + example: "550e8400-e29b-41d4-a716-446655440005" + status: + type: string + enum: [PENDING] + description: Job 상태 (초기 상태는 PENDING) + example: "PENDING" + message: + type: string + description: 안내 메시지 + example: "AI 추천 생성 요청이 접수되었습니다. /api/jobs/{jobId}로 상태를 확인하세요." required: - jobId - status + - message + + JobStatusResponse: + type: object properties: jobId: type: string format: uuid description: Job ID + example: "550e8400-e29b-41d4-a716-446655440005" + jobType: + type: string + enum: [AI_RECOMMENDATION, IMAGE_GENERATION] + description: Job 유형 + example: "AI_RECOMMENDATION" status: type: string enum: [PENDING, PROCESSING, COMPLETED, FAILED] description: Job 상태 - - AIRecommendationJobResult: - type: object - required: - - jobId - - status - properties: - jobId: - type: string - format: uuid - status: - type: string - enum: [PENDING, PROCESSING, COMPLETED, FAILED] - recommendations: - type: array - description: AI 추천 결과 (3가지) - minItems: 3 - maxItems: 3 - items: - $ref: '#/components/schemas/EventRecommendation' - error: - type: string - description: 에러 메시지 (FAILED 상태일 때만) - - EventRecommendation: - type: object - required: - - title - - prize - - participationMethod - - estimatedCost - - estimatedParticipants - - estimatedROI - properties: - title: - type: string - description: 이벤트 제목 (수정 가능) - maxLength: 50 - prize: - type: string - description: 경품 (수정 가능) - maxLength: 100 - participationMethod: - type: string - description: 참여 방법 - maxLength: 200 - estimatedCost: - type: integer - description: 예상 비용 (원) - minimum: 0 - estimatedParticipants: - type: integer - description: 예상 참여자 수 - minimum: 0 - estimatedROI: - type: number - format: double - description: 예상 투자 대비 수익률 (%) - - ImageGenerationJobResult: - type: object - required: - - jobId - - status - properties: - jobId: - type: string - format: uuid - status: - type: string - enum: [PENDING, PROCESSING, COMPLETED, FAILED] + example: "COMPLETED" progress: type: integer - description: 진행률 (%) - PROCESSING 상태일 때 minimum: 0 maximum: 100 - imageUrls: - type: object - description: 생성된 이미지 URL (3가지 스타일) - COMPLETED 상태일 때 - properties: - simple: - type: string - format: uri - description: 심플 스타일 이미지 URL - fancy: - type: string - format: uri - description: 화려한 스타일 이미지 URL - trendy: - type: string - format: uri - description: 트렌디 스타일 이미지 URL - error: + description: 진행률 (%) + example: 100 + resultKey: type: string - description: 에러 메시지 (FAILED 상태일 때만) - - # Content Schemas - UpdateContentRequest: - type: object - required: - - selectedImageUrl - - editedContent - properties: - selectedImageUrl: + description: Redis 결과 키 (COMPLETED 시) + example: "ai:recommendation:550e8400-e29b-41d4-a716-446655440005" + errorMessage: type: string - format: uri - description: 선택한 이미지 URL (simple, fancy, trendy 중 하나) - editedContent: - $ref: '#/components/schemas/EditedContent' - - EditedContent: - type: object - properties: - title: - type: string - description: 편집된 제목 - maxLength: 100 - prizeText: - type: string - description: 편집된 경품 정보 텍스트 - maxLength: 200 - participationText: - type: string - description: 편집된 참여 안내 텍스트 - maxLength: 300 - backgroundColor: - type: string - pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' - description: 배경색 (Hex 코드) - textColor: - type: string - pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' - description: 텍스트 색상 (Hex 코드) - accentColor: - type: string - pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' - description: 강조 색상 (Hex 코드) - - EventContentResponse: - type: object - required: - - eventDraftId - - selectedImageUrl - - editedContent - properties: - eventDraftId: - type: string - format: uuid - selectedImageUrl: - type: string - format: uri - editedContent: - $ref: '#/components/schemas/EditedContent' - - # Publish Schemas - PublishEventRequest: - type: object - required: - - selectedChannels - properties: - selectedChannels: - type: array - description: 배포 채널 목록 (최소 1개) - minItems: 1 - items: - $ref: '#/components/schemas/DistributionChannel' - - DistributionChannel: - type: object - required: - - channelType - properties: - channelType: - type: string - enum: [URINEIGHBOR_TV, RINGO_BIZ, GENIE_TV, INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] - description: | - 배포 채널 타입 - - URINEIGHBOR_TV: 우리동네TV - - RINGO_BIZ: 링고비즈 - - GENIE_TV: 지니TV - - INSTAGRAM: Instagram - - NAVER_BLOG: Naver Blog - - KAKAO_CHANNEL: Kakao Channel - settings: - type: object - description: 채널별 설정 (채널마다 다름) - additionalProperties: true - - PublishEventResponse: - type: object - required: - - eventId - - status - - distributionResults - properties: - eventId: - type: string - format: uuid - description: 발행된 이벤트 ID - status: - type: string - enum: [ACTIVE] - description: 이벤트 상태 - distributionResults: - type: array - description: 채널별 배포 결과 - items: - $ref: '#/components/schemas/ChannelDistributionResult' - - ChannelDistributionResult: - type: object - required: - - channelType - - status - properties: - channelType: - type: string - enum: [URINEIGHBOR_TV, RINGO_BIZ, GENIE_TV, INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] - status: - type: string - enum: [SUCCESS, FAILED] - description: 배포 상태 - distributionId: - type: string - description: 배포 ID (채널에서 발급) - estimatedReach: - type: integer - description: 예상 노출 수 - error: - type: string - description: 에러 메시지 (FAILED 상태일 때) - - # Event Management Schemas - EventDetailResponse: - type: object - required: - - event - - prizes - - distributionStatus - properties: - event: - $ref: '#/components/schemas/EventDetail' - prizes: - type: array - description: 경품 목록 - items: - $ref: '#/components/schemas/Prize' - distributionStatus: - $ref: '#/components/schemas/DistributionStatus' - - EventDetail: - type: object - required: - - eventId - - title - - objective - - status - - createdAt - properties: - eventId: - type: string - format: uuid - title: - type: string - maxLength: 100 - objective: - type: string - enum: [NEW_CUSTOMER, REVISIT, SALES_INCREASE, BRAND_AWARENESS] - period: - $ref: '#/components/schemas/EventPeriod' - status: - type: string - enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] - participationMethod: - type: string - description: 참여 방법 - maxLength: 500 - selectedImageUrl: - type: string - format: uri - description: 선택한 이미지 URL - editedContent: - $ref: '#/components/schemas/EditedContent' + description: 에러 메시지 (FAILED 시) + example: "AI 서비스 연결 실패" createdAt: type: string format: date-time - publishedAt: + description: Job 생성일시 + example: "2025-02-15T10:31:00Z" + completedAt: type: string format: date-time - description: 발행일시 - - Prize: - type: object + description: Job 완료일시 + example: "2025-02-15T10:31:30Z" required: - - prizeId - - prizeName - - quantity + - jobId + - jobType + - status + - progress + - createdAt + + SelectRecommendationRequest: + type: object properties: - prizeId: + recommendationId: type: string format: uuid - prizeName: - type: string - description: 경품명 - maxLength: 100 - quantity: - type: integer - description: 수량 - minimum: 1 + description: 선택한 추천 ID + example: "550e8400-e29b-41d4-a716-446655440007" + customizations: + type: object + description: 커스터마이징 항목 + properties: + eventName: + type: string + description: 수정된 이벤트명 + example: "봄맞이 특별 할인 이벤트" + description: + type: string + description: 수정된 설명 + example: "봄을 맞이하여 전 메뉴 20% 할인" + startDate: + type: string + format: date + description: 수정된 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 수정된 종료일 + example: "2025-03-31" + discountRate: + type: integer + description: 수정된 할인율 + example: 20 + required: + - recommendationId - DistributionStatus: + ImageGenerationRequest: + type: object + properties: + eventInfo: + type: object + description: 이벤트 정보 (이미지 생성에 필요한 정보) + properties: + eventName: + type: string + example: "봄맞이 20% 할인 이벤트" + description: + type: string + example: "봄을 맞이하여 모든 상품 20% 할인 행사를 진행합니다." + promotionType: + type: string + example: "할인" + required: + - eventName + - description + imageCount: + type: integer + minimum: 1 + maximum: 5 + description: 생성할 이미지 개수 + default: 3 + example: 3 + required: + - eventInfo + + ImageEditRequest: + type: object + properties: + editType: + type: string + enum: [TEXT_OVERLAY, COLOR_ADJUST, CROP, FILTER] + description: 편집 유형 + example: "TEXT_OVERLAY" + parameters: + type: object + description: 편집 파라미터 (편집 유형에 따라 다름) + additionalProperties: true + example: + text: "20% 할인" + fontSize: 48 + color: "#FF0000" + position: "center" + required: + - editType + - parameters + + ImageEditResponse: + type: object + properties: + imageId: + type: string + format: uuid + description: 편집된 이미지 ID + example: "550e8400-e29b-41d4-a716-446655440008" + imageUrl: + type: string + format: uri + description: 편집된 이미지 URL + example: "https://cdn.kt-event.com/images/event-img-001-edited.jpg" + editedAt: + type: string + format: date-time + description: 편집일시 + example: "2025-02-16T15:20:00Z" + required: + - imageId + - imageUrl + - editedAt + + SelectChannelsRequest: type: object properties: channels: type: array - description: 배포된 채널 목록 + description: 배포 채널 목록 items: - $ref: '#/components/schemas/ChannelStatus' - - ChannelStatus: - type: object + type: string + enum: [WEBSITE, KAKAO, INSTAGRAM, FACEBOOK, NAVER_BLOG] + example: ["WEBSITE", "KAKAO", "INSTAGRAM"] + minItems: 1 required: - - channelType - - status - properties: - channelType: - type: string - enum: [URINEIGHBOR_TV, RINGO_BIZ, GENIE_TV, INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] - status: - type: string - enum: [PENDING, DISTRIBUTING, ACTIVE, FAILED] - distributionId: - type: string - estimatedReach: - type: integer - actualReach: - type: integer - description: 실제 노출 수 + - channels - EventListResponse: + EventPublishedResponse: type: object - required: - - events - - totalCount - - totalPages - - currentPage - properties: - events: - type: array - items: - $ref: '#/components/schemas/EventListItem' - totalCount: - type: integer - description: 전체 이벤트 수 - totalPages: - type: integer - description: 전체 페이지 수 - currentPage: - type: integer - description: 현재 페이지 (0부터 시작) - - EventListItem: - type: object - required: - - eventId - - title - - status - - createdAt properties: eventId: type: string format: uuid - title: - type: string - maxLength: 100 - period: - $ref: '#/components/schemas/EventPeriod' + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" status: type: string - enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] - participantCount: - type: integer - description: 참여자 수 - default: 0 - roi: - type: number - format: double - description: 투자 대비 수익률 (%) - createdAt: + enum: [PUBLISHED] + description: 이벤트 상태 (항상 PUBLISHED) + example: "PUBLISHED" + publishedAt: type: string format: date-time + description: 배포일시 + example: "2025-02-20T16:00:00Z" + channels: + type: array + description: 배포된 채널 목록 + items: + type: string + example: "WEBSITE" + distributionResults: + type: array + description: 채널별 배포 결과 + items: + $ref: '#/components/schemas/DistributionResult' + required: + - eventId + - status + - publishedAt + - channels + - distributionResults + + DistributionResult: + type: object + properties: + channel: + type: string + description: 채널명 + example: "WEBSITE" + success: + type: boolean + description: 배포 성공 여부 + example: true + url: + type: string + format: uri + description: 배포된 URL + example: "https://store.kt-event.com/event/550e8400-e29b-41d4-a716-446655440000" + message: + type: string + description: 배포 결과 메시지 + example: "웹사이트에 성공적으로 배포되었습니다." + required: + - channel + - success + + UpdateEventRequest: + type: object + properties: + eventName: + type: string + description: 이벤트명 + example: "봄맞이 특별 할인 이벤트" + description: + type: string + description: 이벤트 설명 + example: "봄을 맞이하여 전 메뉴 20% 할인" + startDate: + type: string + format: date + description: 시작일 + example: "2025-03-01" + endDate: + type: string + format: date + description: 종료일 + example: "2025-03-31" + discountRate: + type: integer + description: 할인율 + example: 20 + + EndEventRequest: + type: object + properties: + reason: + type: string + description: 종료 사유 + example: "목표 달성으로 조기 종료" + required: + - reason + + PageInfo: + type: object + properties: + page: + type: integer + description: 현재 페이지 번호 + example: 0 + size: + type: integer + description: 페이지 크기 + example: 20 + totalElements: + type: integer + description: 전체 요소 개수 + example: 45 + totalPages: + type: integer + description: 전체 페이지 개수 + example: 3 + required: + - page + - size + - totalElements + - totalPages - # Error Schemas ErrorResponse: type: object - required: - - error - - message - - timestamp properties: - error: + code: type: string description: 에러 코드 + example: "INVALID_REQUEST" message: type: string description: 에러 메시지 + example: "요청 파라미터가 올바르지 않습니다." + details: + type: array + description: 상세 에러 정보 + items: + type: string + example: ["objective 필드는 필수입니다."] timestamp: type: string format: date-time description: 에러 발생 시각 - details: - type: string - description: 상세 에러 정보 + example: "2025-02-15T10:30:00Z" + required: + - code + - message + - timestamp responses: BadRequest: @@ -1009,9 +1322,11 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' example: - error: BAD_REQUEST - message: 요청 파라미터가 유효하지 않습니다 - timestamp: '2025-10-22T10:00:00Z' + code: INVALID_REQUEST + message: 요청 파라미터가 올바르지 않습니다. + details: + - "objective 필드는 필수입니다." + timestamp: "2025-02-15T10:30:00Z" Unauthorized: description: 인증 실패 @@ -1020,9 +1335,9 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' example: - error: UNAUTHORIZED - message: 인증 토큰이 유효하지 않습니다 - timestamp: '2025-10-22T10:00:00Z' + code: UNAUTHORIZED + message: 인증에 실패했습니다. + timestamp: "2025-02-15T10:30:00Z" Forbidden: description: 권한 없음 @@ -1031,9 +1346,9 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' example: - error: FORBIDDEN - message: 해당 리소스에 접근 권한이 없습니다 - timestamp: '2025-10-22T10:00:00Z' + code: FORBIDDEN + message: 해당 리소스에 접근할 권한이 없습니다. + timestamp: "2025-02-15T10:30:00Z" NotFound: description: 리소스를 찾을 수 없음 @@ -1042,9 +1357,9 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' example: - error: NOT_FOUND - message: 요청한 리소스를 찾을 수 없습니다 - timestamp: '2025-10-22T10:00:00Z' + code: NOT_FOUND + message: 요청한 리소스를 찾을 수 없습니다. + timestamp: "2025-02-15T10:30:00Z" InternalServerError: description: 서버 내부 오류 @@ -1053,6 +1368,6 @@ components: schema: $ref: '#/components/schemas/ErrorResponse' example: - error: INTERNAL_SERVER_ERROR - message: 서버 내부 오류가 발생했습니다 - timestamp: '2025-10-22T10:00:00Z' + code: INTERNAL_SERVER_ERROR + message: 서버 내부 오류가 발생했습니다. + timestamp: "2025-02-15T10:30:00Z" diff --git a/design/backend/api/participation-service-api.yaml b/design/backend/api/participation-service-api.yaml index 104e133..93db96c 100644 --- a/design/backend/api/participation-service-api.yaml +++ b/design/backend/api/participation-service-api.yaml @@ -2,360 +2,100 @@ openapi: 3.0.3 info: title: Participation Service API description: | - KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Participation Service - - ## 주요 기능 - - 이벤트 참여 접수 (비회원 가능) - - 참여자 목록 조회 (사장님 전용) - - 당첨자 추첨 (사장님 전용) - - ## 인증 정보 - - 이벤트 참여: 인증 불필요 (비회원 참여 가능) - - 참여자 목록 조회 및 당첨자 추첨: JWT 토큰 필수 (사장님 권한) + 이벤트 참여 및 당첨자 관리 서비스 API + - 이벤트 참여 등록 + - 참여자 목록 조회 및 관리 + - 당첨자 추첨 및 관리 version: 1.0.0 contact: - name: Digital Garage Team + name: KT Event Marketing Team email: support@kt-event.com servers: + - url: http://localhost:8083 + description: Local Development Server + - url: https://api-dev.kt-event.com/participation + description: Development Server - url: https://api.kt-event.com/participation description: Production Server - - url: https://dev-api.kt-event.com/participation - description: Development Server - - url: http://localhost:8083 - description: Local Server tags: - - name: Participation + - name: participation description: 이벤트 참여 관리 - - name: Participants - description: 참여자 목록 관리 - - name: Draw - description: 당첨자 추첨 + - name: participant + description: 참여자 조회 및 관리 + - name: winner + description: 당첨자 추첨 및 관리 paths: - /api/v1/participations: + /api/events/{eventId}/participate: post: tags: - - Participation + - participation summary: 이벤트 참여 description: | - 고객이 이벤트에 참여합니다. (UFR-PART-010) - - **특징:** - - 비회원 참여 가능 (인증 불필요) - - 전화번호 기반 중복 체크 (1인 1회) - - Redis 캐싱으로 중복 체크 성능 최적화 - - 응모 번호 자동 발급 - - **처리 흐름:** - 1. 요청 데이터 유효성 검증 - 2. Redis 캐시에서 중복 체크 (빠른 응답) - 3. 캐시 MISS 시 DB 조회 - 4. 신규 참여: 응모번호 발급 및 저장 - 5. 중복 방지 캐시 저장 (TTL: 7일) - 6. Kafka 이벤트 발행 (Analytics 연동) - operationId: registerParticipation + 고객이 이벤트에 참여합니다. + - 중복 참여 검증 (전화번호 기반) + - 이벤트 진행 상태 검증 + - Kafka 이벤트 발행 (ParticipantRegistered) + operationId: participateEvent + x-user-story: UFR-PART-010 + x-controller: ParticipationController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/ParticipationRegisterRequest' + $ref: '#/components/schemas/ParticipationRequest' examples: - 신규참여: + standard: + summary: 일반 참여 value: - eventId: "evt-12345-abcde" name: "홍길동" - phoneNumber: "01012345678" - entryPath: "SNS" - consentMarketing: true - 매장방문참여: + phoneNumber: "010-1234-5678" + email: "hong@example.com" + agreeMarketing: true + agreePrivacy: true + storeVisited: false + storeVisit: + summary: 매장 방문 참여 value: - eventId: "evt-12345-abcde" name: "김철수" - phoneNumber: "01098765432" - entryPath: "STORE_VISIT" - consentMarketing: false + phoneNumber: "010-9876-5432" + email: "kim@example.com" + agreeMarketing: false + agreePrivacy: true + storeVisited: true responses: '201': - description: 참여 접수 완료 + description: 참여 성공 content: application/json: schema: - $ref: '#/components/schemas/ParticipationRegisterResponse' + $ref: '#/components/schemas/ParticipationResponse' examples: - 성공: + success: + summary: 참여 성공 value: - applicationNumber: "EVT-20251022-A1B2C3" - drawDate: "2025-11-05" - message: "이벤트 참여가 완료되었습니다. 당첨자 발표일은 2025년 11월 5일입니다." - '400': - description: 잘못된 요청 (유효성 검증 실패) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - 이름오류: - value: - error: "VALIDATION_ERROR" - message: "이름은 2자 이상이어야 합니다." - timestamp: "2025-10-22T10:30:00Z" - 전화번호오류: - value: - error: "VALIDATION_ERROR" - message: "올바른 전화번호 형식이 아닙니다." - timestamp: "2025-10-22T10:30:00Z" - 동의누락: - value: - error: "VALIDATION_ERROR" - message: "개인정보 수집 및 이용에 대한 동의가 필요합니다." - timestamp: "2025-10-22T10:30:00Z" - '409': - description: 중복 참여 (이미 참여한 이벤트) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - 중복참여: - value: - error: "DUPLICATE_PARTICIPATION" - message: "이미 참여하신 이벤트입니다." - timestamp: "2025-10-22T10:30:00Z" - - /api/v1/events/{eventId}/participants: - get: - tags: - - Participants - summary: 참여자 목록 조회 - description: | - 이벤트의 참여자 목록을 조회합니다. (UFR-PART-020) - - **특징:** - - 사장님 전용 기능 (JWT 인증 필수) - - Redis 캐싱 (TTL: 10분) - 실시간 정확도와 성능 균형 - - 동적 필터링 (참여 경로, 당첨 여부) - - 검색 기능 (이름, 전화번호) - - 페이지네이션 지원 - - 전화번호 마스킹 (010-****-1234) - - **성능 최적화:** - - 복합 인덱스: idx_participants_event_filters - - Redis 캐싱으로 반복 조회 성능 개선 - operationId: getParticipantList - security: - - bearerAuth: [] - parameters: - - name: eventId - in: path - required: true - description: 이벤트 ID - schema: - type: string - example: "evt-12345-abcde" - - name: entryPath - in: query - required: false - description: 참여 경로 필터 - schema: - type: string - enum: - - SNS - - URIDONGNE_TV - - RINGO_BIZ - - GENIE_TV - - STORE_VISIT - example: "SNS" - - name: isWinner - in: query - required: false - description: 당첨 여부 필터 - schema: - type: boolean - example: false - - name: name - in: query - required: false - description: 이름 검색 (부분 일치) - schema: - type: string - example: "홍길동" - - name: phone - in: query - required: false - description: 전화번호 검색 (부분 일치, 숫자만) - schema: - type: string - example: "01012" - - name: page - in: query - required: false - description: 페이지 번호 (0부터 시작) - schema: - type: integer - minimum: 0 - default: 0 - example: 0 - - name: size - in: query - required: false - description: 페이지당 항목 수 - schema: - type: integer - minimum: 10 - maximum: 100 - default: 20 - example: 20 - responses: - '200': - description: 참여자 목록 조회 성공 - content: - application/json: - schema: - $ref: '#/components/schemas/ParticipantListResponse' - examples: - 전체목록: - value: - participants: - - participantId: "part-001" - applicationNumber: "EVT-20251022-A1B2C3" - name: "홍길동" - phoneNumber: "010-****-5678" - entryPath: "SNS" - participatedAt: "2025-10-22T10:30:00Z" - isWinner: false - - participantId: "part-002" - applicationNumber: "EVT-20251022-D4E5F6" - name: "김철수" - phoneNumber: "010-****-5432" - entryPath: "STORE_VISIT" - participatedAt: "2025-10-22T11:15:00Z" - isWinner: false - pagination: - currentPage: 0 - totalPages: 5 - totalElements: 100 - size: 20 - 당첨자필터: - value: - participants: - - participantId: "part-050" - applicationNumber: "EVT-20251022-Z9Y8X7" - name: "박영희" - phoneNumber: "010-****-1111" - entryPath: "SNS" - participatedAt: "2025-10-23T14:20:00Z" - isWinner: true - wonAt: "2025-10-25T09:00:00Z" - pagination: - currentPage: 0 - totalPages: 1 - totalElements: 5 - size: 20 - '400': - description: 잘못된 요청 (유효성 검증 실패) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - 페이지오류: - value: - error: "VALIDATION_ERROR" - message: "페이지 번호는 0 이상이어야 합니다." - timestamp: "2025-10-22T10:30:00Z" - 크기오류: - value: - error: "VALIDATION_ERROR" - message: "페이지 크기는 10~100 사이여야 합니다." - timestamp: "2025-10-22T10:30:00Z" - '401': - $ref: '#/components/responses/UnauthorizedError' - - /api/v1/events/{eventId}/draw-winners: - post: - tags: - - Draw - summary: 당첨자 추첨 - description: | - 이벤트의 당첨자를 추첨합니다. (UFR-PART-030) - - **특징:** - - 사장님 전용 기능 (JWT 인증 필수) - - Fisher-Yates Shuffle 알고리즘 (공정성 보장) - - 난수 기반 무작위 추첨 (Crypto.randomBytes) - - 매장 방문 고객 가산점 옵션 (가중치 2배) - - 추첨 과정 로그 자동 기록 (감사 추적) - - 재추첨 가능 (이전 로그 보관) - - **알고리즘 특징:** - - 시간 복잡도: O(n log n) - - 공간 복잡도: O(n) - - 예측 불가능한 난수 시드 (암호학적 안전성) - - **트랜잭션 처리:** - - 당첨자 업데이트 + 추첨 로그 저장 (원자성 보장) - - 실패 시 자동 롤백 - operationId: drawWinners - security: - - bearerAuth: [] - parameters: - - name: eventId - in: path - required: true - description: 이벤트 ID - schema: - type: string - example: "evt-12345-abcde" - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DrawWinnersRequest' - examples: - 기본추첨: - value: - winnerCount: 5 - visitBonus: false - 가산점추첨: - value: - winnerCount: 10 - visitBonus: true - responses: - '200': - description: 당첨자 추첨 완료 - content: - application/json: - schema: - $ref: '#/components/schemas/DrawWinnersResponse' - examples: - 성공: - value: - drawLogId: "draw-log-001" - winners: - - participantId: "part-050" - applicationNumber: "EVT-20251022-Z9Y8X7" - name: "박영희" - phoneNumber: "010-****-1111" - entryPath: "SNS" - - participantId: "part-023" - applicationNumber: "EVT-20251022-K3L4M5" - name: "이순신" - phoneNumber: "010-****-2222" - entryPath: "STORE_VISIT" - - participantId: "part-087" - applicationNumber: "EVT-20251022-N6O7P8" - name: "김유신" - phoneNumber: "010-****-3333" - entryPath: "GENIE_TV" - drawMethod: "RANDOM" - algorithm: "FISHER_YATES_SHUFFLE" - visitBonusApplied: false - drawnAt: "2025-10-25T09:00:00Z" - message: "당첨자 추첨이 완료되었습니다." + success: true + message: "이벤트 참여가 완료되었습니다" + data: + participantId: "prt_20250123_001" + eventId: "evt_20250123_001" + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 '400': description: 잘못된 요청 content: @@ -363,116 +103,442 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' examples: - 당첨자수오류: + invalidPhone: + summary: 유효하지 않은 전화번호 value: - error: "VALIDATION_ERROR" - message: "당첨자 수는 1명 이상이어야 합니다." - timestamp: "2025-10-25T09:00:00Z" - 참여자부족: + success: false + error: + code: "INVALID_PHONE_NUMBER" + message: "유효하지 않은 전화번호 형식입니다" + duplicateParticipation: + summary: 중복 참여 value: - error: "INSUFFICIENT_PARTICIPANTS" - message: "참여자 수가 부족합니다. (요청: 10명, 참여자: 5명)" - timestamp: "2025-10-25T09:00:00Z" - '401': - $ref: '#/components/responses/UnauthorizedError' - '409': - description: 이미 추첨 완료된 이벤트 + success: false + error: + code: "DUPLICATE_PARTICIPATION" + message: "이미 참여하신 이벤트입니다" + '404': + description: 이벤트를 찾을 수 없음 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: - 추첨완료: + notFound: + summary: 이벤트 없음 value: - error: "ALREADY_DRAWN" - message: "이미 추첨이 완료된 이벤트입니다. 재추첨을 원하시면 기존 추첨을 취소해주세요." - timestamp: "2025-10-25T09:00:00Z" + success: false + error: + code: "EVENT_NOT_FOUND" + message: "이벤트를 찾을 수 없습니다" + '409': + description: 이벤트 진행 불가 상태 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notActive: + summary: 진행중이 아닌 이벤트 + value: + success: false + error: + code: "EVENT_NOT_ACTIVE" + message: "현재 참여할 수 없는 이벤트입니다" + + /api/events/{eventId}/participants: + get: + tags: + - participant + summary: 참여자 목록 조회 + description: | + 이벤트의 참여자 목록을 조회합니다. + - 페이징 지원 + - 참여일시 기준 정렬 + - 매장 방문 여부 필터링 + operationId: getParticipants + x-user-story: UFR-PART-020 + x-controller: ParticipantController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + - name: page + in: query + description: 페이지 번호 (0부터 시작) + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + - name: size + in: query + description: 페이지 크기 + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + example: 20 + - name: storeVisited + in: query + description: 매장 방문 여부 필터 + schema: + type: boolean + example: true + responses: + '200': + description: 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantListResponse' + examples: + success: + summary: 참여자 목록 + value: + success: true + message: "참여자 목록을 조회했습니다" + data: + participants: + - participantId: "prt_20250123_001" + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + isWinner: false + - participantId: "prt_20250123_002" + name: "김철수" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + participatedAt: "2025-01-23T11:15:00Z" + storeVisited: true + bonusEntries: 2 + isWinner: true + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 156 + totalPages: 8 + hasNext: true + hasPrevious: false + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/events/{eventId}/participants/{participantId}: + get: + tags: + - participant + summary: 참여자 상세 조회 + description: 특정 참여자의 상세 정보를 조회합니다. + operationId: getParticipantDetail + x-user-story: UFR-PART-020 + x-controller: ParticipantController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + - name: participantId + in: path + required: true + description: 참여자 ID + schema: + type: string + example: "prt_20250123_001" + responses: + '200': + description: 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantDetailResponse' + examples: + success: + summary: 참여자 상세 정보 + value: + success: true + message: "참여자 정보를 조회했습니다" + data: + participantId: "prt_20250123_001" + eventId: "evt_20250123_001" + name: "홍길동" + phoneNumber: "010-1234-5678" + email: "hong@example.com" + participatedAt: "2025-01-23T10:30:00Z" + storeVisited: false + bonusEntries: 1 + agreeMarketing: true + agreePrivacy: true + isWinner: false + winnerInfo: null + '404': + description: 참여자를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 참여자 없음 + value: + success: false + error: + code: "PARTICIPANT_NOT_FOUND" + message: "참여자를 찾을 수 없습니다" + + /api/events/{eventId}/draw-winners: + post: + tags: + - winner + summary: 당첨자 추첨 + description: | + 이벤트 당첨자를 추첨합니다. + - 랜덤 추첨 알고리즘 사용 + - 매장 방문 보너스 가중치 적용 + - 중복 당첨 방지 + operationId: drawWinners + x-user-story: UFR-PART-030 + x-controller: WinnerController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersRequest' + examples: + standard: + summary: 일반 추첨 + value: + winnerCount: 10 + applyStoreVisitBonus: true + responses: + '200': + description: 추첨 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersResponse' + examples: + success: + summary: 추첨 완료 + value: + success: true + message: "당첨자 추첨이 완료되었습니다" + data: + eventId: "evt_20250123_001" + totalParticipants: 156 + winnerCount: 10 + drawnAt: "2025-01-24T15:00:00Z" + winners: + - participantId: "prt_20250123_002" + name: "김철수" + phoneNumber: "010-9876-5432" + rank: 1 + - participantId: "prt_20250123_045" + name: "이영희" + phoneNumber: "010-5555-1234" + rank: 2 + - participantId: "prt_20250123_089" + name: "박민수" + phoneNumber: "010-7777-8888" + rank: 3 + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidCount: + summary: 잘못된 당첨자 수 + value: + success: false + error: + code: "INVALID_WINNER_COUNT" + message: "당첨자 수가 참여자 수보다 많습니다" + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: 이미 추첨 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + alreadyDrawn: + summary: 추첨 완료 상태 + value: + success: false + error: + code: "ALREADY_DRAWN" + message: "이미 당첨자 추첨이 완료되었습니다" + + /api/events/{eventId}/winners: + get: + tags: + - winner + summary: 당첨자 목록 조회 + description: | + 이벤트의 당첨자 목록을 조회합니다. + - 당첨 순위별 정렬 + - 페이징 지원 + operationId: getWinners + x-user-story: UFR-PART-030 + x-controller: WinnerController + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt_20250123_001" + - name: page + in: query + description: 페이지 번호 (0부터 시작) + schema: + type: integer + default: 0 + minimum: 0 + example: 0 + - name: size + in: query + description: 페이지 크기 + schema: + type: integer + default: 20 + minimum: 1 + maximum: 100 + example: 20 + responses: + '200': + description: 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/WinnerListResponse' + examples: + success: + summary: 당첨자 목록 + value: + success: true + message: "당첨자 목록을 조회했습니다" + data: + eventId: "evt_20250123_001" + drawnAt: "2025-01-24T15:00:00Z" + totalWinners: 10 + winners: + - participantId: "prt_20250123_002" + name: "김철수" + phoneNumber: "010-9876-5432" + email: "kim@example.com" + rank: 1 + wonAt: "2025-01-24T15:00:00Z" + - participantId: "prt_20250123_045" + name: "이영희" + phoneNumber: "010-5555-1234" + email: "lee@example.com" + rank: 2 + wonAt: "2025-01-24T15:00:00Z" + pagination: + currentPage: 0 + pageSize: 20 + totalElements: 10 + totalPages: 1 + hasNext: false + hasPrevious: false + '404': + description: 이벤트를 찾을 수 없음 또는 당첨자가 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + noWinners: + summary: 당첨자 없음 + value: + success: false + error: + code: "NO_WINNERS_YET" + message: "아직 당첨자 추첨이 진행되지 않았습니다" components: - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT - description: | - JWT 토큰을 사용한 인증 - - **헤더 형식:** - ``` - Authorization: Bearer {token} - ``` - schemas: - ParticipationRegisterRequest: + ParticipationRequest: type: object required: - - eventId - name - phoneNumber - - entryPath + - agreePrivacy properties: - eventId: - type: string - description: 이벤트 ID - example: "evt-12345-abcde" name: type: string + description: 참여자 이름 minLength: 2 - description: 참여자 이름 (2자 이상) + maxLength: 50 example: "홍길동" phoneNumber: type: string - pattern: '^\d{10,11}$' - description: 전화번호 (숫자만, 10~11자리) - example: "01012345678" - entryPath: + description: 참여자 전화번호 (하이픈 포함) + pattern: '^\d{3}-\d{3,4}-\d{4}$' + example: "010-1234-5678" + email: type: string - enum: - - SNS - - URIDONGNE_TV - - RINGO_BIZ - - GENIE_TV - - STORE_VISIT - description: | - 참여 경로 - - SNS: Instagram, Naver Blog, Kakao Channel - - URIDONGNE_TV: 우리동네TV - - RINGO_BIZ: 링고비즈 연결음 - - GENIE_TV: 지니TV 광고 - - STORE_VISIT: 매장 방문 - example: "SNS" - consentMarketing: + format: email + description: 참여자 이메일 + example: "hong@example.com" + agreeMarketing: type: boolean - description: 마케팅 활용 동의 (선택) + description: 마케팅 정보 수신 동의 default: false example: true + agreePrivacy: + type: boolean + description: 개인정보 수집 및 이용 동의 (필수) + example: true + storeVisited: + type: boolean + description: 매장 방문 여부 + default: false + example: false - ParticipationRegisterResponse: + ParticipationResponse: type: object properties: - applicationNumber: - type: string - description: 응모 번호 (형식 EVT-{timestamp}-{random}) - example: "EVT-20251022-A1B2C3" - drawDate: - type: string - format: date - description: 당첨자 발표일 (이벤트 종료일 + 3일) - example: "2025-11-05" + success: + type: boolean + example: true message: type: string - description: 참여 완료 메시지 - example: "이벤트 참여가 완료되었습니다. 당첨자 발표일은 2025년 11월 5일입니다." - - ParticipantListResponse: - type: object - properties: - participants: - type: array - items: - $ref: '#/components/schemas/ParticipantInfo' - pagination: - $ref: '#/components/schemas/PaginationInfo' + example: "이벤트 참여가 완료되었습니다" + data: + $ref: '#/components/schemas/ParticipantInfo' ParticipantInfo: type: object @@ -480,64 +546,89 @@ components: participantId: type: string description: 참여자 ID - example: "part-001" - applicationNumber: + example: "prt_20250123_001" + eventId: type: string - description: 응모 번호 - example: "EVT-20251022-A1B2C3" + description: 이벤트 ID + example: "evt_20250123_001" name: type: string description: 참여자 이름 example: "홍길동" phoneNumber: type: string - description: 전화번호 (마스킹됨, 010-****-5678) - example: "010-****-5678" - entryPath: + description: 참여자 전화번호 + example: "010-1234-5678" + email: type: string - enum: - - SNS - - URIDONGNE_TV - - RINGO_BIZ - - GENIE_TV - - STORE_VISIT - description: 참여 경로 - example: "SNS" + description: 참여자 이메일 + example: "hong@example.com" participatedAt: type: string format: date-time description: 참여 일시 - example: "2025-10-22T10:30:00Z" + example: "2025-01-23T10:30:00Z" + storeVisited: + type: boolean + description: 매장 방문 여부 + example: false + bonusEntries: + type: integer + description: 보너스 응모권 수 (매장 방문 시 +1) + minimum: 1 + example: 1 isWinner: type: boolean description: 당첨 여부 example: false - wonAt: - type: string - format: date-time - description: 당첨 일시 (당첨자인 경우만) - example: "2025-10-25T09:00:00Z" - nullable: true - PaginationInfo: + ParticipantDetailInfo: + allOf: + - $ref: '#/components/schemas/ParticipantInfo' + - type: object + properties: + agreeMarketing: + type: boolean + description: 마케팅 정보 수신 동의 + example: true + agreePrivacy: + type: boolean + description: 개인정보 수집 및 이용 동의 + example: true + winnerInfo: + $ref: '#/components/schemas/WinnerInfo' + nullable: true + + ParticipantListResponse: type: object properties: - currentPage: - type: integer - description: 현재 페이지 번호 (0부터 시작) - example: 0 - totalPages: - type: integer - description: 전체 페이지 수 - example: 5 - totalElements: - type: integer - description: 전체 항목 수 - example: 100 - size: - type: integer - description: 페이지당 항목 수 - example: 20 + success: + type: boolean + example: true + message: + type: string + example: "참여자 목록을 조회했습니다" + data: + type: object + properties: + participants: + type: array + items: + $ref: '#/components/schemas/ParticipantInfo' + pagination: + $ref: '#/components/schemas/Pagination' + + ParticipantDetailResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "참여자 정보를 조회했습니다" + data: + $ref: '#/components/schemas/ParticipantDetailInfo' DrawWinnersRequest: type: object @@ -546,51 +637,70 @@ components: properties: winnerCount: type: integer + description: 당첨자 수 minimum: 1 - description: 당첨자 수 (경품 수량 기반) - example: 5 - visitBonus: + example: 10 + applyStoreVisitBonus: type: boolean - description: | - 매장 방문 고객 가산점 적용 여부 - - true: 매장 방문 고객 가중치 2배 - - false: 모든 참여자 동일 가중치 - default: false - example: false + description: 매장 방문 보너스 적용 여부 + default: true + example: true DrawWinnersResponse: type: object properties: - drawLogId: - type: string - description: 추첨 로그 ID (감사 추적용) - example: "draw-log-001" - winners: - type: array - description: 당첨자 목록 - items: - $ref: '#/components/schemas/WinnerInfo' - drawMethod: - type: string - description: 추첨 방식 - example: "RANDOM" - algorithm: - type: string - description: 추첨 알고리즘 - example: "FISHER_YATES_SHUFFLE" - visitBonusApplied: + success: type: boolean - description: 매장 방문 가산점 적용 여부 - example: false - drawnAt: - type: string - format: date-time - description: 추첨 일시 - example: "2025-10-25T09:00:00Z" + example: true message: type: string - description: 추첨 완료 메시지 - example: "당첨자 추첨이 완료되었습니다." + example: "당첨자 추첨이 완료되었습니다" + data: + type: object + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_20250123_001" + totalParticipants: + type: integer + description: 전체 참여자 수 + example: 156 + winnerCount: + type: integer + description: 당첨자 수 + example: 10 + drawnAt: + type: string + format: date-time + description: 추첨 일시 + example: "2025-01-24T15:00:00Z" + winners: + type: array + description: 당첨자 목록 + items: + $ref: '#/components/schemas/WinnerSummary' + + WinnerSummary: + type: object + properties: + participantId: + type: string + description: 참여자 ID + example: "prt_20250123_002" + name: + type: string + description: 당첨자 이름 + example: "김철수" + phoneNumber: + type: string + description: 당첨자 전화번호 + example: "010-9876-5432" + rank: + type: integer + description: 당첨 순위 + minimum: 1 + example: 1 WinnerInfo: type: object @@ -598,61 +708,113 @@ components: participantId: type: string description: 참여자 ID - example: "part-050" - applicationNumber: - type: string - description: 응모 번호 - example: "EVT-20251022-Z9Y8X7" + example: "prt_20250123_002" name: type: string description: 당첨자 이름 - example: "박영희" + example: "김철수" phoneNumber: type: string - description: 전화번호 (마스킹됨) - example: "010-****-1111" - entryPath: + description: 당첨자 전화번호 + example: "010-9876-5432" + email: type: string - description: 참여 경로 - example: "SNS" + description: 당첨자 이메일 + example: "kim@example.com" + rank: + type: integer + description: 당첨 순위 + minimum: 1 + example: 1 + wonAt: + type: string + format: date-time + description: 당첨 일시 + example: "2025-01-24T15:00:00Z" + + WinnerListResponse: + type: object + properties: + success: + type: boolean + example: true + message: + type: string + example: "당첨자 목록을 조회했습니다" + data: + type: object + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt_20250123_001" + drawnAt: + type: string + format: date-time + description: 추첨 일시 + example: "2025-01-24T15:00:00Z" + totalWinners: + type: integer + description: 전체 당첨자 수 + example: 10 + winners: + type: array + items: + $ref: '#/components/schemas/WinnerInfo' + pagination: + $ref: '#/components/schemas/Pagination' + + Pagination: + type: object + properties: + currentPage: + type: integer + description: 현재 페이지 번호 (0부터 시작) + minimum: 0 + example: 0 + pageSize: + type: integer + description: 페이지 크기 + minimum: 1 + example: 20 + totalElements: + type: integer + description: 전체 요소 수 + minimum: 0 + example: 156 + totalPages: + type: integer + description: 전체 페이지 수 + minimum: 0 + example: 8 + hasNext: + type: boolean + description: 다음 페이지 존재 여부 + example: true + hasPrevious: + type: boolean + description: 이전 페이지 존재 여부 + example: false ErrorResponse: type: object properties: + success: + type: boolean + example: false error: - type: string - description: 오류 코드 - example: "VALIDATION_ERROR" - message: - type: string - description: 오류 메시지 - example: "요청 데이터가 올바르지 않습니다." - timestamp: - type: string - format: date-time - description: 오류 발생 시각 - example: "2025-10-22T10:30:00Z" - - responses: - UnauthorizedError: - description: 인증 실패 (JWT 토큰 없음 또는 유효하지 않음) - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - 토큰없음: - value: - error: "UNAUTHORIZED" - message: "인증 토큰이 필요합니다." - timestamp: "2025-10-22T10:30:00Z" - 토큰만료: - value: - error: "UNAUTHORIZED" - message: "토큰이 만료되었습니다. 다시 로그인해주세요." - timestamp: "2025-10-22T10:30:00Z" - 권한없음: - value: - error: "FORBIDDEN" - message: "이 작업을 수행할 권한이 없습니다." - timestamp: "2025-10-22T10:30:00Z" + type: object + properties: + code: + type: string + description: 에러 코드 + example: "DUPLICATE_PARTICIPATION" + message: + type: string + description: 에러 메시지 + example: "이미 참여하신 이벤트입니다" + details: + type: object + description: 추가 에러 상세 정보 + additionalProperties: true + nullable: true diff --git a/design/backend/api/user-service-api.yaml b/design/backend/api/user-service-api.yaml index b6f036a..29dd55e 100644 --- a/design/backend/api/user-service-api.yaml +++ b/design/backend/api/user-service-api.yaml @@ -487,6 +487,101 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/users/{userId}/store: + get: + tags: + - Profile + summary: 매장정보 조회 (서비스 연동용) + description: | + 특정 사용자의 매장정보를 조회하는 API (내부 서비스 연동용) + + **사용 목적:** + - Event Service에서 이벤트 생성 시 매장정보 조회 + - Content Service에서 매장정보 기반 콘텐츠 생성 + - Service-to-Service 통신용 내부 API + + **주의사항:** + - Internal API로 외부 노출 금지 + - API Gateway에서 인증된 서비스만 접근 허용 + - 매장정보는 Redis 캐시 우선 조회 (TTL 30분) + operationId: getStoreByUserId + x-user-story: Service Integration + x-controller: UserController + security: + - BearerAuth: [] + parameters: + - name: userId + in: path + required: true + description: 사용자 ID + schema: + type: integer + format: int64 + example: 123 + responses: + '200': + description: 매장정보 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/StoreDetailResponse' + examples: + success: + summary: 매장정보 조회 성공 응답 + value: + userId: 123 + storeId: 456 + storeName: 맛있는집 + industry: 음식점 + address: 서울시 강남구 테헤란로 123 + businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00" + businessNumber: "1234567890" + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + unauthorized: + summary: 인증 실패 + value: + code: AUTH_002 + error: 유효하지 않은 토큰입니다 + timestamp: 2025-10-22T10:30:00Z + '403': + description: 권한 없음 (내부 서비스만 접근 가능) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + forbidden: + summary: 권한 없음 + value: + code: AUTH_003 + error: 이 API는 내부 서비스만 접근 가능합니다 + timestamp: 2025-10-22T10:30:00Z + '404': + description: 사용자 또는 매장을 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 사용자 또는 매장 없음 + value: + code: USER_003 + error: 사용자 또는 매장을 찾을 수 없습니다 + timestamp: 2025-10-22T10:30:00Z + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + components: securitySchemes: BearerAuth: @@ -844,6 +939,46 @@ components: description: 응답 메시지 example: 비밀번호가 성공적으로 변경되었습니다 + StoreDetailResponse: + type: object + required: + - userId + - storeId + - storeName + - industry + - address + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 + industry: + type: string + description: 업종 + example: 음식점 + address: + type: string + description: 매장 주소 + example: 서울시 강남구 테헤란로 123 + businessHours: + type: string + description: 영업시간 + example: "월-금 11:00-22:00, 토-일 12:00-21:00" + businessNumber: + type: string + description: 사업자번호 (10자리) + example: "1234567890" + ErrorResponse: type: object required: @@ -863,6 +998,7 @@ components: - USER_005 # 동시성 충돌 - AUTH_001 # 인증 실패 - AUTH_002 # 유효하지 않은 토큰 + - AUTH_003 # 권한 없음 (내부 서비스만 접근) - VALIDATION_ERROR # 입력 검증 오류 error: type: string