mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2026-06-13 07:39:11 +00:00
초기 프로젝트 설정 및 설계 문서 추가
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,914 @@
|
||||
# OpenAPI 3.0.3 공통 컨벤션
|
||||
|
||||
KT AI 기반 소상공인 이벤트 자동 생성 서비스의 모든 마이크로서비스 API 명세서에 적용되는 공통 컨벤션입니다.
|
||||
|
||||
## 목차
|
||||
1. [기본 정보 섹션](#1-기본-정보-섹션)
|
||||
2. [서버 정의](#2-서버-정의)
|
||||
3. [보안 스키마](#3-보안-스키마)
|
||||
4. [태그 구성](#4-태그-구성)
|
||||
5. [엔드포인트 정의](#5-엔드포인트-정의)
|
||||
6. [응답 구조](#6-응답-구조)
|
||||
7. [에러 응답 구조](#7-에러-응답-구조)
|
||||
8. [스키마 정의](#8-스키마-정의)
|
||||
9. [메타데이터 주석](#9-메타데이터-주석)
|
||||
10. [기술 명세 섹션](#10-기술-명세-섹션)
|
||||
11. [예제 작성](#11-예제-작성)
|
||||
|
||||
---
|
||||
|
||||
## 1. 기본 정보 섹션
|
||||
|
||||
### 1.1 OpenAPI 버전
|
||||
```yaml
|
||||
openapi: 3.0.3
|
||||
```
|
||||
- **필수**: 모든 명세서는 OpenAPI 3.0.3 버전을 사용합니다.
|
||||
|
||||
### 1.2 Info 객체
|
||||
```yaml
|
||||
info:
|
||||
title: {Service Name} API
|
||||
description: |
|
||||
KT AI 기반 소상공인 이벤트 자동 생성 서비스 - {Service Name} API
|
||||
|
||||
{서비스 설명 1-2줄}
|
||||
|
||||
**주요 기능:**
|
||||
- {기능 1}
|
||||
- {기능 2}
|
||||
- {기능 3}
|
||||
|
||||
**보안:** (보안 관련 서비스인 경우)
|
||||
- {보안 메커니즘 1}
|
||||
- {보안 메커니즘 2}
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Digital Garage Team
|
||||
email: support@kt-event-marketing.com
|
||||
```
|
||||
|
||||
**필수 항목:**
|
||||
- `title`: "{서비스명} API" 형식
|
||||
- `description`: 마크다운 형식으로 서비스 설명 작성
|
||||
- 첫 줄: 프로젝트명과 서비스 역할
|
||||
- 서비스 설명
|
||||
- 주요 기능 목록 (bullet points)
|
||||
- 보안 관련 서비스의 경우 보안 섹션 추가
|
||||
- `version`: "1.0.0"
|
||||
- `contact`: name과 email 필수
|
||||
|
||||
---
|
||||
|
||||
## 2. 서버 정의
|
||||
|
||||
### 2.1 서버 URL 구조
|
||||
```yaml
|
||||
servers:
|
||||
- url: http://localhost:{port}
|
||||
description: Local Development Server
|
||||
- url: https://dev-api.kt-event-marketing.com/{service}/v1
|
||||
description: Development Server
|
||||
- url: https://api.kt-event-marketing.com/{service}/v1
|
||||
description: Production Server
|
||||
```
|
||||
|
||||
**포트 번호 할당:**
|
||||
- User Service: 8081
|
||||
- Event Service: 8080
|
||||
- Content Service: 8082
|
||||
- AI Service: 8083
|
||||
- Participation Service: 8084
|
||||
- Distribution Service: 8085
|
||||
- Analytics Service: 8086
|
||||
|
||||
**URL 패턴:**
|
||||
- Local: `http://localhost:{port}`
|
||||
- Dev: `https://dev-api.kt-event-marketing.com/{service}/v1`
|
||||
- Prod: `https://api.kt-event-marketing.com/{service}/v1`
|
||||
|
||||
---
|
||||
|
||||
## 3. 보안 스키마
|
||||
|
||||
### 3.1 JWT Bearer 인증
|
||||
```yaml
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT Bearer 토큰 인증
|
||||
|
||||
**형식:** Authorization: Bearer {JWT_TOKEN}
|
||||
|
||||
**토큰 만료:** 7일
|
||||
|
||||
**Claims:**
|
||||
- userId: 사용자 ID
|
||||
- role: 사용자 역할 (OWNER)
|
||||
- iat: 발급 시각
|
||||
- exp: 만료 시각
|
||||
```
|
||||
|
||||
### 3.2 전역 보안 적용
|
||||
```yaml
|
||||
security:
|
||||
- BearerAuth: []
|
||||
```
|
||||
|
||||
**적용 방법:**
|
||||
- 인증이 필요한 모든 엔드포인트에 `security` 섹션 추가
|
||||
- 공개 API (예: 로그인, 회원가입)는 엔드포인트 레벨에서 `security: []`로 오버라이드
|
||||
|
||||
---
|
||||
|
||||
## 4. 태그 구성
|
||||
|
||||
### 4.1 태그 정의 패턴
|
||||
```yaml
|
||||
tags:
|
||||
- name: {Category Name}
|
||||
description: {카테고리 설명 (한글)}
|
||||
```
|
||||
|
||||
**태그 명명 규칙:**
|
||||
- **영문 사용**: 명확한 영문 카테고리명
|
||||
- **설명 한글**: description은 한글로 상세 설명
|
||||
- **일관성 유지**: 유사 기능은 동일한 태그명 사용
|
||||
|
||||
**예시:**
|
||||
```yaml
|
||||
tags:
|
||||
- name: Authentication
|
||||
description: 인증 관련 API (로그인, 로그아웃, 회원가입)
|
||||
- name: Profile
|
||||
description: 프로필 관련 API (조회, 수정, 비밀번호 변경)
|
||||
- name: Event Creation
|
||||
description: 이벤트 생성 플로우
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 엔드포인트 정의
|
||||
|
||||
### 5.1 엔드포인트 경로 규칙
|
||||
|
||||
**경로 패턴:**
|
||||
```
|
||||
/{resource}
|
||||
/{resource}/{id}
|
||||
/{resource}/{id}/{sub-resource}
|
||||
```
|
||||
|
||||
**중요: `/api` prefix 사용 금지**
|
||||
- ❌ 잘못된 예: `/api/users/register`
|
||||
- ✅ 올바른 예: `/users/register`
|
||||
|
||||
API Gateway 또는 서버 URL에서 서비스 구분이 이루어지므로, 엔드포인트 경로에 `/api`를 포함하지 않습니다.
|
||||
|
||||
### 5.2 공통 엔드포인트 구조
|
||||
```yaml
|
||||
paths:
|
||||
/{resource}:
|
||||
{http-method}:
|
||||
tags:
|
||||
- {Tag Name}
|
||||
summary: {짧은 한글 설명}
|
||||
description: |
|
||||
{상세 설명}
|
||||
|
||||
**유저스토리:** {UFR 코드}
|
||||
|
||||
**주요 기능:**
|
||||
- {기능 1}
|
||||
- {기능 2}
|
||||
|
||||
**처리 흐름:** (복잡한 로직인 경우)
|
||||
1. {단계 1}
|
||||
2. {단계 2}
|
||||
|
||||
**보안:** (보안 관련 엔드포인트인 경우)
|
||||
- {보안 메커니즘}
|
||||
operationId: {camelCase 메서드명}
|
||||
x-user-story: {UFR 코드}
|
||||
x-controller: {ControllerClass}.{methodName}
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/{ParameterName}'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{RequestSchema}'
|
||||
examples:
|
||||
{exampleName}:
|
||||
summary: {예시 설명}
|
||||
value: {...}
|
||||
responses:
|
||||
'{statusCode}':
|
||||
description: {응답 설명}
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{ResponseSchema}'
|
||||
examples:
|
||||
{exampleName}:
|
||||
summary: {예시 설명}
|
||||
value: {...}
|
||||
```
|
||||
|
||||
### 5.3 필수 항목
|
||||
- `tags`: 1개 이상의 태그 지정
|
||||
- `summary`: 한글로 간결하게 (10자 이내 권장)
|
||||
- `description`: 마크다운 형식의 상세 설명
|
||||
- 유저스토리 코드 명시
|
||||
- 주요 기능 bullet points
|
||||
- 복잡한 경우 처리 흐름 순서 작성
|
||||
- 보안 관련 내용 (해당 시)
|
||||
- `operationId`: camelCase 메서드명 (예: `getUserProfile`, `createEvent`)
|
||||
- `x-user-story`: UFR 코드 (예: `UFR-USER-010`)
|
||||
- `x-controller`: 컨트롤러 클래스와 메서드 (예: `UserController.getProfile`)
|
||||
|
||||
### 5.4 operationId 명명 규칙
|
||||
```
|
||||
{동사}{명사}
|
||||
```
|
||||
|
||||
**동사 목록:**
|
||||
- `get`: 조회
|
||||
- `list`: 목록 조회
|
||||
- `create`: 생성
|
||||
- `update`: 수정
|
||||
- `delete`: 삭제
|
||||
- `register`: 등록
|
||||
- `login`: 로그인
|
||||
- `logout`: 로그아웃
|
||||
- `select`: 선택
|
||||
- `request`: 요청
|
||||
- `publish`: 배포
|
||||
- `end`: 종료
|
||||
|
||||
**예시:**
|
||||
- `getUser`, `listEvents`, `createEvent`
|
||||
- `updateProfile`, `deleteEvent`
|
||||
- `registerUser`, `loginUser`, `logoutUser`
|
||||
- `selectRecommendation`, `publishEvent`
|
||||
|
||||
---
|
||||
|
||||
## 6. 응답 구조
|
||||
|
||||
### 6.1 성공 응답 (Success Response)
|
||||
|
||||
**원칙: 직접 응답 (Direct Response)**
|
||||
```yaml
|
||||
responses:
|
||||
'200':
|
||||
description: {작업} 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/{ResponseSchema}'
|
||||
```
|
||||
|
||||
**응답 스키마 예시:**
|
||||
```yaml
|
||||
UserProfileResponse:
|
||||
type: object
|
||||
required:
|
||||
- userId
|
||||
- userName
|
||||
- email
|
||||
properties:
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 사용자 ID
|
||||
example: 123
|
||||
userName:
|
||||
type: string
|
||||
description: 사용자 이름
|
||||
example: 홍길동
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 이메일 주소
|
||||
example: hong@example.com
|
||||
```
|
||||
|
||||
**예외: Wrapper가 필요한 경우 (메시지 전달 필요 시)**
|
||||
```yaml
|
||||
LogoutResponse:
|
||||
type: object
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: 성공 여부
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
description: 응답 메시지
|
||||
example: 안전하게 로그아웃되었습니다
|
||||
```
|
||||
|
||||
### 6.2 페이징 응답 (Pagination Response)
|
||||
```yaml
|
||||
{Resource}ListResponse:
|
||||
type: object
|
||||
required:
|
||||
- content
|
||||
- page
|
||||
properties:
|
||||
content:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/{ResourceSummary}'
|
||||
page:
|
||||
$ref: '#/components/schemas/PageInfo'
|
||||
|
||||
PageInfo:
|
||||
type: object
|
||||
required:
|
||||
- page
|
||||
- size
|
||||
- totalElements
|
||||
- totalPages
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 에러 응답 구조
|
||||
|
||||
### 7.1 표준 에러 응답 스키마
|
||||
```yaml
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
- timestamp
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 에러 코드
|
||||
example: USER_001
|
||||
message:
|
||||
type: string
|
||||
description: 에러 메시지
|
||||
example: 이미 가입된 전화번호입니다
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 에러 발생 시각
|
||||
example: 2025-10-22T10:30:00Z
|
||||
details:
|
||||
type: array
|
||||
description: 상세 에러 정보 (선택 사항)
|
||||
items:
|
||||
type: string
|
||||
example: ["필드명: 필수 항목입니다"]
|
||||
```
|
||||
|
||||
**필수 필드:**
|
||||
- `code`: 에러 코드 (서비스별 고유 코드)
|
||||
- `message`: 사용자에게 표시할 에러 메시지 (한글)
|
||||
- `timestamp`: 에러 발생 시각 (ISO 8601 형식)
|
||||
|
||||
**선택 필드:**
|
||||
- `details`: 상세 에러 정보 배열 (validation 에러 등)
|
||||
|
||||
### 7.2 에러 코드 명명 규칙
|
||||
```
|
||||
{SERVICE}_{NUMBER}
|
||||
```
|
||||
|
||||
**서비스 약어:**
|
||||
- `USER`: User Service
|
||||
- `EVENT`: Event Service
|
||||
- `CONT`: Content Service
|
||||
- `AI`: AI Service
|
||||
- `PART`: Participation Service
|
||||
- `DIST`: Distribution Service
|
||||
- `ANAL`: Analytics Service
|
||||
- `AUTH`: 인증 관련 (공통)
|
||||
- `VALIDATION_ERROR`: 입력 검증 오류 (공통)
|
||||
|
||||
**예시:**
|
||||
- `USER_001`: 중복 사용자
|
||||
- `USER_002`: 사업자번호 검증 실패
|
||||
- `AUTH_001`: 인증 실패
|
||||
- `AUTH_002`: 유효하지 않은 토큰
|
||||
- `VALIDATION_ERROR`: 입력 검증 오류
|
||||
|
||||
### 7.3 공통 에러 응답 정의
|
||||
```yaml
|
||||
components:
|
||||
responses:
|
||||
BadRequest:
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
validationError:
|
||||
summary: 입력 검증 오류
|
||||
value:
|
||||
code: VALIDATION_ERROR
|
||||
message: 요청 파라미터가 올바르지 않습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
details:
|
||||
- "필드명: 필수 항목입니다"
|
||||
|
||||
Unauthorized:
|
||||
description: 인증 실패
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
authFailed:
|
||||
summary: 인증 실패
|
||||
value:
|
||||
code: AUTH_001
|
||||
message: 인증에 실패했습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
Forbidden:
|
||||
description: 권한 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
forbidden:
|
||||
summary: 권한 없음
|
||||
value:
|
||||
code: AUTH_003
|
||||
message: 해당 리소스에 접근할 권한이 없습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
NotFound:
|
||||
description: 리소스를 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
notFound:
|
||||
summary: 리소스 없음
|
||||
value:
|
||||
code: NOT_FOUND
|
||||
message: 요청한 리소스를 찾을 수 없습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
InternalServerError:
|
||||
description: 서버 내부 오류
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
serverError:
|
||||
summary: 서버 오류
|
||||
value:
|
||||
code: INTERNAL_SERVER_ERROR
|
||||
message: 서버 내부 오류가 발생했습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
```
|
||||
|
||||
### 7.4 엔드포인트별 에러 응답 적용
|
||||
```yaml
|
||||
responses:
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
```
|
||||
|
||||
**특수 에러 (비즈니스 로직 에러):**
|
||||
```yaml
|
||||
'409':
|
||||
description: 비즈니스 로직 충돌
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
duplicateUser:
|
||||
summary: 중복 사용자
|
||||
value:
|
||||
code: USER_001
|
||||
message: 이미 가입된 전화번호입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 스키마 정의
|
||||
|
||||
### 8.1 스키마 명명 규칙
|
||||
|
||||
**Request 스키마:**
|
||||
```
|
||||
{Action}{Resource}Request
|
||||
```
|
||||
예: `RegisterRequest`, `LoginRequest`, `CreateEventRequest`
|
||||
|
||||
**Response 스키마:**
|
||||
```
|
||||
{Resource}{Type}Response
|
||||
```
|
||||
예: `UserProfileResponse`, `EventListResponse`, `EventDetailResponse`
|
||||
|
||||
**공통 모델:**
|
||||
```
|
||||
{Resource}{Type}
|
||||
```
|
||||
예: `EventSummary`, `GeneratedImage`, `PageInfo`
|
||||
|
||||
### 8.2 스키마 작성 원칙
|
||||
|
||||
**필수 항목:**
|
||||
- `type`: 객체 타입 (object, array, string 등)
|
||||
- `required`: 필수 필드 목록
|
||||
- `properties`: 각 필드 정의
|
||||
- `type`: 필드 타입
|
||||
- `description`: 필드 설명 (한글)
|
||||
- `example`: 예시 값
|
||||
|
||||
**선택 항목:**
|
||||
- `format`: 특수 형식 (date, date-time, email, uri, uuid, int64 등)
|
||||
- `pattern`: 정규식 패턴 (전화번호, 사업자번호 등)
|
||||
- `minLength`, `maxLength`: 문자열 길이 제한
|
||||
- `minimum`, `maximum`: 숫자 범위 제한
|
||||
- `enum`: 허용 값 목록
|
||||
|
||||
**예시:**
|
||||
```yaml
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- phoneNumber
|
||||
- email
|
||||
- password
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 2
|
||||
maxLength: 50
|
||||
description: 사용자 이름 (2자 이상, 한글/영문)
|
||||
example: 홍길동
|
||||
phoneNumber:
|
||||
type: string
|
||||
pattern: '^010\d{8}$'
|
||||
description: 휴대폰 번호 (010XXXXXXXX)
|
||||
example: "01012345678"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
maxLength: 100
|
||||
description: 이메일 주소
|
||||
example: hong@example.com
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 100
|
||||
description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
|
||||
example: "Password123!"
|
||||
```
|
||||
|
||||
### 8.3 날짜/시간 형식
|
||||
|
||||
**날짜:** `format: date`, 형식 `YYYY-MM-DD`
|
||||
```yaml
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
description: 시작일
|
||||
example: "2025-03-01"
|
||||
```
|
||||
|
||||
**날짜/시간:** `format: date-time`, 형식 `ISO 8601`
|
||||
```yaml
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 생성일시
|
||||
example: 2025-10-22T10:30:00Z
|
||||
```
|
||||
|
||||
### 8.4 ID 형식
|
||||
|
||||
**UUID:**
|
||||
```yaml
|
||||
eventId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: 이벤트 ID
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
```
|
||||
|
||||
**정수 ID:**
|
||||
```yaml
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 사용자 ID
|
||||
example: 123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 메타데이터 주석
|
||||
|
||||
### 9.1 필수 메타데이터
|
||||
```yaml
|
||||
x-user-story: {UFR 코드}
|
||||
x-controller: {ControllerClass}.{methodName}
|
||||
```
|
||||
|
||||
**x-user-story:**
|
||||
- 유저스토리 코드 명시
|
||||
- 여러 유저스토리와 관련된 경우 콤마로 구분
|
||||
- 예: `UFR-USER-010`, `UFR-EVENT-010, UFR-EVENT-070`
|
||||
|
||||
**x-controller:**
|
||||
- 컨트롤러 클래스와 메서드 매핑
|
||||
- 백엔드 개발 시 참조
|
||||
- 예: `UserController.registerUser`, `EventController.getEvents`
|
||||
|
||||
### 9.2 선택 메타데이터 (필요 시)
|
||||
```yaml
|
||||
x-internal: true # 내부 API 표시
|
||||
x-async: true # 비동기 처리 표시
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 기술 명세 섹션
|
||||
|
||||
### 10.1 x-technical-specifications
|
||||
|
||||
**비동기 처리 서비스 (AI, Content 등):**
|
||||
```yaml
|
||||
x-technical-specifications:
|
||||
async-processing:
|
||||
message-queue: Kafka
|
||||
topics:
|
||||
request: ai.recommendation.request
|
||||
response: ai.recommendation.response
|
||||
job-tracking: Redis (TTL 24h)
|
||||
timeout: 300s
|
||||
|
||||
resilience:
|
||||
circuit-breaker:
|
||||
failure-threshold: 5
|
||||
timeout: 10s
|
||||
half-open-requests: 3
|
||||
retry:
|
||||
max-attempts: 3
|
||||
backoff: exponential
|
||||
initial-interval: 1s
|
||||
max-interval: 10s
|
||||
fallback:
|
||||
strategy: cached-result
|
||||
|
||||
caching:
|
||||
provider: Redis
|
||||
ttl: 7d
|
||||
key-pattern: "content:event:{eventDraftId}"
|
||||
|
||||
external-apis:
|
||||
- name: Claude API
|
||||
endpoint: https://api.anthropic.com/v1/messages
|
||||
timeout: 60s
|
||||
circuit-breaker: true
|
||||
- name: GPT-4 API
|
||||
endpoint: https://api.openai.com/v1/chat/completions
|
||||
timeout: 60s
|
||||
circuit-breaker: true
|
||||
```
|
||||
|
||||
**동기 처리 서비스:**
|
||||
```yaml
|
||||
x-technical-specifications:
|
||||
database:
|
||||
type: PostgreSQL
|
||||
connection-pool:
|
||||
min: 10
|
||||
max: 50
|
||||
timeout: 30s
|
||||
|
||||
caching:
|
||||
provider: Redis
|
||||
ttl: 30m
|
||||
key-pattern: "user:{userId}"
|
||||
|
||||
security:
|
||||
authentication: JWT Bearer
|
||||
password-hashing: bcrypt
|
||||
encryption:
|
||||
algorithm: AES-256-GCM
|
||||
fields: [businessNumber]
|
||||
```
|
||||
|
||||
### 10.2 적용 기준
|
||||
|
||||
**필수 포함 서비스:**
|
||||
- Content Service: 비동기 처리, Kafka, 외부 API 통합
|
||||
- AI Service: 비동기 처리, Kafka, Claude/GPT 통합
|
||||
|
||||
**선택 포함 서비스:**
|
||||
- User Service: 보안 관련 명세
|
||||
- Event Service: 오케스트레이션 패턴
|
||||
- Participation Service: 대용량 트래픽 대비 캐싱
|
||||
|
||||
---
|
||||
|
||||
## 11. 예제 작성
|
||||
|
||||
### 11.1 Request/Response 예제 원칙
|
||||
|
||||
**모든 requestBody와 주요 response에 예제 필수:**
|
||||
```yaml
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
examples:
|
||||
restaurant:
|
||||
summary: 음식점 회원가입 예시
|
||||
value:
|
||||
name: 홍길동
|
||||
phoneNumber: "01012345678"
|
||||
email: hong@example.com
|
||||
password: "Password123!"
|
||||
storeName: 맛있는집
|
||||
industry: 음식점
|
||||
cafe:
|
||||
summary: 카페 회원가입 예시
|
||||
value:
|
||||
name: 김철수
|
||||
phoneNumber: "01087654321"
|
||||
email: kim@example.com
|
||||
password: "SecurePass456!"
|
||||
storeName: 아메리카노 카페
|
||||
industry: 카페
|
||||
```
|
||||
|
||||
**성공 응답 예제:**
|
||||
```yaml
|
||||
responses:
|
||||
'200':
|
||||
description: 프로필 조회 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProfileResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 프로필 조회 성공 응답
|
||||
value:
|
||||
userId: 123
|
||||
userName: 홍길동
|
||||
phoneNumber: "01012345678"
|
||||
email: hong@example.com
|
||||
```
|
||||
|
||||
**에러 응답 예제:**
|
||||
```yaml
|
||||
responses:
|
||||
'400':
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
duplicateUser:
|
||||
summary: 중복 사용자
|
||||
value:
|
||||
code: USER_001
|
||||
message: 이미 가입된 전화번호입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
invalidBusinessNumber:
|
||||
summary: 사업자번호 검증 실패
|
||||
value:
|
||||
code: USER_002
|
||||
message: 유효하지 않은 사업자번호입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
```
|
||||
|
||||
### 11.2 예제 명명 규칙
|
||||
- `success`: 성공 케이스
|
||||
- `{errorType}`: 에러 케이스 (예: `duplicateUser`, `validationError`)
|
||||
- `{scenario}`: 시나리오별 예제 (예: `restaurant`, `cafe`)
|
||||
|
||||
---
|
||||
|
||||
## 12. 체크리스트
|
||||
|
||||
API 명세서 작성 시 아래 체크리스트를 확인하세요:
|
||||
|
||||
### 기본 정보
|
||||
- [ ] OpenAPI 버전 3.0.3 명시
|
||||
- [ ] info.title에 서비스명 포함
|
||||
- [ ] info.description에 주요 기능 목록 포함
|
||||
- [ ] info.version 1.0.0
|
||||
- [ ] contact 정보 포함
|
||||
|
||||
### 서버 및 보안
|
||||
- [ ] servers에 Local, Dev, Prod 정의
|
||||
- [ ] 포트 번호 정확히 할당
|
||||
- [ ] components.securitySchemes에 BearerAuth 정의
|
||||
- [ ] 인증 필요한 엔드포인트에 security 적용
|
||||
|
||||
### 엔드포인트
|
||||
- [ ] 모든 엔드포인트에 tags 지정
|
||||
- [ ] summary와 description 작성 (한글)
|
||||
- [ ] operationId camelCase로 작성
|
||||
- [ ] x-user-story UFR 코드 명시
|
||||
- [ ] x-controller 매핑 정보 포함
|
||||
|
||||
### 스키마
|
||||
- [ ] Request/Response 스키마 명명 규칙 준수
|
||||
- [ ] required 필드 명시
|
||||
- [ ] 모든 properties에 description과 example 포함
|
||||
- [ ] 적절한 format 사용 (date, date-time, email, uuid 등)
|
||||
|
||||
### 응답 구조
|
||||
- [ ] ErrorResponse 표준 스키마 사용
|
||||
- [ ] 공통 에러 응답 ($ref) 활용
|
||||
- [ ] 에러 코드 명명 규칙 준수
|
||||
- [ ] 페이징 응답에 PageInfo 사용
|
||||
|
||||
### 예제
|
||||
- [ ] requestBody에 최소 1개 이상 예제
|
||||
- [ ] 주요 response에 success 예제
|
||||
- [ ] 주요 에러 케이스에 예제
|
||||
|
||||
### 기술 명세 (해당 시)
|
||||
- [ ] 비동기 처리 서비스: x-technical-specifications 포함
|
||||
- [ ] Kafka 토픽, Redis 캐싱 정보 명시
|
||||
- [ ] 외부 API 연동 정보 포함
|
||||
|
||||
---
|
||||
|
||||
## 13. 참고 자료
|
||||
|
||||
### 서비스별 API 명세서
|
||||
- User Service API: `/design/backend/api/user-service-api.yaml`
|
||||
- Event Service API: `/design/backend/api/event-service-api.yaml`
|
||||
- Content Service API: `/design/backend/api/content-service-api.yaml`
|
||||
- AI Service API: `/design/backend/api/ai-service-api.yaml`
|
||||
- Participation Service API: `/design/backend/api/participation-service-api.yaml`
|
||||
- Distribution Service API: `/design/backend/api/distribution-service-api.yaml`
|
||||
- Analytics Service API: `/design/backend/api/analytics-service-api.yaml`
|
||||
|
||||
### OpenAPI 3.0.3 공식 문서
|
||||
- https://swagger.io/specification/
|
||||
|
||||
### 프로젝트 아키텍처
|
||||
- High-Level Architecture: `/design/high-level-architecture.md`
|
||||
- Logical Architecture: `/design/backend/logical/`
|
||||
|
||||
---
|
||||
|
||||
**문서 버전:** 1.0.0
|
||||
**최종 수정일:** 2025-10-23
|
||||
**작성자:** Digital Garage Team
|
||||
@@ -0,0 +1,849 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: AI Service API
|
||||
description: |
|
||||
KT AI 기반 소상공인 이벤트 자동 생성 서비스 - AI Service
|
||||
|
||||
## 서비스 개요
|
||||
- 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
|
||||
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Digital Garage Team
|
||||
email: support@kt-event-marketing.com
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8083
|
||||
description: Local Development Server
|
||||
- url: https://dev-api.kt-event-marketing.com/ai/v1
|
||||
description: Development Server
|
||||
- url: https://api.kt-event-marketing.com/ai/v1
|
||||
description: Production Server
|
||||
|
||||
tags:
|
||||
- name: Health Check
|
||||
description: 서비스 상태 확인
|
||||
- name: Internal API
|
||||
description: 내부 서비스 간 통신용 API
|
||||
- name: Kafka Consumer
|
||||
description: 비동기 작업 처리 (문서화만)
|
||||
|
||||
paths:
|
||||
/health:
|
||||
get:
|
||||
tags:
|
||||
- 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
|
||||
|
||||
/internal/jobs/{jobId}/status:
|
||||
get:
|
||||
tags:
|
||||
- Internal API
|
||||
summary: 작업 상태 조회
|
||||
description: Redis에 저장된 AI 추천 작업 상태 조회 (Event Service에서 호출)
|
||||
operationId: getJobStatus
|
||||
x-user-story: UFR-AI-010
|
||||
x-controller: InternalJobController
|
||||
parameters:
|
||||
- name: jobId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Job ID
|
||||
example: "job-ai-evt001-20251023103000"
|
||||
responses:
|
||||
'200':
|
||||
description: 작업 상태 조회 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/JobStatusResponse'
|
||||
examples:
|
||||
processing:
|
||||
summary: 처리 중
|
||||
value:
|
||||
jobId: "job-ai-evt001-20251023103000"
|
||||
status: "PROCESSING"
|
||||
progress: 50
|
||||
message: "AI 추천 생성 중"
|
||||
createdAt: "2025-10-23T10:30:00Z"
|
||||
startedAt: "2025-10-23T10:30:05Z"
|
||||
completed:
|
||||
summary: 완료
|
||||
value:
|
||||
jobId: "job-ai-evt001-20251023103000"
|
||||
status: "COMPLETED"
|
||||
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: 실패
|
||||
value:
|
||||
jobId: "job-ai-evt001-20251023103000"
|
||||
status: "FAILED"
|
||||
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:
|
||||
schemas:
|
||||
# ==================== Health Check ====================
|
||||
HealthCheckResponse:
|
||||
type: object
|
||||
description: 서비스 헬스체크 응답
|
||||
required:
|
||||
- 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
|
||||
properties:
|
||||
jobId:
|
||||
type: string
|
||||
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: 업종
|
||||
example: "음식점"
|
||||
region:
|
||||
type: string
|
||||
description: 지역 (시/구/동)
|
||||
example: "서울 강남구"
|
||||
storeName:
|
||||
type: string
|
||||
description: 매장명 (선택)
|
||||
example: "맛있는 고깃집"
|
||||
targetAudience:
|
||||
type: string
|
||||
description: 목표 고객층 (선택)
|
||||
example: "20-30대 여성"
|
||||
budget:
|
||||
type: integer
|
||||
description: 예산 (원) (선택)
|
||||
example: 500000
|
||||
requestedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 요청 시각
|
||||
example: "2025-10-23T10:30:00Z"
|
||||
|
||||
# ==================== AI Recommendation Result ====================
|
||||
AIRecommendationResult:
|
||||
type: object
|
||||
description: |
|
||||
**Redis Key**: `ai:recommendation:{eventId}`
|
||||
**TTL**: 86400초 (24시간)
|
||||
|
||||
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개)
|
||||
minItems: 3
|
||||
maxItems: 3
|
||||
items:
|
||||
$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:
|
||||
- optionNumber
|
||||
- concept
|
||||
- title
|
||||
- description
|
||||
- targetAudience
|
||||
- duration
|
||||
- mechanics
|
||||
- promotionChannels
|
||||
- estimatedCost
|
||||
- expectedMetrics
|
||||
- differentiator
|
||||
properties:
|
||||
optionNumber:
|
||||
type: integer
|
||||
description: 옵션 번호 (1-3)
|
||||
minimum: 1
|
||||
maximum: 3
|
||||
example: 1
|
||||
concept:
|
||||
type: string
|
||||
description: 이벤트 컨셉
|
||||
example: "프리미엄 경험형"
|
||||
title:
|
||||
type: string
|
||||
description: 이벤트 제목
|
||||
maxLength: 100
|
||||
example: "가을 한정 시그니처 디저트 페어링 이벤트"
|
||||
description:
|
||||
type: string
|
||||
description: 이벤트 설명
|
||||
maxLength: 500
|
||||
example: "가을 제철 재료를 활용한 시그니처 디저트와 음료 페어링 체험"
|
||||
targetAudience:
|
||||
type: string
|
||||
description: 목표 고객층
|
||||
example: "20-30대 여성, SNS 활동적인 고객"
|
||||
duration:
|
||||
type: object
|
||||
description: 이벤트 기간
|
||||
required:
|
||||
- recommendedDays
|
||||
properties:
|
||||
recommendedDays:
|
||||
type: integer
|
||||
description: 권장 진행 일수
|
||||
minimum: 1
|
||||
example: 14
|
||||
recommendedPeriod:
|
||||
type: string
|
||||
description: 권장 진행 시기
|
||||
example: "10월 중순 ~ 11월 초"
|
||||
mechanics:
|
||||
type: object
|
||||
description: 이벤트 메커니즘
|
||||
required:
|
||||
- type
|
||||
- details
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [DISCOUNT, GIFT, STAMP, EXPERIENCE, LOTTERY, COMBO]
|
||||
description: 이벤트 유형
|
||||
example: "EXPERIENCE"
|
||||
details:
|
||||
type: string
|
||||
description: 상세 메커니즘
|
||||
maxLength: 500
|
||||
example: "디저트+음료 페어링 세트 주문 시 인스타그램 업로드 고객에게 다음 방문 시 사용 가능한 10% 할인권 제공"
|
||||
promotionChannels:
|
||||
type: array
|
||||
description: 추천 홍보 채널 (최대 5개)
|
||||
maxItems: 5
|
||||
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 바이럴 효과 극대화, 브랜드 이미지 향상에 집중"
|
||||
|
||||
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
|
||||
- message
|
||||
- timestamp
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 에러 코드
|
||||
enum:
|
||||
- AI_SERVICE_ERROR
|
||||
- JOB_NOT_FOUND
|
||||
- RECOMMENDATION_NOT_FOUND
|
||||
- REDIS_ERROR
|
||||
- KAFKA_ERROR
|
||||
- CIRCUIT_BREAKER_OPEN
|
||||
- INTERNAL_ERROR
|
||||
example: "JOB_NOT_FOUND"
|
||||
message:
|
||||
type: string
|
||||
description: 에러 메시지
|
||||
example: "작업을 찾을 수 없습니다"
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 에러 발생 시각
|
||||
example: "2025-10-23T10:30:00Z"
|
||||
details:
|
||||
type: object
|
||||
description: 추가 에러 상세
|
||||
additionalProperties: true
|
||||
example:
|
||||
jobId: "job-ai-evt001-20251023103000"
|
||||
|
||||
responses:
|
||||
NotFound:
|
||||
description: 리소스를 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "JOB_NOT_FOUND"
|
||||
message: "작업을 찾을 수 없습니다"
|
||||
timestamp: "2025-10-23T10:30:00Z"
|
||||
|
||||
InternalServerError:
|
||||
description: 서버 내부 오류
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
code: "INTERNAL_ERROR"
|
||||
message: "서버 내부 오류가 발생했습니다"
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,651 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Distribution Service API
|
||||
description: |
|
||||
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: 실패 채널 스킵 및 알림
|
||||
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Digital Garage Team
|
||||
email: support@kt-event-marketing.com
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8085
|
||||
description: Local Development Server
|
||||
- url: https://dev-api.kt-event-marketing.com/distribution/v1
|
||||
description: Development Server
|
||||
- url: https://api.kt-event-marketing.com/distribution/v1
|
||||
description: Production Server
|
||||
|
||||
tags:
|
||||
- name: Distribution
|
||||
description: 다중 채널 배포 관리
|
||||
- name: Monitoring
|
||||
description: 배포 상태 모니터링
|
||||
|
||||
paths:
|
||||
/distribution/distribute:
|
||||
post:
|
||||
tags:
|
||||
- Distribution
|
||||
summary: 다중 채널 배포 요청
|
||||
description: |
|
||||
이벤트 콘텐츠를 선택된 채널들에 동시 배포합니다.
|
||||
|
||||
## 처리 흐름
|
||||
1. 배포 요청 검증 (이벤트 ID, 채널 목록, 콘텐츠 데이터)
|
||||
2. 채널별 병렬 배포 실행 (1분 이내 완료 목표)
|
||||
3. Circuit Breaker로 장애 채널 격리
|
||||
4. 실패 시 Retry (지수 백오프: 1s, 2s, 4s)
|
||||
5. Fallback: 실패 채널 스킵 및 알림
|
||||
6. 배포 결과 집계 및 로그 저장
|
||||
7. DistributionCompleted 이벤트 Kafka 발행
|
||||
|
||||
## Resilience 처리
|
||||
- 각 채널별 독립적인 Circuit Breaker 적용
|
||||
- 최대 3회 재시도 (지수 백오프)
|
||||
- 일부 채널 실패 시에도 성공 채널은 유지
|
||||
- 실패 채널 정보는 응답에 포함
|
||||
|
||||
x-user-story: UFR-DIST-010
|
||||
x-controller: DistributionController
|
||||
operationId: distributeToChannels
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DistributionRequest'
|
||||
examples:
|
||||
multiChannel:
|
||||
summary: 다중 채널 배포 예시
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
channels:
|
||||
- type: "WOORIDONGNE_TV"
|
||||
config:
|
||||
radius: "1km"
|
||||
timeSlots:
|
||||
- "weekday_evening"
|
||||
- "weekend_lunch"
|
||||
- type: "INSTAGRAM"
|
||||
config:
|
||||
scheduledTime: "2025-11-01T10:00:00Z"
|
||||
- type: "NAVER_BLOG"
|
||||
config:
|
||||
scheduledTime: "2025-11-01T10:30:00Z"
|
||||
contentUrls:
|
||||
instagram: "https://cdn.example.com/images/event-instagram.jpg"
|
||||
naverBlog: "https://cdn.example.com/images/event-naver.jpg"
|
||||
kakaoChannel: "https://cdn.example.com/images/event-kakao.jpg"
|
||||
responses:
|
||||
'200':
|
||||
description: 배포 완료
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DistributionResponse'
|
||||
examples:
|
||||
allSuccess:
|
||||
summary: 모든 채널 배포 성공
|
||||
value:
|
||||
distributionId: "dist-12345"
|
||||
eventId: "evt-12345"
|
||||
status: "COMPLETED"
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
results:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
status: "SUCCESS"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1000
|
||||
message: "배포 완료"
|
||||
- channel: "INSTAGRAM"
|
||||
status: "SUCCESS"
|
||||
postUrl: "https://instagram.com/p/generated-post-id"
|
||||
postId: "ig-post-12345"
|
||||
message: "게시 완료"
|
||||
- channel: "NAVER_BLOG"
|
||||
status: "SUCCESS"
|
||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||
message: "게시 완료"
|
||||
'400':
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalidEventId:
|
||||
summary: 유효하지 않은 이벤트 ID
|
||||
value:
|
||||
error: "BAD_REQUEST"
|
||||
message: "유효하지 않은 이벤트 ID입니다"
|
||||
timestamp: "2025-11-01T09:00:00Z"
|
||||
noChannels:
|
||||
summary: 선택된 채널 없음
|
||||
value:
|
||||
error: "BAD_REQUEST"
|
||||
message: "최소 1개 이상의 채널을 선택해야 합니다"
|
||||
timestamp: "2025-11-01T09:00:00Z"
|
||||
'404':
|
||||
description: 이벤트를 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
eventNotFound:
|
||||
summary: 존재하지 않는 이벤트
|
||||
value:
|
||||
error: "NOT_FOUND"
|
||||
message: "이벤트를 찾을 수 없습니다: evt-12345"
|
||||
timestamp: "2025-11-01T09:00:00Z"
|
||||
'500':
|
||||
description: 서버 내부 오류
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
internalError:
|
||||
summary: 서버 오류
|
||||
value:
|
||||
error: "INTERNAL_SERVER_ERROR"
|
||||
message: "배포 처리 중 오류가 발생했습니다"
|
||||
timestamp: "2025-11-01T09:00:00Z"
|
||||
|
||||
/distribution/{eventId}/status:
|
||||
get:
|
||||
tags:
|
||||
- Monitoring
|
||||
summary: 배포 상태 조회
|
||||
description: |
|
||||
특정 이벤트의 배포 상태를 실시간으로 조회합니다.
|
||||
|
||||
## 조회 정보
|
||||
- 전체 배포 상태 (진행중, 완료, 부분성공, 실패)
|
||||
- 채널별 배포 상태 및 결과
|
||||
- 실패 채널 상세 정보 (오류 유형, 재시도 횟수)
|
||||
- 배포 시작/완료 시간 및 소요 시간
|
||||
- 외부 채널 ID 및 배포 URL
|
||||
|
||||
## 상태 값
|
||||
- **IN_PROGRESS**: 배포 진행 중
|
||||
- **COMPLETED**: 모든 채널 배포 완료
|
||||
- **PARTIAL_SUCCESS**: 일부 채널 배포 성공
|
||||
- **FAILED**: 모든 채널 배포 실패
|
||||
|
||||
x-user-story: UFR-DIST-020
|
||||
x-controller: DistributionController
|
||||
operationId: getDistributionStatus
|
||||
parameters:
|
||||
- name: eventId
|
||||
in: path
|
||||
required: true
|
||||
description: 이벤트 ID
|
||||
schema:
|
||||
type: string
|
||||
example: "evt-12345"
|
||||
responses:
|
||||
'200':
|
||||
description: 배포 상태 조회 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DistributionStatusResponse'
|
||||
examples:
|
||||
completed:
|
||||
summary: 배포 완료 상태
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
overallStatus: "COMPLETED"
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
channels:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
status: "COMPLETED"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1500
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
- channel: "RINGO_BIZ"
|
||||
status: "COMPLETED"
|
||||
updateTimestamp: "2025-11-01T09:00:00Z"
|
||||
- channel: "GENIE_TV"
|
||||
status: "COMPLETED"
|
||||
adId: "gtv-uuid-12345"
|
||||
impressionSchedule:
|
||||
- "2025-11-01 18:00-20:00"
|
||||
- "2025-11-02 12:00-14:00"
|
||||
- channel: "INSTAGRAM"
|
||||
status: "COMPLETED"
|
||||
postUrl: "https://instagram.com/p/generated-post-id"
|
||||
postId: "ig-post-12345"
|
||||
- channel: "NAVER_BLOG"
|
||||
status: "COMPLETED"
|
||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||
- channel: "KAKAO_CHANNEL"
|
||||
status: "COMPLETED"
|
||||
messageId: "kakao-msg-12345"
|
||||
inProgress:
|
||||
summary: 배포 진행중 상태
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
overallStatus: "IN_PROGRESS"
|
||||
startedAt: "2025-11-01T08:58:00Z"
|
||||
channels:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
status: "COMPLETED"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1500
|
||||
- channel: "INSTAGRAM"
|
||||
status: "IN_PROGRESS"
|
||||
progress: 50
|
||||
- channel: "NAVER_BLOG"
|
||||
status: "PENDING"
|
||||
partialFailure:
|
||||
summary: 일부 채널 실패 상태
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
overallStatus: "PARTIAL_FAILURE"
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
channels:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
status: "COMPLETED"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1500
|
||||
- channel: "INSTAGRAM"
|
||||
status: "FAILED"
|
||||
errorMessage: "Instagram API 타임아웃"
|
||||
retries: 3
|
||||
lastRetryAt: "2025-11-01T08:59:30Z"
|
||||
- channel: "NAVER_BLOG"
|
||||
status: "COMPLETED"
|
||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||
'404':
|
||||
description: 배포 이력을 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
notFound:
|
||||
summary: 배포 이력 없음
|
||||
value:
|
||||
error: "NOT_FOUND"
|
||||
message: "배포 이력을 찾을 수 없습니다: evt-12345"
|
||||
timestamp: "2025-11-01T09:00:00Z"
|
||||
'500':
|
||||
description: 서버 내부 오류
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
DistributionRequest:
|
||||
type: object
|
||||
required:
|
||||
- eventId
|
||||
- channels
|
||||
- contentUrls
|
||||
properties:
|
||||
eventId:
|
||||
type: string
|
||||
description: 이벤트 ID
|
||||
example: "evt-12345"
|
||||
channels:
|
||||
type: array
|
||||
description: 배포할 채널 목록
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelConfig'
|
||||
contentUrls:
|
||||
type: object
|
||||
description: 플랫폼별 콘텐츠 URL
|
||||
properties:
|
||||
wooridongneTV:
|
||||
type: string
|
||||
description: 우리동네TV 영상 URL (15초)
|
||||
example: "https://cdn.example.com/videos/event-15s.mp4"
|
||||
ringoBiz:
|
||||
type: string
|
||||
description: 링고비즈 연결음 파일 URL
|
||||
example: "https://cdn.example.com/audio/ringtone.mp3"
|
||||
genieTV:
|
||||
type: string
|
||||
description: 지니TV 광고 영상 URL
|
||||
example: "https://cdn.example.com/videos/event-ad.mp4"
|
||||
instagram:
|
||||
type: string
|
||||
description: Instagram 이미지 URL (1080x1080)
|
||||
example: "https://cdn.example.com/images/event-instagram.jpg"
|
||||
naverBlog:
|
||||
type: string
|
||||
description: Naver Blog 이미지 URL (800x600)
|
||||
example: "https://cdn.example.com/images/event-naver.jpg"
|
||||
kakaoChannel:
|
||||
type: string
|
||||
description: Kakao Channel 이미지 URL (800x800)
|
||||
example: "https://cdn.example.com/images/event-kakao.jpg"
|
||||
|
||||
ChannelConfig:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 채널 타입
|
||||
enum:
|
||||
- WOORIDONGNE_TV
|
||||
- RINGO_BIZ
|
||||
- GENIE_TV
|
||||
- INSTAGRAM
|
||||
- NAVER_BLOG
|
||||
- KAKAO_CHANNEL
|
||||
example: "INSTAGRAM"
|
||||
config:
|
||||
type: object
|
||||
description: 채널별 설정 (채널에 따라 다름)
|
||||
additionalProperties: true
|
||||
example:
|
||||
scheduledTime: "2025-11-01T10:00:00Z"
|
||||
caption: "이벤트 안내"
|
||||
hashtags:
|
||||
- "이벤트"
|
||||
- "할인"
|
||||
|
||||
DistributionResponse:
|
||||
type: object
|
||||
required:
|
||||
- distributionId
|
||||
- eventId
|
||||
- status
|
||||
- results
|
||||
properties:
|
||||
distributionId:
|
||||
type: string
|
||||
description: 배포 ID
|
||||
example: "dist-12345"
|
||||
eventId:
|
||||
type: string
|
||||
description: 이벤트 ID
|
||||
example: "evt-12345"
|
||||
status:
|
||||
type: string
|
||||
description: 전체 배포 상태
|
||||
enum:
|
||||
- PENDING
|
||||
- IN_PROGRESS
|
||||
- COMPLETED
|
||||
- PARTIAL_FAILURE
|
||||
- FAILED
|
||||
example: "COMPLETED"
|
||||
startedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 배포 시작 시각
|
||||
example: "2025-11-01T08:59:00Z"
|
||||
completedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 배포 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
results:
|
||||
type: array
|
||||
description: 채널별 배포 결과
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelResult'
|
||||
|
||||
ChannelResult:
|
||||
type: object
|
||||
required:
|
||||
- channel
|
||||
- status
|
||||
properties:
|
||||
channel:
|
||||
type: string
|
||||
description: 채널 타입
|
||||
enum:
|
||||
- WOORIDONGNE_TV
|
||||
- RINGO_BIZ
|
||||
- GENIE_TV
|
||||
- INSTAGRAM
|
||||
- NAVER_BLOG
|
||||
- KAKAO_CHANNEL
|
||||
example: "INSTAGRAM"
|
||||
status:
|
||||
type: string
|
||||
description: 채널별 배포 상태
|
||||
enum:
|
||||
- PENDING
|
||||
- IN_PROGRESS
|
||||
- SUCCESS
|
||||
- FAILED
|
||||
example: "SUCCESS"
|
||||
distributionId:
|
||||
type: string
|
||||
description: 채널별 배포 ID (우리동네TV, 지니TV)
|
||||
example: "wtv-uuid-12345"
|
||||
estimatedViews:
|
||||
type: integer
|
||||
description: 예상 노출 수 (우리동네TV, 지니TV)
|
||||
example: 1500
|
||||
updateTimestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 업데이트 완료 시각 (링고비즈)
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
adId:
|
||||
type: string
|
||||
description: 광고 ID (지니TV)
|
||||
example: "gtv-uuid-12345"
|
||||
impressionSchedule:
|
||||
type: array
|
||||
description: 노출 스케줄 (지니TV)
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- "2025-11-01 18:00-20:00"
|
||||
- "2025-11-02 12:00-14:00"
|
||||
postUrl:
|
||||
type: string
|
||||
description: 게시물 URL (Instagram, Naver Blog)
|
||||
example: "https://instagram.com/p/generated-post-id"
|
||||
postId:
|
||||
type: string
|
||||
description: 게시물 ID (Instagram)
|
||||
example: "ig-post-12345"
|
||||
messageId:
|
||||
type: string
|
||||
description: 메시지 ID (Kakao Channel)
|
||||
example: "kakao-msg-12345"
|
||||
message:
|
||||
type: string
|
||||
description: 결과 메시지
|
||||
example: "배포 완료"
|
||||
errorMessage:
|
||||
type: string
|
||||
description: 오류 메시지 (실패 시)
|
||||
example: "Instagram API 타임아웃"
|
||||
retries:
|
||||
type: integer
|
||||
description: 재시도 횟수
|
||||
example: 0
|
||||
lastRetryAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 마지막 재시도 시각
|
||||
example: "2025-11-01T08:59:30Z"
|
||||
|
||||
DistributionStatusResponse:
|
||||
type: object
|
||||
required:
|
||||
- eventId
|
||||
- overallStatus
|
||||
- channels
|
||||
properties:
|
||||
eventId:
|
||||
type: string
|
||||
description: 이벤트 ID
|
||||
example: "evt-12345"
|
||||
overallStatus:
|
||||
type: string
|
||||
description: 전체 배포 상태
|
||||
enum:
|
||||
- PENDING
|
||||
- IN_PROGRESS
|
||||
- COMPLETED
|
||||
- PARTIAL_FAILURE
|
||||
- FAILED
|
||||
- NOT_FOUND
|
||||
example: "COMPLETED"
|
||||
startedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 배포 시작 시각
|
||||
example: "2025-11-01T08:59:00Z"
|
||||
completedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 배포 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
channels:
|
||||
type: array
|
||||
description: 채널별 배포 상태
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelStatus'
|
||||
|
||||
ChannelStatus:
|
||||
type: object
|
||||
required:
|
||||
- channel
|
||||
- status
|
||||
properties:
|
||||
channel:
|
||||
type: string
|
||||
description: 채널 타입
|
||||
enum:
|
||||
- WOORIDONGNE_TV
|
||||
- RINGO_BIZ
|
||||
- GENIE_TV
|
||||
- INSTAGRAM
|
||||
- NAVER_BLOG
|
||||
- KAKAO_CHANNEL
|
||||
example: "INSTAGRAM"
|
||||
status:
|
||||
type: string
|
||||
description: 채널별 배포 상태
|
||||
enum:
|
||||
- PENDING
|
||||
- IN_PROGRESS
|
||||
- COMPLETED
|
||||
- FAILED
|
||||
example: "COMPLETED"
|
||||
progress:
|
||||
type: integer
|
||||
description: 진행률 (0-100, IN_PROGRESS 상태일 때)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
example: 75
|
||||
distributionId:
|
||||
type: string
|
||||
description: 채널별 배포 ID
|
||||
example: "wtv-uuid-12345"
|
||||
estimatedViews:
|
||||
type: integer
|
||||
description: 예상 노출 수
|
||||
example: 1500
|
||||
updateTimestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 업데이트 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
adId:
|
||||
type: string
|
||||
description: 광고 ID
|
||||
example: "gtv-uuid-12345"
|
||||
impressionSchedule:
|
||||
type: array
|
||||
description: 노출 스케줄
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- "2025-11-01 18:00-20:00"
|
||||
postUrl:
|
||||
type: string
|
||||
description: 게시물 URL
|
||||
example: "https://instagram.com/p/generated-post-id"
|
||||
postId:
|
||||
type: string
|
||||
description: 게시물 ID
|
||||
example: "ig-post-12345"
|
||||
messageId:
|
||||
type: string
|
||||
description: 메시지 ID
|
||||
example: "kakao-msg-12345"
|
||||
completedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
errorMessage:
|
||||
type: string
|
||||
description: 오류 메시지
|
||||
example: "Instagram API 타임아웃"
|
||||
retries:
|
||||
type: integer
|
||||
description: 재시도 횟수
|
||||
example: 3
|
||||
lastRetryAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 마지막 재시도 시각
|
||||
example: "2025-11-01T08:59:30Z"
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
- message
|
||||
- timestamp
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: 오류 코드
|
||||
enum:
|
||||
- BAD_REQUEST
|
||||
- NOT_FOUND
|
||||
- INTERNAL_SERVER_ERROR
|
||||
example: "BAD_REQUEST"
|
||||
message:
|
||||
type: string
|
||||
description: 오류 메시지
|
||||
example: "유효하지 않은 이벤트 ID입니다"
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 오류 발생 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
details:
|
||||
type: object
|
||||
description: 추가 오류 정보 (선택 사항)
|
||||
additionalProperties: true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,820 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Participation Service API
|
||||
description: |
|
||||
이벤트 참여 및 당첨자 관리 서비스 API
|
||||
- 이벤트 참여 등록
|
||||
- 참여자 목록 조회 및 관리
|
||||
- 당첨자 추첨 및 관리
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Digital Garage Team
|
||||
email: support@kt-event-marketing.com
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8084
|
||||
description: Local Development Server
|
||||
- url: https://dev-api.kt-event-marketing.com/participation/v1
|
||||
description: Development Server
|
||||
- url: https://api.kt-event-marketing.com/participation/v1
|
||||
description: Production Server
|
||||
|
||||
tags:
|
||||
- name: participation
|
||||
description: 이벤트 참여 관리
|
||||
- name: participant
|
||||
description: 참여자 조회 및 관리
|
||||
- name: winner
|
||||
description: 당첨자 추첨 및 관리
|
||||
|
||||
paths:
|
||||
/events/{eventId}/participate:
|
||||
post:
|
||||
tags:
|
||||
- participation
|
||||
summary: 이벤트 참여
|
||||
description: |
|
||||
고객이 이벤트에 참여합니다.
|
||||
- 중복 참여 검증 (전화번호 기반)
|
||||
- 이벤트 진행 상태 검증
|
||||
- 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/ParticipationRequest'
|
||||
examples:
|
||||
standard:
|
||||
summary: 일반 참여
|
||||
value:
|
||||
name: "홍길동"
|
||||
phoneNumber: "010-1234-5678"
|
||||
email: "hong@example.com"
|
||||
agreeMarketing: true
|
||||
agreePrivacy: true
|
||||
storeVisited: false
|
||||
storeVisit:
|
||||
summary: 매장 방문 참여
|
||||
value:
|
||||
name: "김철수"
|
||||
phoneNumber: "010-9876-5432"
|
||||
email: "kim@example.com"
|
||||
agreeMarketing: false
|
||||
agreePrivacy: true
|
||||
storeVisited: true
|
||||
responses:
|
||||
'201':
|
||||
description: 참여 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ParticipationResponse'
|
||||
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
|
||||
'400':
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalidPhone:
|
||||
summary: 유효하지 않은 전화번호
|
||||
value:
|
||||
success: false
|
||||
error:
|
||||
code: "INVALID_PHONE_NUMBER"
|
||||
message: "유효하지 않은 전화번호 형식입니다"
|
||||
duplicateParticipation:
|
||||
summary: 중복 참여
|
||||
value:
|
||||
success: false
|
||||
error:
|
||||
code: "DUPLICATE_PARTICIPATION"
|
||||
message: "이미 참여하신 이벤트입니다"
|
||||
'404':
|
||||
description: 이벤트를 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
notFound:
|
||||
summary: 이벤트 없음
|
||||
value:
|
||||
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: "현재 참여할 수 없는 이벤트입니다"
|
||||
|
||||
/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'
|
||||
|
||||
/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: "참여자를 찾을 수 없습니다"
|
||||
|
||||
/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: "이미 당첨자 추첨이 완료되었습니다"
|
||||
|
||||
/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:
|
||||
schemas:
|
||||
ParticipationRequest:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- phoneNumber
|
||||
- agreePrivacy
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 참여자 이름
|
||||
minLength: 2
|
||||
maxLength: 50
|
||||
example: "홍길동"
|
||||
phoneNumber:
|
||||
type: string
|
||||
description: 참여자 전화번호 (하이픈 포함)
|
||||
pattern: '^\d{3}-\d{3,4}-\d{4}$'
|
||||
example: "010-1234-5678"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 참여자 이메일
|
||||
example: "hong@example.com"
|
||||
agreeMarketing:
|
||||
type: boolean
|
||||
description: 마케팅 정보 수신 동의
|
||||
default: false
|
||||
example: true
|
||||
agreePrivacy:
|
||||
type: boolean
|
||||
description: 개인정보 수집 및 이용 동의 (필수)
|
||||
example: true
|
||||
storeVisited:
|
||||
type: boolean
|
||||
description: 매장 방문 여부
|
||||
default: false
|
||||
example: false
|
||||
|
||||
ParticipationResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
example: "이벤트 참여가 완료되었습니다"
|
||||
data:
|
||||
$ref: '#/components/schemas/ParticipantInfo'
|
||||
|
||||
ParticipantInfo:
|
||||
type: object
|
||||
properties:
|
||||
participantId:
|
||||
type: string
|
||||
description: 참여자 ID
|
||||
example: "prt_20250123_001"
|
||||
eventId:
|
||||
type: string
|
||||
description: 이벤트 ID
|
||||
example: "evt_20250123_001"
|
||||
name:
|
||||
type: string
|
||||
description: 참여자 이름
|
||||
example: "홍길동"
|
||||
phoneNumber:
|
||||
type: string
|
||||
description: 참여자 전화번호
|
||||
example: "010-1234-5678"
|
||||
email:
|
||||
type: string
|
||||
description: 참여자 이메일
|
||||
example: "hong@example.com"
|
||||
participatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 참여 일시
|
||||
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
|
||||
|
||||
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:
|
||||
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
|
||||
required:
|
||||
- winnerCount
|
||||
properties:
|
||||
winnerCount:
|
||||
type: integer
|
||||
description: 당첨자 수
|
||||
minimum: 1
|
||||
example: 10
|
||||
applyStoreVisitBonus:
|
||||
type: boolean
|
||||
description: 매장 방문 보너스 적용 여부
|
||||
default: true
|
||||
example: true
|
||||
|
||||
DrawWinnersResponse:
|
||||
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"
|
||||
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
|
||||
properties:
|
||||
participantId:
|
||||
type: string
|
||||
description: 참여자 ID
|
||||
example: "prt_20250123_002"
|
||||
name:
|
||||
type: string
|
||||
description: 당첨자 이름
|
||||
example: "김철수"
|
||||
phoneNumber:
|
||||
type: string
|
||||
description: 당첨자 전화번호
|
||||
example: "010-9876-5432"
|
||||
email:
|
||||
type: string
|
||||
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: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 에러 코드
|
||||
example: "DUPLICATE_PARTICIPATION"
|
||||
message:
|
||||
type: string
|
||||
description: 에러 메시지
|
||||
example: "이미 참여하신 이벤트입니다"
|
||||
details:
|
||||
type: object
|
||||
description: 추가 에러 상세 정보
|
||||
additionalProperties: true
|
||||
nullable: true
|
||||
@@ -0,0 +1,991 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: User Service API
|
||||
description: |
|
||||
KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service API
|
||||
|
||||
사용자 인증 및 매장정보 관리를 담당하는 마이크로서비스
|
||||
|
||||
**주요 기능:**
|
||||
- 회원가입
|
||||
- 로그인/로그아웃
|
||||
- 프로필 조회 및 수정
|
||||
- 비밀번호 변경
|
||||
|
||||
**보안:**
|
||||
- JWT Bearer 토큰 기반 인증
|
||||
- bcrypt 비밀번호 해싱
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Digital Garage Team
|
||||
email: support@kt-event-marketing.com
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8081
|
||||
description: Local Development Server
|
||||
- url: https://dev-api.kt-event-marketing.com/user/v1
|
||||
description: Development Server
|
||||
- url: https://api.kt-event-marketing.com/user/v1
|
||||
description: Production Server
|
||||
|
||||
tags:
|
||||
- name: Authentication
|
||||
description: 인증 관련 API (로그인, 로그아웃, 회원가입)
|
||||
- name: Profile
|
||||
description: 프로필 관련 API (조회, 수정, 비밀번호 변경)
|
||||
|
||||
paths:
|
||||
/users/register:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: 회원가입
|
||||
description: |
|
||||
소상공인 회원가입 API
|
||||
|
||||
**유저스토리:** UFR-USER-010
|
||||
|
||||
**주요 기능:**
|
||||
- 기본 정보 및 매장 정보 등록
|
||||
- 비밀번호 bcrypt 해싱
|
||||
- JWT 토큰 자동 발급
|
||||
|
||||
**처리 흐름:**
|
||||
1. 중복 사용자 확인 (전화번호 기반)
|
||||
2. 비밀번호 해싱 (bcrypt)
|
||||
3. User/Store 데이터베이스 트랜잭션 처리
|
||||
4. JWT 토큰 생성 및 세션 저장 (Redis)
|
||||
operationId: registerUser
|
||||
x-user-story: UFR-USER-010
|
||||
x-controller: UserController
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterRequest'
|
||||
examples:
|
||||
restaurant:
|
||||
summary: 음식점 회원가입 예시
|
||||
value:
|
||||
name: 홍길동
|
||||
phoneNumber: "01012345678"
|
||||
email: hong@example.com
|
||||
password: "Password123!"
|
||||
storeName: 맛있는집
|
||||
industry: 음식점
|
||||
address: 서울시 강남구 테헤란로 123
|
||||
businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
|
||||
cafe:
|
||||
summary: 카페 회원가입 예시
|
||||
value:
|
||||
name: 김철수
|
||||
phoneNumber: "01087654321"
|
||||
email: kim@example.com
|
||||
password: "SecurePass456!"
|
||||
storeName: 아메리카노 카페
|
||||
industry: 카페
|
||||
address: 서울시 서초구 서초대로 456
|
||||
businessHours: "매일 09:00-20:00"
|
||||
responses:
|
||||
'201':
|
||||
description: 회원가입 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RegisterResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 회원가입 성공 응답
|
||||
value:
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
userId: 123
|
||||
userName: 홍길동
|
||||
storeId: 456
|
||||
storeName: 맛있는집
|
||||
'400':
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
duplicateUser:
|
||||
summary: 중복 사용자
|
||||
value:
|
||||
code: USER_001
|
||||
message: 이미 가입된 전화번호입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
validationError:
|
||||
summary: 입력 검증 오류
|
||||
value:
|
||||
code: VALIDATION_ERROR
|
||||
message: 비밀번호는 8자 이상이어야 합니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
'500':
|
||||
description: 서버 오류
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/users/login:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: 로그인
|
||||
description: |
|
||||
소상공인 로그인 API
|
||||
|
||||
**유저스토리:** UFR-USER-020
|
||||
|
||||
**주요 기능:**
|
||||
- 전화번호/비밀번호 인증
|
||||
- JWT 토큰 발급
|
||||
- Redis 세션 저장
|
||||
- 최종 로그인 시각 업데이트 (비동기)
|
||||
|
||||
**보안:**
|
||||
- Timing Attack 방어 (에러 메시지 통일)
|
||||
- bcrypt 비밀번호 검증
|
||||
- JWT 토큰 7일 만료
|
||||
operationId: loginUser
|
||||
x-user-story: UFR-USER-020
|
||||
x-controller: UserController
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginRequest'
|
||||
examples:
|
||||
default:
|
||||
summary: 로그인 요청 예시
|
||||
value:
|
||||
phoneNumber: "01012345678"
|
||||
password: "Password123!"
|
||||
responses:
|
||||
'200':
|
||||
description: 로그인 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LoginResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 로그인 성공 응답
|
||||
value:
|
||||
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
userId: 123
|
||||
userName: 홍길동
|
||||
role: OWNER
|
||||
email: hong@example.com
|
||||
'401':
|
||||
description: 인증 실패
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
authFailed:
|
||||
summary: 인증 실패
|
||||
value:
|
||||
code: AUTH_001
|
||||
message: 전화번호 또는 비밀번호를 확인해주세요
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
/users/logout:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: 로그아웃
|
||||
description: |
|
||||
로그아웃 API
|
||||
|
||||
**유저스토리:** UFR-USER-040
|
||||
|
||||
**주요 기능:**
|
||||
- Redis 세션 삭제
|
||||
- JWT 토큰 Blacklist 추가
|
||||
- 멱등성 보장
|
||||
|
||||
**처리 흐름:**
|
||||
1. JWT 토큰 검증
|
||||
2. Redis 세션 삭제
|
||||
3. JWT Blacklist 추가 (남은 만료 시간만큼 TTL 설정)
|
||||
4. 로그아웃 이벤트 발행
|
||||
operationId: logoutUser
|
||||
x-user-story: UFR-USER-040
|
||||
x-controller: UserController
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 로그아웃 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LogoutResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 로그아웃 성공 응답
|
||||
value:
|
||||
success: true
|
||||
message: 안전하게 로그아웃되었습니다
|
||||
'401':
|
||||
description: 인증 실패 (유효하지 않은 토큰)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalidToken:
|
||||
summary: 유효하지 않은 토큰
|
||||
value:
|
||||
code: AUTH_002
|
||||
message: 유효하지 않은 토큰입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
/users/profile:
|
||||
get:
|
||||
tags:
|
||||
- Profile
|
||||
summary: 프로필 조회
|
||||
description: |
|
||||
사용자 프로필 조회 API
|
||||
|
||||
**유저스토리:** UFR-USER-030
|
||||
|
||||
**조회 정보:**
|
||||
- 기본 정보 (이름, 전화번호, 이메일)
|
||||
- 매장 정보 (매장명, 업종, 주소, 영업시간)
|
||||
operationId: getProfile
|
||||
x-user-story: UFR-USER-030
|
||||
x-controller: UserController
|
||||
security:
|
||||
- BearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: 프로필 조회 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProfileResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 프로필 조회 성공 응답
|
||||
value:
|
||||
userId: 123
|
||||
userName: 홍길동
|
||||
phoneNumber: "01012345678"
|
||||
email: hong@example.com
|
||||
role: OWNER
|
||||
storeId: 456
|
||||
storeName: 맛있는집
|
||||
industry: 음식점
|
||||
address: 서울시 강남구 테헤란로 123
|
||||
businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00"
|
||||
createdAt: 2025-09-01T10:00:00Z
|
||||
lastLoginAt: 2025-10-22T09:00:00Z
|
||||
'401':
|
||||
description: 인증 실패
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: 사용자를 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
notFound:
|
||||
summary: 사용자 없음
|
||||
value:
|
||||
code: USER_003
|
||||
message: 사용자를 찾을 수 없습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
put:
|
||||
tags:
|
||||
- Profile
|
||||
summary: 프로필 수정
|
||||
description: |
|
||||
사용자 프로필 수정 API
|
||||
|
||||
**유저스토리:** UFR-USER-030
|
||||
|
||||
**수정 가능 항목:**
|
||||
- 기본 정보: 이름, 전화번호, 이메일
|
||||
- 매장 정보: 매장명, 업종, 주소, 영업시간
|
||||
|
||||
**주의사항:**
|
||||
- 비밀번호 변경은 별도 API 사용 (/users/password)
|
||||
- 전화번호 변경 시 향후 재인증 필요 (현재는 직접 변경 가능)
|
||||
- Optimistic Locking으로 동시성 제어
|
||||
operationId: updateProfile
|
||||
x-user-story: UFR-USER-030
|
||||
x-controller: UserController
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateProfileRequest'
|
||||
examples:
|
||||
fullUpdate:
|
||||
summary: 전체 정보 수정
|
||||
value:
|
||||
name: 홍길동
|
||||
phoneNumber: "01012345678"
|
||||
email: hong.new@example.com
|
||||
storeName: 맛있는집 (리뉴얼)
|
||||
industry: 퓨전음식점
|
||||
address: 서울시 강남구 테헤란로 456
|
||||
businessHours: "매일 11:00-23:00"
|
||||
partialUpdate:
|
||||
summary: 일부 정보 수정 (이메일, 영업시간)
|
||||
value:
|
||||
email: hong.updated@example.com
|
||||
businessHours: "월-금 10:00-22:00, 토-일 휴무"
|
||||
responses:
|
||||
'200':
|
||||
description: 프로필 수정 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateProfileResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 프로필 수정 성공 응답
|
||||
value:
|
||||
userId: 123
|
||||
userName: 홍길동
|
||||
email: hong.new@example.com
|
||||
storeId: 456
|
||||
storeName: 맛있는집 (리뉴얼)
|
||||
'400':
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'401':
|
||||
description: 인증 실패
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'404':
|
||||
description: 사용자를 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
'409':
|
||||
description: 동시성 충돌 (다른 세션에서 수정)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
conflict:
|
||||
summary: 동시성 충돌
|
||||
value:
|
||||
code: USER_005
|
||||
message: 다른 세션에서 프로필을 수정했습니다. 새로고침 후 다시 시도하세요
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
|
||||
/users/password:
|
||||
put:
|
||||
tags:
|
||||
- Profile
|
||||
summary: 비밀번호 변경
|
||||
description: |
|
||||
비밀번호 변경 API
|
||||
|
||||
**유저스토리:** UFR-USER-030
|
||||
|
||||
**주요 기능:**
|
||||
- 현재 비밀번호 확인 필수
|
||||
- 새 비밀번호 규칙 검증 (8자 이상, 영문/숫자/특수문자 포함)
|
||||
- bcrypt 해싱
|
||||
|
||||
**보안:**
|
||||
- 현재 비밀번호 검증 실패 시 400 Bad Request
|
||||
- 비밀번호 변경 후 기존 세션 유지 (로그아웃 불필요)
|
||||
operationId: changePassword
|
||||
x-user-story: UFR-USER-030
|
||||
x-controller: UserController
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChangePasswordRequest'
|
||||
examples:
|
||||
default:
|
||||
summary: 비밀번호 변경 요청
|
||||
value:
|
||||
currentPassword: "Password123!"
|
||||
newPassword: "NewSecurePass456!"
|
||||
responses:
|
||||
'200':
|
||||
description: 비밀번호 변경 성공
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ChangePasswordResponse'
|
||||
examples:
|
||||
success:
|
||||
summary: 비밀번호 변경 성공 응답
|
||||
value:
|
||||
success: true
|
||||
message: 비밀번호가 성공적으로 변경되었습니다
|
||||
'400':
|
||||
description: 현재 비밀번호 불일치 또는 새 비밀번호 규칙 위반
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
invalidCurrentPassword:
|
||||
summary: 현재 비밀번호 불일치
|
||||
value:
|
||||
code: USER_004
|
||||
message: 현재 비밀번호가 일치하지 않습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
invalidNewPassword:
|
||||
summary: 새 비밀번호 규칙 위반
|
||||
value:
|
||||
code: VALIDATION_ERROR
|
||||
message: 비밀번호는 8자 이상이어야 하며 영문/숫자/특수문자를 포함해야 합니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
'401':
|
||||
description: 인증 실패
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
/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"
|
||||
'401':
|
||||
description: 인증 실패
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
unauthorized:
|
||||
summary: 인증 실패
|
||||
value:
|
||||
code: AUTH_002
|
||||
message: 유효하지 않은 토큰입니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
'403':
|
||||
description: 권한 없음 (내부 서비스만 접근 가능)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
forbidden:
|
||||
summary: 권한 없음
|
||||
value:
|
||||
code: AUTH_003
|
||||
message: 이 API는 내부 서비스만 접근 가능합니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
'404':
|
||||
description: 사용자 또는 매장을 찾을 수 없음
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
examples:
|
||||
notFound:
|
||||
summary: 사용자 또는 매장 없음
|
||||
value:
|
||||
code: USER_003
|
||||
message: 사용자 또는 매장을 찾을 수 없습니다
|
||||
timestamp: 2025-10-22T10:30:00Z
|
||||
'500':
|
||||
description: 서버 오류
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT Bearer 토큰 인증
|
||||
|
||||
**형식:** Authorization: Bearer {JWT_TOKEN}
|
||||
|
||||
**토큰 만료:** 7일
|
||||
|
||||
**Claims:**
|
||||
- userId: 사용자 ID
|
||||
- role: 사용자 역할 (OWNER)
|
||||
- iat: 발급 시각
|
||||
- exp: 만료 시각
|
||||
|
||||
schemas:
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- phoneNumber
|
||||
- email
|
||||
- password
|
||||
- storeName
|
||||
- industry
|
||||
- address
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 2
|
||||
maxLength: 50
|
||||
description: 사용자 이름 (2자 이상, 한글/영문)
|
||||
example: 홍길동
|
||||
phoneNumber:
|
||||
type: string
|
||||
pattern: '^010\d{8}$'
|
||||
description: 휴대폰 번호 (010XXXXXXXX)
|
||||
example: "01012345678"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
maxLength: 100
|
||||
description: 이메일 주소
|
||||
example: hong@example.com
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 100
|
||||
description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
|
||||
example: "Password123!"
|
||||
storeName:
|
||||
type: string
|
||||
minLength: 2
|
||||
maxLength: 100
|
||||
description: 매장명
|
||||
example: 맛있는집
|
||||
industry:
|
||||
type: string
|
||||
maxLength: 50
|
||||
description: 업종 (예 음식점, 카페, 소매점 등)
|
||||
example: 음식점
|
||||
address:
|
||||
type: string
|
||||
minLength: 5
|
||||
maxLength: 200
|
||||
description: 매장 주소
|
||||
example: 서울시 강남구 테헤란로 123
|
||||
businessHours:
|
||||
type: string
|
||||
maxLength: 200
|
||||
description: 영업시간 (선택 사항)
|
||||
example: "월-금 11:00-22:00, 토-일 12:00-21:00"
|
||||
|
||||
RegisterResponse:
|
||||
type: object
|
||||
required:
|
||||
- token
|
||||
- userId
|
||||
- userName
|
||||
- storeId
|
||||
- storeName
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: JWT 토큰 (7일 만료)
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 사용자 ID
|
||||
example: 123
|
||||
userName:
|
||||
type: string
|
||||
description: 사용자 이름
|
||||
example: 홍길동
|
||||
storeId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 매장 ID
|
||||
example: 456
|
||||
storeName:
|
||||
type: string
|
||||
description: 매장명
|
||||
example: 맛있는집
|
||||
|
||||
LoginRequest:
|
||||
type: object
|
||||
required:
|
||||
- phoneNumber
|
||||
- password
|
||||
properties:
|
||||
phoneNumber:
|
||||
type: string
|
||||
pattern: '^010\d{8}$'
|
||||
description: 휴대폰 번호
|
||||
example: "01012345678"
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
description: 비밀번호
|
||||
example: "Password123!"
|
||||
|
||||
LoginResponse:
|
||||
type: object
|
||||
required:
|
||||
- token
|
||||
- userId
|
||||
- userName
|
||||
- role
|
||||
- email
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
description: JWT 토큰 (7일 만료)
|
||||
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 사용자 ID
|
||||
example: 123
|
||||
userName:
|
||||
type: string
|
||||
description: 사용자 이름
|
||||
example: 홍길동
|
||||
role:
|
||||
type: string
|
||||
enum: [OWNER, ADMIN]
|
||||
description: 사용자 역할
|
||||
example: OWNER
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 이메일 주소
|
||||
example: hong@example.com
|
||||
|
||||
LogoutResponse:
|
||||
type: object
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: 로그아웃 성공 여부
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
description: 응답 메시지
|
||||
example: 안전하게 로그아웃되었습니다
|
||||
|
||||
ProfileResponse:
|
||||
type: object
|
||||
required:
|
||||
- userId
|
||||
- userName
|
||||
- phoneNumber
|
||||
- email
|
||||
- role
|
||||
- storeId
|
||||
- storeName
|
||||
- industry
|
||||
- address
|
||||
properties:
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 사용자 ID
|
||||
example: 123
|
||||
userName:
|
||||
type: string
|
||||
description: 사용자 이름
|
||||
example: 홍길동
|
||||
phoneNumber:
|
||||
type: string
|
||||
description: 휴대폰 번호
|
||||
example: "01012345678"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 이메일 주소
|
||||
example: hong@example.com
|
||||
role:
|
||||
type: string
|
||||
enum: [OWNER, ADMIN]
|
||||
description: 사용자 역할
|
||||
example: OWNER
|
||||
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"
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 가입 일시
|
||||
example: 2025-09-01T10:00:00Z
|
||||
lastLoginAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 최종 로그인 일시
|
||||
example: 2025-10-22T09:00:00Z
|
||||
|
||||
UpdateProfileRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 2
|
||||
maxLength: 50
|
||||
description: 사용자 이름 (선택 사항)
|
||||
example: 홍길동
|
||||
phoneNumber:
|
||||
type: string
|
||||
pattern: '^010\d{8}$'
|
||||
description: 휴대폰 번호 (선택 사항, 향후 재인증 필요)
|
||||
example: "01012345678"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
maxLength: 100
|
||||
description: 이메일 주소 (선택 사항)
|
||||
example: hong.new@example.com
|
||||
storeName:
|
||||
type: string
|
||||
minLength: 2
|
||||
maxLength: 100
|
||||
description: 매장명 (선택 사항)
|
||||
example: 맛있는집 (리뉴얼)
|
||||
industry:
|
||||
type: string
|
||||
maxLength: 50
|
||||
description: 업종 (선택 사항)
|
||||
example: 퓨전음식점
|
||||
address:
|
||||
type: string
|
||||
minLength: 5
|
||||
maxLength: 200
|
||||
description: 매장 주소 (선택 사항)
|
||||
example: 서울시 강남구 테헤란로 456
|
||||
businessHours:
|
||||
type: string
|
||||
maxLength: 200
|
||||
description: 영업시간 (선택 사항)
|
||||
example: "매일 11:00-23:00"
|
||||
|
||||
UpdateProfileResponse:
|
||||
type: object
|
||||
required:
|
||||
- userId
|
||||
- userName
|
||||
- email
|
||||
- storeId
|
||||
- storeName
|
||||
properties:
|
||||
userId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 사용자 ID
|
||||
example: 123
|
||||
userName:
|
||||
type: string
|
||||
description: 사용자 이름
|
||||
example: 홍길동
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: 이메일 주소
|
||||
example: hong.new@example.com
|
||||
storeId:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 매장 ID
|
||||
example: 456
|
||||
storeName:
|
||||
type: string
|
||||
description: 매장명
|
||||
example: 맛있는집 (리뉴얼)
|
||||
|
||||
ChangePasswordRequest:
|
||||
type: object
|
||||
required:
|
||||
- currentPassword
|
||||
- newPassword
|
||||
properties:
|
||||
currentPassword:
|
||||
type: string
|
||||
minLength: 8
|
||||
description: 현재 비밀번호
|
||||
example: "Password123!"
|
||||
newPassword:
|
||||
type: string
|
||||
minLength: 8
|
||||
maxLength: 100
|
||||
description: 새 비밀번호 (8자 이상, 영문/숫자/특수문자 포함)
|
||||
example: "NewSecurePass456!"
|
||||
|
||||
ChangePasswordResponse:
|
||||
type: object
|
||||
required:
|
||||
- success
|
||||
- message
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: 비밀번호 변경 성공 여부
|
||||
example: true
|
||||
message:
|
||||
type: string
|
||||
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"
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
required:
|
||||
- code
|
||||
- message
|
||||
- timestamp
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
description: 에러 코드
|
||||
example: USER_001
|
||||
enum:
|
||||
- USER_001 # 중복 사용자
|
||||
- USER_003 # 사용자 없음
|
||||
- USER_004 # 현재 비밀번호 불일치
|
||||
- USER_005 # 동시성 충돌
|
||||
- AUTH_001 # 인증 실패
|
||||
- AUTH_002 # 유효하지 않은 토큰
|
||||
- AUTH_003 # 권한 없음 (내부 서비스만 접근)
|
||||
- VALIDATION_ERROR # 입력 검증 오류
|
||||
message:
|
||||
type: string
|
||||
description: 에러 메시지
|
||||
example: 이미 가입된 전화번호입니다
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 에러 발생 시각
|
||||
example: 2025-10-22T10:30:00Z
|
||||
details:
|
||||
type: array
|
||||
description: 상세 에러 정보 (선택 사항)
|
||||
items:
|
||||
type: string
|
||||
example: ["필드명: 필수 항목입니다"]
|
||||
Reference in New Issue
Block a user