mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2026-06-13 04:19: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: ["필드명: 필수 항목입니다"]
|
||||
@@ -0,0 +1,869 @@
|
||||
# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 논리 아키텍처
|
||||
|
||||
## 문서 정보
|
||||
- **작성일**: 2025-10-21
|
||||
- **최종 수정일**: 2025-10-22
|
||||
- **버전**: 2.0 (CQRS + Event-Driven 전환)
|
||||
- **작성자**: System Architect
|
||||
- **관련 문서**:
|
||||
- [유저스토리](../../userstory.md)
|
||||
- [아키텍처 패턴](../../pattern/architecture-pattern.md)
|
||||
- [UI/UX 설계서](../../uiux/uiux.md)
|
||||
|
||||
## 버전 이력
|
||||
- **v1.0** (2025-10-21): 초기 마이크로서비스 아키텍처 설계
|
||||
- **v2.0** (2025-10-22): CQRS 패턴 및 Event-Driven 아키텍처 전환, Resilience 패턴 전면 적용
|
||||
- **v2.1** (2025-10-22): 서비스 구조 간소화, Kafka 통합 (Event Bus + Job Queue), Distribution 비동기 처리
|
||||
- **v2.2** (2025-10-22): Distribution Service 동기 호출 전환 (REST API 직접 호출)
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
1. [개요](#1-개요)
|
||||
2. [서비스 아키텍처](#2-서비스-아키텍처)
|
||||
3. [주요 사용자 플로우](#3-주요-사용자-플로우)
|
||||
4. [데이터 흐름 및 캐싱 전략](#4-데이터-흐름-및-캐싱-전략)
|
||||
5. [확장성 및 성능 고려사항](#5-확장성-및-성능-고려사항)
|
||||
6. [보안 고려사항](#6-보안-고려사항)
|
||||
7. [논리 아키텍처 다이어그램](#7-논리-아키텍처-다이어그램)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
### 1.1 설계 원칙
|
||||
|
||||
본 논리 아키텍처는 다음 원칙을 기반으로 설계되었습니다:
|
||||
|
||||
#### 유저스토리 기반 설계
|
||||
- 20개 유저스토리와 정확히 매칭
|
||||
- 불필요한 추가 기능 배제
|
||||
- 비즈니스 요구사항 우선 반영
|
||||
|
||||
#### Event-Driven 아키텍처
|
||||
- **Kafka 기반 통합**: Event Bus와 Job Queue를 Kafka로 통합
|
||||
- **비동기 메시징**: Kafka Topics를 통한 서비스 간 통신
|
||||
- **느슨한 결합**: 서비스 간 직접 의존성 제거
|
||||
- **확장성**: 이벤트 구독자 추가로 기능 확장 용이
|
||||
- **장애 격리**: 이벤트 발행/구독 실패 시 서비스 독립성 유지
|
||||
|
||||
#### Kafka 통합 전략
|
||||
- **Event Topics**: 도메인 이벤트 발행/구독 (EventCreated, ParticipantRegistered 등)
|
||||
- **Job Topics**: 비동기 작업 요청/처리 (ai 이벤트 생성, 이미지 생성)
|
||||
- **단일 메시징 플랫폼**: 운영 복잡도 감소 및 일관된 메시지 처리
|
||||
|
||||
#### Resilience 패턴 적용
|
||||
- **Circuit Breaker**: 외부 API 장애 시 빠른 실패 및 복구 (Hystrix/Resilience4j)
|
||||
- **Retry Pattern**: 일시적 장애 시 자동 재시도 (지수 백오프)
|
||||
- **Timeout Pattern**: 응답 시간 제한으로 리소스 점유 방지
|
||||
- **Bulkhead Pattern**: 리소스 격리로 장애 전파 차단
|
||||
- **Fallback Pattern**: 장애 시 대체 로직 실행 (캐시 응답 등)
|
||||
|
||||
### 1.2 핵심 컴포넌트 정의
|
||||
|
||||
#### Core Services
|
||||
1. **User Service**: 사용자 인증 및 매장정보 관리
|
||||
- 회원가입/로그인 (JWT 발급)
|
||||
- 프로필 CRUD
|
||||
- Event Service로 회원정보 제공
|
||||
|
||||
2. **Event Service**: 이벤트 전체 생명주기 관리
|
||||
- 이벤트 생성/수정/삭제/조회
|
||||
- 이벤트 생성 플로우 오케스트레이션
|
||||
- Kafka Job 발행 (AI, 이미지)
|
||||
- Distribution Service 동기 호출 (배포)
|
||||
- Kafka Event 발행 (EventCreated)
|
||||
|
||||
3. **Participation Service**: 참여 및 당첨자 관리
|
||||
- 참여 접수 및 중복 체크
|
||||
- 참여자 목록 조회
|
||||
- 당첨자 추첨 및 조회
|
||||
- Kafka Event 발행 (ParticipantRegistered)
|
||||
|
||||
4. **Analytics Service**: 실시간 성과 분석 및 대시보드
|
||||
- 대시보드 데이터 조회 (Redis 캐싱)
|
||||
- Kafka Event 구독 (EventCreated, ParticipantRegistered, DistributionCompleted)
|
||||
- 외부 채널 통계 수집 (Circuit Breaker + Fallback)
|
||||
- ROI 계산 및 성과 분석
|
||||
|
||||
#### Async Services (비동기 처리)
|
||||
1. **AI Service**: AI 기반 이벤트 추천
|
||||
- Kafka Job 구독 (ai 이벤트 생성)
|
||||
- 외부 AI API 호출 (Circuit Breaker, Timeout 5분)
|
||||
- 결과 Redis 저장 (TTL 24시간)
|
||||
|
||||
2. **Content Service**: SNS 이미지 생성
|
||||
- Redis에서 AI 데이터 읽기
|
||||
- 외부 이미지 생성 API 호출 (Circuit Breaker, Timeout 5분)
|
||||
- 생성된 이미지 Redis 저장 (CDN URL, TTL 7일)
|
||||
|
||||
3. **Distribution Service**: 다중 채널 배포 (동기)
|
||||
- REST API 제공 (Event Service에서 호출)
|
||||
- 병렬 배포 (Circuit Breaker, Retry, Bulkhead)
|
||||
- Kafka Event 발행 (DistributionCompleted)
|
||||
|
||||
#### Kafka (통합 메시징 플랫폼)
|
||||
**Event Topics** (도메인 이벤트):
|
||||
- **EventCreated**: 이벤트 생성 시
|
||||
- **ParticipantRegistered**: 참여자 등록 시
|
||||
- **DistributionCompleted**: 배포 완료 시
|
||||
|
||||
**Job Topics** (비동기 작업):
|
||||
- **ai 이벤트 생성**: AI 추천 작업
|
||||
- **이미지 생성**: 이미지 생성 작업
|
||||
|
||||
**특징**:
|
||||
- At-Least-Once Delivery 보장
|
||||
- Partition Key 기반 순서 보장
|
||||
- Dead Letter Queue 지원
|
||||
|
||||
#### Data Layer
|
||||
- **Redis Cache**: 세션, AI 결과, 이미지 URL, 대시보드 캐싱
|
||||
- **PostgreSQL**: 서비스별 독립 데이터베이스
|
||||
- User DB, Event DB, Participation DB, Analytics DB
|
||||
|
||||
#### External Systems
|
||||
- **AI APIs**: Claude/GPT-4 (트렌드 분석)
|
||||
- **이미지 생성 APIs**: Stable Diffusion/DALL-E
|
||||
- **배포 채널 APIs**: 우리동네TV, 링고비즈, 지니TV, SNS APIs (비동기 배포)
|
||||
|
||||
---
|
||||
|
||||
## 2. 서비스 아키텍처
|
||||
|
||||
### 2.1 서비스별 책임
|
||||
|
||||
#### User Service
|
||||
**핵심 책임**:
|
||||
- 회원가입/로그인 (JWT 토큰 발급)
|
||||
- 프로필 CRUD (매장 정보 포함)
|
||||
- 세션 관리
|
||||
- Event Service로 회원정보 제공
|
||||
|
||||
**관련 유저스토리**: UFR-USER-010, 020, 030, 040
|
||||
|
||||
**서비스 간 호출**:
|
||||
- **Event Service**: 회원정보 조회 API 제공 (매장 정보 포함)
|
||||
|
||||
**데이터 저장**:
|
||||
- User DB: users, stores 테이블
|
||||
- Redis: 세션 정보 (TTL 7일)
|
||||
|
||||
#### Event Service
|
||||
**핵심 책임**:
|
||||
- 이벤트 생성/수정/삭제/조회
|
||||
- 이벤트 생성 플로우 오케스트레이션
|
||||
- Kafka Job 발행 (AI, 이미지, 배포)
|
||||
- Kafka Event 발행 (EventCreated)
|
||||
|
||||
**관련 유저스토리**: UFR-EVENT-010, 020, 030, 040, 050, 060, 070
|
||||
|
||||
**Kafka 이벤트 발행**:
|
||||
1. **EventCreated**: 이벤트 생성 완료 시
|
||||
- Payload: eventId, storeId, title, objective, createdAt
|
||||
- 구독자: Analytics Service
|
||||
|
||||
**Kafka Job 발행**:
|
||||
1. **ai 이벤트 생성**: AI 추천 요청
|
||||
2. **이미지 생성**: 이미지 생성 요청
|
||||
|
||||
**서비스 간 호출**:
|
||||
- **Distribution Service**: 다중 채널 배포 (동기 호출, Circuit Breaker 적용)
|
||||
|
||||
**주요 플로우**:
|
||||
1. 이벤트 목적 선택 → Event DB 저장 → EventCreated 발행
|
||||
2. AI 추천 요청 → ai 이벤트 생성 발행
|
||||
3. 이미지 생성 요청 → 이미지 생성 발행
|
||||
4. 배포 승인 → Distribution Service 동기 호출
|
||||
|
||||
**데이터 저장**:
|
||||
- Event DB: events, event_objectives, event_prizes 테이블
|
||||
|
||||
#### Participation Service
|
||||
**핵심 책임**:
|
||||
- 이벤트 참여 접수 및 검증
|
||||
- 참여자 목록 조회
|
||||
- 당첨자 추첨 및 조회
|
||||
- Kafka Event 발행 (ParticipantRegistered)
|
||||
|
||||
**관련 유저스토리**: UFR-PART-010, 020, 030
|
||||
|
||||
**Kafka 이벤트 발행**:
|
||||
1. **ParticipantRegistered**: 참여자 등록 시
|
||||
- Payload: participantId, eventId, phoneNumber, registeredAt
|
||||
- 구독자: Analytics Service
|
||||
|
||||
**주요 기능**:
|
||||
- 중복 참여 체크 (전화번호 기반)
|
||||
- 참여자 목록 조회 (페이지네이션 지원)
|
||||
- 난수 기반 무작위 추첨
|
||||
- 매장 방문 고객 가산점 적용
|
||||
- 당첨자 조회
|
||||
|
||||
**데이터 저장**:
|
||||
- Participation DB: participants, winners 테이블
|
||||
|
||||
#### Analytics Service
|
||||
**핵심 책임**:
|
||||
- 실시간 성과 대시보드 조회
|
||||
- 채널별 성과 분석 및 통계
|
||||
- ROI 계산 및 성과 집계
|
||||
|
||||
**관련 유저스토리**: UFR-ANAL-010
|
||||
|
||||
**Kafka 이벤트 구독**:
|
||||
- **EventCreated**: 이벤트 기본 정보 초기화
|
||||
- **ParticipantRegistered**: 참여자 수 실시간 증가
|
||||
- **DistributionCompleted**: 배포 완료 통계 업데이트
|
||||
|
||||
**Resilience 패턴**:
|
||||
- **Circuit Breaker**: 외부 채널 API 조회 시 (실패율 50% 초과 시 Open)
|
||||
- **Fallback**: 캐시된 이전 데이터 반환
|
||||
- **Cache-Aside**: Redis 캐싱 (TTL 5분)
|
||||
|
||||
**데이터 통합**:
|
||||
- Event Service: 이벤트 정보 조회 (DB 직접 또는 REST)
|
||||
- Participation Service: 참여자/당첨자 데이터 조회
|
||||
- 외부 APIs: 우리동네TV, 지니TV, SNS 통계 수집
|
||||
|
||||
**데이터 저장**:
|
||||
- Analytics DB: event_stats, channel_stats
|
||||
- Redis: 대시보드 데이터 (TTL 5분)
|
||||
|
||||
### 2.2 Async Services (비동기 처리)
|
||||
|
||||
#### AI Service
|
||||
**핵심 책임**:
|
||||
- 업종/지역/시즌 트렌드 분석
|
||||
- 3가지 이벤트 기획안 자동 생성
|
||||
- 예상 성과 계산
|
||||
|
||||
**관련 유저스토리**: UFR-AI-010
|
||||
|
||||
**Kafka Job 구독**:
|
||||
- **ai 이벤트 생성**: AI 추천 작업 요청
|
||||
|
||||
**Resilience 패턴**:
|
||||
- **Circuit Breaker**: AI API 호출 시 (실패율 50% 초과 시 Open)
|
||||
- **Timeout**: 5분 (300초)
|
||||
- **Fallback**: 캐시된 이전 추천 결과 + 안내 메시지
|
||||
- **Cache-Aside**: Redis 캐싱 (TTL 24시간)
|
||||
|
||||
**처리 시간**:
|
||||
- 캐시 HIT: 0.1초
|
||||
- 캐시 MISS: 5분 이내 (비동기 처리)
|
||||
|
||||
**데이터 저장**:
|
||||
- Redis: AI 추천 결과 (TTL 24시간)
|
||||
- Redis: Job 상태 정보 (TTL 1시간)
|
||||
|
||||
#### Content Service
|
||||
**핵심 책임**:
|
||||
- 3가지 스타일 SNS 이미지 자동 생성
|
||||
- 플랫폼별 이미지 최적화
|
||||
- 이미지 편집 기능
|
||||
|
||||
**관련 유저스토리**: UFR-CONT-010, 020
|
||||
|
||||
**데이터 읽기**:
|
||||
- Redis에서 AI Service가 저장한 이벤트 데이터 읽기
|
||||
|
||||
**Resilience 패턴**:
|
||||
- **Circuit Breaker**: 이미지 생성 API 호출 시 (실패율 50% 초과 시 Open)
|
||||
- **Timeout**: 5분 (300초)
|
||||
- **Fallback**: 기본 템플릿 이미지 제공
|
||||
- **Cache-Aside**: Redis 캐싱 (TTL 7일)
|
||||
|
||||
**처리 시간**:
|
||||
- 캐시 HIT: 0.1초
|
||||
- 캐시 MISS: 5분 이내 (비동기 처리)
|
||||
|
||||
**데이터 저장**:
|
||||
- Redis: 이미지 생성 결과 (CDN URL, TTL 7일)
|
||||
- CDN: 생성된 이미지 파일
|
||||
|
||||
#### Distribution Service
|
||||
**핵심 책임**:
|
||||
- 다중 채널 병렬 배포 (동기)
|
||||
- 배포 상태 모니터링
|
||||
- Kafka Event 발행 (DistributionCompleted)
|
||||
|
||||
**관련 유저스토리**: UFR-DIST-010, 020
|
||||
|
||||
**주요 API**:
|
||||
- `POST /api/distribution/distribute`: 다중 채널 배포 요청 (Event Service에서 호출)
|
||||
|
||||
**Kafka 이벤트 발행**:
|
||||
- **DistributionCompleted**: 배포 완료 시
|
||||
- Payload: eventId, distributedChannels, completedAt
|
||||
- 구독자: Analytics Service
|
||||
|
||||
**Resilience 패턴**:
|
||||
- **Circuit Breaker**: 각 외부 채널 API별 독립 적용 (실패율 50% 초과 시 Open)
|
||||
- **Retry**: 최대 3회 재시도 (지수 백오프: 1초, 2초, 4초)
|
||||
- **Bulkhead**: 채널별 스레드 풀 격리 (장애 전파 방지)
|
||||
- **Fallback**: 실패 채널 스킵 + 알림
|
||||
|
||||
**처리 시간**: 1분 이내 (모든 채널 배포 완료)
|
||||
|
||||
**배포 채널**:
|
||||
- 우리동네TV API (영상 업로드)
|
||||
- 링고비즈 API (연결음 업데이트)
|
||||
- 지니TV API (TV 광고 등록)
|
||||
- SNS APIs (Instagram, Naver, Kakao 자동 포스팅)
|
||||
|
||||
**데이터 저장**:
|
||||
- Event DB: distribution_logs 테이블
|
||||
|
||||
### 2.3 Kafka 통신 전략
|
||||
|
||||
#### Kafka 아키텍처
|
||||
**기술 스택**: Apache Kafka (Event Topics + Job Topics 통합)
|
||||
**보장 수준**: At-Least-Once Delivery
|
||||
**메시지 포맷**: JSON
|
||||
|
||||
#### Event Topics (도메인 이벤트)
|
||||
|
||||
| 토픽명 | 발행자 | 구독자 | Payload | 용도 |
|
||||
|---------|--------|--------|---------|------|
|
||||
| **EventCreated** | Event Service | Analytics Service | eventId, storeId, title, objective, createdAt | 이벤트 생성 시 통계 초기화 |
|
||||
| **ParticipantRegistered** | Participation Service | Analytics Service | participantId, eventId, phoneNumber, registeredAt | 참여자 등록 시 실시간 통계 업데이트 |
|
||||
| **DistributionCompleted** | Distribution Service | Analytics Service | eventId, distributedChannels, completedAt | 배포 완료 시 통계 업데이트 |
|
||||
|
||||
#### Job Topics (비동기 작업)
|
||||
|
||||
| 토픽명 | 발행자 | 구독자 | Payload | 용도 |
|
||||
|---------|--------|--------|---------|------|
|
||||
| **ai 이벤트 생성** | Event Service | AI Service | eventId, objective, industry, region | AI 트렌드 분석 및 이벤트 추천 요청 |
|
||||
| **이미지 생성** | Event Service | Content Service | eventId, content, style | SNS 이미지 생성 요청 (3가지 스타일) |
|
||||
|
||||
#### 통신 패턴별 설계
|
||||
|
||||
**1. Event Topics (도메인 이벤트)**
|
||||
- **사용 시나리오**: 서비스 간 상태 변경 알림 및 동기화
|
||||
- **통신 방식**: Kafka Pub/Sub
|
||||
- **장점**:
|
||||
- 서비스 독립성 보장
|
||||
- 장애 격리
|
||||
- 확장 용이
|
||||
- **단점**:
|
||||
- 최종 일관성 (Eventual Consistency)
|
||||
- 디버깅 복잡도 증가
|
||||
|
||||
**2. Job Topics (비동기 작업)**
|
||||
- **사용 시나리오**: 장시간 작업 (AI 추천, 이미지 생성)
|
||||
- **통신 방식**: Kafka 메시지 큐
|
||||
- **패턴**: Asynchronous Request-Reply
|
||||
- **처리 플로우**:
|
||||
1. Event Service → Kafka Job Topic: Job 발행
|
||||
2. Async Service → Kafka: Job 수신 및 처리
|
||||
3. Client → Event Service: Job 상태 폴링 (5초 간격)
|
||||
4. Async Service → Redis: 결과 캐싱
|
||||
5. Event Service → Client: 완료 응답
|
||||
|
||||
**3. 서비스 간 동기 호출**
|
||||
- **사용 시나리오**: 다중 채널 배포 (Distribution Service)
|
||||
- **통신 방식**: REST API (HTTP/JSON)
|
||||
- **패턴**: Synchronous Request-Reply
|
||||
- **처리 플로우**:
|
||||
1. Event Service → Distribution Service: POST /api/distribution/distribute
|
||||
2. Distribution Service: 다중 채널 병렬 배포 (1분 이내)
|
||||
3. Distribution Service → Event Service: 배포 완료 응답
|
||||
4. Distribution Service → Kafka: DistributionCompleted 이벤트 발행
|
||||
- **Resilience**: Circuit Breaker 적용 (실패율 50% 초과 시 Open)
|
||||
|
||||
**4. 데이터베이스 직접 조회**
|
||||
- **사용 시나리오**: Analytics Service가 이벤트/참여 데이터 필요 시
|
||||
- **패턴**: Database-per-Service 원칙 유지, 필요 시 이벤트로 데이터 동기화
|
||||
- **통신 방식**: Kafka 이벤트 구독 → Analytics DB 저장 → 로컬 조회
|
||||
- **특징**: 서비스 간 직접 API 호출 최소화
|
||||
|
||||
#### Cache-Aside 전략
|
||||
|
||||
| 서비스 | 캐시 키 패턴 | TTL | 히트율 목표 | 효과 |
|
||||
|--------|-------------|-----|-----------|------|
|
||||
| AI Service | `ai:recommendation:{업종}:{지역}:{목적}` | 24시간 | 80% | 10초 → 0.1초 (99% 개선) |
|
||||
| Content Service | `content:image:{이벤트ID}:{스타일}` | 7일 | 80% | 5초 → 0.1초 (98% 개선) |
|
||||
| User Service | `user:business:{사업자번호}` | 7일 | 90% | - |
|
||||
| Analytics Query | `analytics:dashboard:{이벤트ID}` | 5분 | 95% | 3초 → 0.5초 (83% 개선) |
|
||||
|
||||
#### Resilience 패턴 적용
|
||||
|
||||
**1. Circuit Breaker 패턴**
|
||||
- **적용 대상**: 모든 외부 API 호출
|
||||
- **라이브러리**: Resilience4j 또는 Hystrix
|
||||
- **설정**:
|
||||
```yaml
|
||||
circuit-breaker:
|
||||
failure-rate-threshold: 50% # 실패율 50% 초과 시 Open
|
||||
slow-call-rate-threshold: 50% # 느린 호출 50% 초과 시 Open
|
||||
slow-call-duration-threshold: 5s # 5초 초과 시 느린 호출로 간주
|
||||
wait-duration-in-open-state: 30s # Open 상태 30초 유지 후 Half-Open
|
||||
permitted-calls-in-half-open: 3 # Half-Open 상태에서 3개 요청 테스트
|
||||
```
|
||||
|
||||
**2. Retry 패턴**
|
||||
- **적용 대상**: 일시적 장애가 예상되는 외부 API
|
||||
- **재시도 전략**: 지수 백오프 (Exponential Backoff)
|
||||
- **설정**:
|
||||
```yaml
|
||||
retry:
|
||||
max-attempts: 3 # 최대 3회 재시도
|
||||
wait-duration: 1s # 초기 대기 시간 1초
|
||||
exponential-backoff-multiplier: 2 # 2배씩 증가 (1초, 2초, 4초)
|
||||
retry-exceptions:
|
||||
- java.net.SocketTimeoutException
|
||||
- java.net.ConnectException
|
||||
```
|
||||
|
||||
**3. Timeout 패턴**
|
||||
- **적용 대상**: 모든 외부 API 호출
|
||||
- **설정**:
|
||||
| 서비스 | Timeout | 이유 |
|
||||
|--------|---------|------|
|
||||
| AI Service (AI API) | 5분 (300초) | 복잡한 분석 작업 |
|
||||
| Content Service (이미지 API) | 5분 (300초) | 이미지 생성 시간 고려 |
|
||||
| Distribution Service (채널 APIs) | 10초 | 빠른 배포 필요 |
|
||||
|
||||
**4. Bulkhead 패턴**
|
||||
- **적용 대상**: Distribution Service (다중 채널 배포)
|
||||
- **목적**: 채널별 리소스 격리로 장애 전파 차단
|
||||
- **설정**:
|
||||
```yaml
|
||||
bulkhead:
|
||||
max-concurrent-calls: 10 # 채널당 최대 10개 동시 호출
|
||||
max-wait-duration: 0s # 대기 없이 즉시 실패
|
||||
```
|
||||
|
||||
**5. Fallback 패턴**
|
||||
- **적용 대상**: 모든 외부 API 호출
|
||||
- **전략**:
|
||||
| 서비스 | Fallback 전략 |
|
||||
|--------|---------------|
|
||||
| AI Service | 캐시된 이전 추천 결과 + 안내 메시지 |
|
||||
| Content Service | 기본 템플릿 이미지 제공 |
|
||||
| Distribution Service | 실패 채널 스킵 + 알림 |
|
||||
| Analytics Service | 캐시된 이전 데이터 반환 |
|
||||
|
||||
#### 이벤트 순서 보장
|
||||
- **Kafka Partition Key**: eventId 기준으로 파티션 할당
|
||||
- **동일 이벤트의 모든 이벤트**: 동일 파티션 → 순서 보장
|
||||
- **다른 이벤트**: 독립적 처리 → 병렬 처리 가능
|
||||
|
||||
#### 이벤트 재처리 (At-Least-Once)
|
||||
- **멱등성 보장**: 구독자는 동일 이벤트 중복 처리 시 멱등성 유지
|
||||
- **방법**: 이벤트 ID 기반 중복 체크 (Redis Set 사용)
|
||||
|
||||
---
|
||||
|
||||
## 3. 주요 사용자 플로우
|
||||
|
||||
### 3.1 이벤트 생성 플로우 (Event-Driven + Kafka)
|
||||
|
||||
```
|
||||
1. [이벤트 목적 선택]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client → Event Service │
|
||||
│ - POST /api/events (목적, 매장 정보) │
|
||||
│ - Event DB에 저장 │
|
||||
│ - EventCreated 이벤트 발행 → Kafka │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Kafka → Analytics Service │
|
||||
│ - EventCreated 이벤트 구독 │
|
||||
│ - Analytics DB에 기본 통계 초기화 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
2. [AI 이벤트 추천]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client → Event Service │
|
||||
│ - POST /api/events/{id}/ai-recommendations │
|
||||
│ - Kafka ai 이벤트 생성 토픽 발행 (AI 작업 요청) │
|
||||
│ - Job ID 즉시 반환 (0.1초) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AI Service (Background) │
|
||||
│ - Kafka ai 이벤트 생성 토픽 구독 │
|
||||
│ - Redis 캐시 확인 (Cache-Aside) │
|
||||
│ - 캐시 MISS: Claude API 호출 (5분) [Circuit Breaker] │
|
||||
│ - AI 추천 결과를 Redis에 저장 (TTL 24시간) │
|
||||
│ - Job 상태 완료로 업데이트 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client (Polling) │
|
||||
│ - GET /api/jobs/{id} (5초 간격) │
|
||||
│ - 완료 시: AI 추천 결과 반환 (3가지 옵션) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
3. [SNS 이미지 생성]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Content Service (Background) │
|
||||
│ - Redis에서 AI Service가 저장한 이벤트 데이터 읽기 │
|
||||
│ - Redis 캐시 확인 (이미지 생성 여부) │
|
||||
│ - 캐시 MISS: Stable Diffusion API (5분) [Circuit Breaker] │
|
||||
│ - 이미지 CDN 업로드 │
|
||||
│ - 생성된 이미지 URL을 Redis에 저장 (TTL 7일) │
|
||||
│ - Job 상태 완료로 업데이트 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client (Polling) │
|
||||
│ - GET /api/jobs/{id} (3초 간격) │
|
||||
│ - 완료 시: 3가지 스타일 이미지 URL 반환 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
4. [최종 승인 및 배포]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client → Event Service │
|
||||
│ - POST /api/events/{id}/publish │
|
||||
│ - Redis의 이벤트 관련 정보(AI 추천, 이미지 URL)를 조회 │
|
||||
│ - Event DB에 이벤트 정보 저장 │
|
||||
│ - Event 상태 변경 (DRAFT → PUBLISHED) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Event Service → Distribution Service │
|
||||
│ - POST /api/distribution/distribute (동기 호출) │
|
||||
│ - 다중 채널 병렬 배포 [Circuit Breaker + Bulkhead] │
|
||||
│ * 우리동네TV API (영상 업로드) [Retry: 3회] │
|
||||
│ * 링고비즈 API (연결음 업데이트) [Retry: 3회] │
|
||||
│ * 지니TV API (광고 등록) [Retry: 3회] │
|
||||
│ * SNS APIs (Instagram, Naver, Kakao) [Retry: 3회] │
|
||||
│ - 배포 완료: DistributionCompleted 이벤트 발행 → Kafka │
|
||||
│ - Event Service로 배포 완료 응답 (1분 이내) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Kafka → Analytics Service │
|
||||
│ - DistributionCompleted 이벤트 구독 │
|
||||
│ - Analytics DB 배포 통계 업데이트 │
|
||||
│ - 대시보드 캐시 무효화 (다음 조회 시 갱신) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Event Service → Client │
|
||||
│ - 배포 완료 응답 반환 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 고객 참여 플로우 (Event-Driven)
|
||||
|
||||
```
|
||||
1. [이벤트 참여]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client → Participation Service │
|
||||
│ - POST /api/events/{id}/participate │
|
||||
│ - 중복 참여 체크 (전화번호 기반) │
|
||||
│ - Participation DB에 저장 │
|
||||
│ - ParticipantRegistered 이벤트 발행 → Kafka │
|
||||
│ - 응모 번호 즉시 반환 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Kafka → Analytics Service │
|
||||
│ - ParticipantRegistered 이벤트 구독 │
|
||||
│ - 실시간 참여자 수 증가 │
|
||||
│ - Analytics DB에 참여 통계 업데이트 │
|
||||
│ - 대시보드 캐시 무효화 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
2. [당첨자 추첨]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client → Participation Service │
|
||||
│ - POST /api/events/{id}/draw-winners │
|
||||
│ - 난수 기반 무작위 추첨 │
|
||||
│ - Winners DB에 저장 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 성과 분석 플로우 (Event-Driven)
|
||||
|
||||
```
|
||||
1. [실시간 대시보드 조회]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Client → Analytics Service │
|
||||
│ - GET /api/events/{id}/analytics │
|
||||
│ - Redis 캐시 확인 (TTL 5분) │
|
||||
│ * 캐시 HIT: 즉시 반환 (0.5초) │
|
||||
│ * 캐시 MISS: 아래 데이터 통합 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Analytics Service (데이터 통합) │
|
||||
│ - Analytics DB: 이벤트/참여 통계 조회 (로컬 DB) │
|
||||
│ - 외부 APIs: 채널별 노출/클릭 수 [Circuit Breaker + Fallback] │
|
||||
│ * 우리동네TV API (조회수) │
|
||||
│ * 지니TV API (광고 노출 수) │
|
||||
│ * SNS APIs (좋아요, 댓글, 공유 수) │
|
||||
│ - Redis 캐싱 (TTL 5분) │
|
||||
│ - 대시보드 데이터 반환 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
2. [실시간 업데이트 (Event 구독)]
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Analytics Service (Background) │
|
||||
│ - EventCreated 구독: 이벤트 기본 정보 초기화 │
|
||||
│ - ParticipantRegistered 구독: 참여자 수 실시간 증가 │
|
||||
│ - DistributionCompleted 구독: 배포 채널 통계 업데이트 │
|
||||
│ - 캐시 무효화: 다음 조회 시 최신 데이터 갱신 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.4 플로우 특징
|
||||
|
||||
#### Kafka 통합 이점
|
||||
- **단일 메시징 플랫폼**: Event Bus와 Job Queue를 Kafka로 통합, 운영 복잡도 감소
|
||||
- **일관된 메시지 처리**: 모든 비동기 통신이 Kafka를 통해 이루어져 모니터링 및 디버깅 용이
|
||||
- **확장성**: Kafka의 높은 처리량으로 대규모 이벤트 처리 지원
|
||||
|
||||
#### Event-Driven 이점
|
||||
- **느슨한 결합**: 서비스 간 직접 의존성 제거
|
||||
- **장애 격리**: 한 서비스 장애가 다른 서비스에 영향 없음
|
||||
- **확장 용이**: 새로운 구독자 추가로 기능 확장
|
||||
- **비동기 처리**: 사용자 응답 시간 단축
|
||||
|
||||
#### Resilience 이점
|
||||
- **Circuit Breaker**: 외부 API 장애 시 빠른 실패 및 복구
|
||||
- **Retry**: 일시적 장애 자동 복구
|
||||
- **Fallback**: 장애 시에도 서비스 지속 (Graceful Degradation)
|
||||
- **Bulkhead**: 리소스 격리로 장애 전파 차단
|
||||
|
||||
---
|
||||
|
||||
## 4. 데이터 흐름 및 캐싱 전략
|
||||
|
||||
### 4.1 데이터 흐름
|
||||
|
||||
#### 읽기 플로우 (Cache-Aside 패턴)
|
||||
```
|
||||
1. Application → Cache 확인
|
||||
- Cache HIT: 캐시된 데이터 즉시 반환
|
||||
- Cache MISS:
|
||||
2. Application → Database/External API 조회
|
||||
3. Database/External API → Application 데이터 반환
|
||||
4. Application → Cache 데이터 저장 (TTL 설정)
|
||||
5. Application → Client 데이터 반환
|
||||
```
|
||||
|
||||
#### 쓰기 플로우 (Write-Through 패턴)
|
||||
```
|
||||
1. Application → Database 쓰기
|
||||
2. Database → Application 성공 응답
|
||||
3. Application → Cache 무효화 또는 업데이트
|
||||
4. Application → Client 성공 응답
|
||||
```
|
||||
|
||||
### 4.2 캐싱 전략
|
||||
|
||||
#### Redis 캐시 구조
|
||||
|
||||
| 서비스 | 캐시 키 패턴 | 데이터 타입 | TTL | 예상 크기 | 히트율 목표 |
|
||||
|--------|-------------|-----------|-----|----------|-----------|
|
||||
| User | `user:session:{token}` | String | 7일 | 1KB | - |
|
||||
| AI | `ai:recommendation:{업종}:{지역}:{목적}` | Hash | 24시간 | 10KB | 80% |
|
||||
| AI | `ai:event:{이벤트ID}` | Hash | 24시간 | 10KB | - |
|
||||
| Content | `content:image:{이벤트ID}:{스타일}` | String | 7일 | 0.2KB (URL) | 80% |
|
||||
| Analytics | `analytics:dashboard:{이벤트ID}` | Hash | 5분 | 5KB | 95% |
|
||||
| AI | `job:{jobId}` | Hash | 1시간 | 1KB | - |
|
||||
| Content | `job:{jobId}` | Hash | 1시간 | 1KB | - |
|
||||
|
||||
#### Redis 메모리 산정
|
||||
- **예상 동시 사용자**: 100명
|
||||
- **예상 이벤트 수**: 50개
|
||||
- **예상 캐시 항목 수**: 10,000개
|
||||
- **예상 총 메모리**: 약 50MB (운영 환경 2GB 할당)
|
||||
|
||||
#### 캐시 무효화 전략
|
||||
- **TTL 기반 자동 만료**: 대부분의 캐시
|
||||
- **수동 무효화**: 이벤트 수정/삭제 시 관련 캐시 삭제
|
||||
- **Lazy 무효화**: 데이터 변경 시 다음 조회 시점에 갱신
|
||||
|
||||
### 4.3 데이터베이스 전략
|
||||
|
||||
#### 서비스별 독립 데이터베이스
|
||||
- **User DB**: users, stores
|
||||
- **Event DB**: events, event_objectives, event_prizes, distribution_logs
|
||||
- **Participation DB**: participants, winners
|
||||
- **Analytics DB**: event_stats, channel_stats
|
||||
|
||||
#### 데이터 일관성 전략
|
||||
- **Eventual Consistency**: 서비스 간 데이터는 최종 일관성 보장
|
||||
- **Strong Consistency**: 서비스 내부 트랜잭션은 강한 일관성 보장
|
||||
- **Saga 패턴**: 이벤트 생성 플로우 (보상 트랜잭션)
|
||||
|
||||
---
|
||||
|
||||
## 5. 확장성 및 성능 고려사항
|
||||
|
||||
### 5.1 수평 확장 전략
|
||||
|
||||
#### 서비스별 확장 전략
|
||||
| 서비스 | 초기 인스턴스 | 확장 조건 | 최대 인스턴스 | Auto-scaling 메트릭 |
|
||||
|--------|-------------|----------|-------------|-------------------|
|
||||
| User | 2 | CPU > 70% | 5 | CPU, 메모리 |
|
||||
| Event | 2 | CPU > 70% | 10 | CPU, 메모리 |
|
||||
| AI | 1 | Job Queue > 10 | 3 | Queue 길이 |
|
||||
| Content | 1 | Job Queue > 10 | 3 | Queue 길이 |
|
||||
| Distribution | 2 | CPU > 70% | 5 | CPU, 메모리 |
|
||||
| Participation | 1 | CPU > 70% | 3 | CPU, 메모리 |
|
||||
| Analytics | 1 | CPU > 70% | 3 | CPU, 메모리 |
|
||||
|
||||
#### Redis Cluster
|
||||
- **초기 구성**: 3 노드 (Master 3, Replica 3)
|
||||
- **확장**: 노드 추가를 통한 수평 확장
|
||||
- **HA**: Redis Sentinel을 통한 자동 Failover
|
||||
|
||||
#### Database Replication
|
||||
- **Primary-Replica 구조**: 읽기 부하 분산
|
||||
- **읽기 확장**: Read Replica 추가 (필요 시)
|
||||
- **쓰기 확장**: Sharding (Phase 2 이후)
|
||||
|
||||
### 5.2 성능 목표
|
||||
|
||||
#### 응답 시간 목표
|
||||
| 기능 | 목표 시간 | 캐시 HIT | 캐시 MISS |
|
||||
|------|----------|---------|----------|
|
||||
| 로그인 | 0.5초 | - | - |
|
||||
| 이벤트 목록 조회 | 0.3초 | - | - |
|
||||
| AI 트렌드 분석 + 추천 | 0.1초 | ✅ | 10초 (비동기) |
|
||||
| SNS 이미지 생성 | 0.1초 | ✅ | 5초 (비동기) |
|
||||
| 다중 채널 배포 | 1분 | - | - |
|
||||
| 대시보드 로딩 | 0.5초 | ✅ | 3초 |
|
||||
|
||||
#### 처리량 목표
|
||||
- **동시 사용자**: 100명 (MVP 목표)
|
||||
- **API 요청**: 1,000 req/min
|
||||
- **AI 작업**: 10 jobs/min
|
||||
- **이미지 생성**: 10 jobs/min
|
||||
|
||||
### 5.3 성능 최적화 기법
|
||||
|
||||
#### Frontend 최적화
|
||||
- **Code Splitting**: 페이지별 번들 분할
|
||||
- **Lazy Loading**: 차트 라이브러리 지연 로딩
|
||||
- **CDN**: 정적 자산 CDN 배포
|
||||
- **Compression**: Gzip/Brotli 압축
|
||||
|
||||
#### Backend 최적화
|
||||
- **Connection Pooling**: 데이터베이스 연결 풀 관리
|
||||
- **Query Optimization**: 인덱스 최적화, N+1 쿼리 방지
|
||||
- **Batch Processing**: 대량 데이터 일괄 처리
|
||||
- **Pagination**: 목록 조회 페이지네이션
|
||||
|
||||
#### Cache 최적화
|
||||
- **Multi-Level Caching**: Browser Cache → CDN → Redis → Database
|
||||
- **Cache Warming**: 자주 사용되는 데이터 사전 로딩
|
||||
- **Cache Preloading**: 피크 시간 전 캐시 준비
|
||||
|
||||
---
|
||||
|
||||
## 6. 보안 고려사항
|
||||
|
||||
### 6.1 인증 및 인가
|
||||
|
||||
#### JWT 기반 인증
|
||||
- **토큰 발급**: User Service에서 로그인 시 JWT 토큰 발급
|
||||
- **토큰 검증**: API Gateway에서 모든 요청의 JWT 토큰 검증
|
||||
- **토큰 저장**: Redis에 세션 정보 저장 (TTL 7일)
|
||||
- **토큰 갱신**: Refresh Token 패턴 (선택)
|
||||
|
||||
#### 역할 기반 접근 제어 (RBAC)
|
||||
- **역할**: OWNER (매장 사장님), CUSTOMER (이벤트 참여자)
|
||||
- **권한 관리**: API별 필요 역할 정의
|
||||
- **API Gateway 검증**: 요청자의 역할 확인
|
||||
|
||||
### 6.2 데이터 보안
|
||||
|
||||
#### 민감 정보 암호화
|
||||
- **비밀번호**: bcrypt 해싱 (Cost Factor: 10)
|
||||
- **사업자번호**: AES-256 암호화 저장
|
||||
- **개인정보**: 전화번호 마스킹 (010-****-1234)
|
||||
|
||||
#### 전송 보안
|
||||
- **HTTPS**: 모든 통신 TLS 1.3 암호화
|
||||
- **API Key**: 외부 API 호출 시 안전한 Key 관리 (AWS Secrets Manager)
|
||||
|
||||
#### 데이터 접근 통제
|
||||
- **Database**: 서비스별 독립 계정, 최소 권한 원칙
|
||||
- **Redis**: 비밀번호 설정, ACL 적용
|
||||
- **백업**: 암호화된 백업 저장
|
||||
|
||||
### 6.3 보안 모니터링
|
||||
|
||||
#### 위협 탐지
|
||||
- **Rate Limiting**: API Gateway에서 사용자당 100 req/min
|
||||
- **Brute Force 방지**: 로그인 5회 실패 시 계정 잠금 (삭제됨, 향후 추가 가능)
|
||||
- **SQL Injection 방지**: Prepared Statement 사용
|
||||
- **XSS 방지**: 입력 데이터 Sanitization
|
||||
|
||||
#### 로깅 및 감사
|
||||
- **Access Log**: 모든 API 요청 로깅
|
||||
- **Audit Log**: 민감 작업 (로그인, 이벤트 생성, 당첨자 추첨) 감사 로그
|
||||
- **중앙집중식 로깅**: ELK Stack 또는 CloudWatch Logs
|
||||
|
||||
---
|
||||
|
||||
## 7. 논리 아키텍처 다이어그램
|
||||
|
||||
논리 아키텍처 다이어그램은 별도 Mermaid 파일로 작성되었습니다.
|
||||
|
||||
**파일 위치**: `logical-architecture.mmd`
|
||||
|
||||
**다이어그램 확인 방법**:
|
||||
1. https://mermaid.live/edit 접속
|
||||
2. `logical-architecture.mmd` 파일 내용 붙여넣기
|
||||
3. 다이어그램 시각적 확인
|
||||
|
||||
**다이어그램 구성**:
|
||||
- Services: 4개 핵심 서비스 (User, Event, Participation, Analytics)
|
||||
- Async Services: 3개 비동기 서비스 (AI, Content, Distribution)
|
||||
- Kafka: Event Topics + Job Topics 통합 메시징 플랫폼
|
||||
- External System: 통합된 외부 시스템 (국세청 API, AI API, 이미지 생성 API, 배포 채널 APIs)
|
||||
|
||||
**의존성 표현**:
|
||||
- 굵은 화살표 (==>): Kafka Event Topics 발행
|
||||
- 실선 화살표 (-->): Kafka Job Topics 발행 또는 외부 시스템 호출
|
||||
- 점선 화살표 (-.->): Kafka 구독
|
||||
|
||||
---
|
||||
|
||||
## 부록
|
||||
|
||||
### A. 참고 문서
|
||||
- [유저스토리](../../userstory.md)
|
||||
- [아키텍처 패턴](../../pattern/architecture-pattern.md)
|
||||
- [UI/UX 설계서](../../uiux/uiux.md)
|
||||
- [클라우드 디자인 패턴](../../../claude/cloud-design-patterns.md)
|
||||
|
||||
### B. 주요 결정사항
|
||||
1. **Kafka 통합 메시징 플랫폼 채택**: Event Bus와 Job Queue를 Kafka로 통합하여 운영 복잡도 감소
|
||||
2. **Event-Driven 아키텍처 채택**: Kafka를 통한 서비스 간 느슨한 결합 및 비동기 통신
|
||||
3. **도메인 이벤트 정의**: 3개 Event Topics (EventCreated, ParticipantRegistered, DistributionCompleted)
|
||||
4. **Job Topics 정의**: 2개 Job Topics (ai 이벤트 생성, 이미지 생성)로 장시간 비동기 작업 처리
|
||||
5. **Resilience 패턴 전면 적용**: Circuit Breaker, Retry, Timeout, Bulkhead, Fallback
|
||||
6. **At-Least-Once Delivery**: Kafka 메시지 보장 및 멱등성 설계
|
||||
7. **Cache-Aside 패턴**: AI/이미지 생성 결과 캐싱으로 응답 시간 90% 개선
|
||||
8. **Redis 기반 서비스 간 데이터 공유**: AI Service → Redis → Content Service 데이터 흐름
|
||||
9. **Redis to DB 영구 저장**: 이벤트 생성 완료 시 Redis 데이터를 Event DB에 저장
|
||||
10. **동기 배포**: Event Service가 Distribution Service를 REST API로 직접 호출하여 다중 채널 배포 동기 처리
|
||||
11. **서비스별 독립 Database**: Database-per-Service 패턴으로 서비스 독립성 보장
|
||||
12. **장시간 작업 Timeout 조정**: AI/Content Service Timeout을 5분으로 설정하여 복잡한 생성 작업 지원
|
||||
|
||||
### C. 향후 개선 방안 (Phase 2 이후)
|
||||
1. **Event Sourcing 완전 적용**: 모든 상태 변경을 이벤트로 저장하여 시간 여행 및 감사 추적 강화
|
||||
2. **Saga 패턴 적용**: 복잡한 분산 트랜잭션 보상 로직 체계화
|
||||
3. **Service Mesh 도입**: Istio를 통한 서비스 간 통신 관찰성 및 보안 강화
|
||||
4. **Database Sharding**: Event/Participation Write DB 샤딩으로 쓰기 확장성 개선
|
||||
5. **WebSocket 기반 실시간 푸시**: 대시보드 실시간 업데이트 (폴링 대체)
|
||||
6. **GraphQL API Gateway**: 클라이언트 맞춤형 데이터 조회 최적화
|
||||
7. **Dead Letter Queue 고도화**: 실패 이벤트 재처리 및 알림 자동화
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 2.2
|
||||
**최종 수정일**: 2025-10-22
|
||||
**작성자**: System Architect
|
||||
**변경 사항**: Distribution Service 동기 호출 전환 (REST API 직접 호출)
|
||||
@@ -0,0 +1,71 @@
|
||||
graph TB
|
||||
%% KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 논리 아키텍처 (Event-Driven + Kafka)
|
||||
|
||||
%% Services
|
||||
subgraph "Services"
|
||||
UserSvc["User Service<br/>• 회원가입/로그인<br/>• 프로필 관리<br/>• 회원정보 제공"]
|
||||
EventSvc["Event Service<br/>• 이벤트 생성/수정/삭제<br/>• 플로우 오케스트레이션<br/>• AI 작업 요청<br/>• 배포 작업 요청<br/>• Redis → DB 저장"]
|
||||
PartSvc["Participation<br/>Service<br/>• 참여 접수<br/>• 참여자 목록<br/>• 당첨자 추첨"]
|
||||
AnalSvc["Analytics Service<br/>• 실시간 대시보드<br/>• 성과 분석<br/>• 채널별 통계<br/>[Circuit Breaker]"]
|
||||
end
|
||||
|
||||
%% Async Services
|
||||
subgraph "Async Services"
|
||||
AISvc["AI Service<br/>• 트렌드 분석<br/>• 이벤트 추천<br/>• Redis 저장<br/>[Circuit Breaker]<br/>[Timeout: 5분]"]
|
||||
ContentSvc["Content Service<br/>• Redis 데이터 읽기<br/>• SNS 이미지 생성<br/>• Redis 저장<br/>[Circuit Breaker]<br/>[Timeout: 5분]"]
|
||||
DistSvc["Distribution<br/>Service<br/>• 다중 채널 배포<br/>[Circuit Breaker]<br/>[Retry: 3회]<br/>[Bulkhead]"]
|
||||
end
|
||||
|
||||
%% Kafka (Event Bus + Job Queue)
|
||||
Kafka["Kafka<br/>━━━━━━━━━━<br/><Event Topics><br/>• EventCreated<br/>• ParticipantRegistered<br/>• DistributionCompleted<br/>━━━━━━━━━━<br/><Job Topics><br/>• ai 이벤트 생성"]
|
||||
|
||||
%% External System
|
||||
External["외부시스템<br/>[Circuit Breaker]<br/>━━━━━━━━━━<br/>• AI API<br/>• 이미지 생성 API<br/>• 배포 채널 APIs<br/>(비동기)"]
|
||||
|
||||
%% Redis
|
||||
Redis["Redis Cache<br/>━━━━━━━━━━<br/>• AI 결과<br/>• 이미지 URL<br/>• 이벤트 데이터"]
|
||||
|
||||
%% Event Publishing
|
||||
EventSvc ==>|"EventCreated<br/>발행"| Kafka
|
||||
PartSvc ==>|"ParticipantRegistered<br/>발행"| Kafka
|
||||
DistSvc ==>|"DistributionCompleted<br/>발행"| Kafka
|
||||
|
||||
%% Job Publishing (비동기 작업 요청)
|
||||
EventSvc -->|"ai 이벤트 생성 발행"| Kafka
|
||||
|
||||
%% Event Subscription
|
||||
Kafka -.->|"EventCreated<br/>구독"| AnalSvc
|
||||
Kafka -.->|"ParticipantRegistered<br/>구독"| AnalSvc
|
||||
Kafka -.->|"DistributionCompleted<br/>구독"| AnalSvc
|
||||
|
||||
%% Job Subscription
|
||||
Kafka -.->|"ai 이벤트 생성 구독"| AISvc
|
||||
|
||||
%% Service to Service (동기 호출)
|
||||
EventSvc -->|"다중 채널 배포<br/>[Circuit Breaker]"| DistSvc
|
||||
EventSvc -->|"회원정보 조회"| UserSvc
|
||||
|
||||
%% Redis Interactions
|
||||
AISvc -->|"AI 결과 저장"| Redis
|
||||
ContentSvc -->|"AI 데이터 읽기"| Redis
|
||||
ContentSvc -->|"이미지 URL 저장"| Redis
|
||||
EventSvc -->|"Redis → DB 저장"| Redis
|
||||
|
||||
%% Services to External (Resilience 패턴)
|
||||
AISvc -->|"트렌드 분석/추천"| External
|
||||
ContentSvc -->|"이미지 생성"| External
|
||||
DistSvc -->|"다중 채널 배포<br/>(비동기)"| External
|
||||
AnalSvc -->|"채널별 통계<br/>[Fallback: Cache]"| External
|
||||
|
||||
%% Styling
|
||||
classDef service fill:#4ECDC4,stroke:#14B8A6,stroke-width:3px
|
||||
classDef async fill:#8B5CF6,stroke:#7C3AED,stroke-width:3px,color:#fff
|
||||
classDef kafka fill:#F59E0B,stroke:#D97706,stroke-width:3px
|
||||
classDef external fill:#E5E7EB,stroke:#9CA3AF,stroke-width:2px
|
||||
classDef cache fill:#EF4444,stroke:#DC2626,stroke-width:3px
|
||||
|
||||
class UserSvc,EventSvc,PartSvc,AnalSvc service
|
||||
class AISvc,ContentSvc,DistSvc async
|
||||
class Kafka kafka
|
||||
class External external
|
||||
class Redis cache
|
||||
@@ -0,0 +1,393 @@
|
||||
# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 내부 시퀀스 설계서
|
||||
|
||||
## 문서 정보
|
||||
- **작성일**: 2025-10-22
|
||||
- **버전**: 1.0
|
||||
- **작성자**: System Architect
|
||||
- **관련 문서**:
|
||||
- [유저스토리](../../userstory.md)
|
||||
- [외부 시퀀스 설계서](../outer/)
|
||||
- [논리 아키텍처](../../logical/logical-architecture.md)
|
||||
|
||||
---
|
||||
|
||||
## 목차
|
||||
1. [개요](#1-개요)
|
||||
2. [서비스별 시나리오 목록](#2-서비스별-시나리오-목록)
|
||||
3. [설계 원칙](#3-설계-원칙)
|
||||
4. [주요 패턴](#4-주요-패턴)
|
||||
5. [파일 구조](#5-파일-구조)
|
||||
6. [PlantUML 다이어그램 확인 방법](#6-plantuml-다이어그램-확인-방법)
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
본 문서는 KT AI 기반 소상공인 이벤트 자동 생성 서비스의 **7개 마이크로서비스**에 대한 **26개 내부 시퀀스 다이어그램**을 포함합니다.
|
||||
|
||||
### 1.1 설계 범위
|
||||
|
||||
각 마이크로서비스 내부의 처리 흐름을 상세히 표현:
|
||||
- **API 레이어**: Controller
|
||||
- **비즈니스 레이어**: Service, Validator, Domain Logic
|
||||
- **데이터 레이어**: Repository, Cache Manager
|
||||
- **인프라 레이어**: Kafka, Redis, Database, External APIs
|
||||
|
||||
### 1.2 설계 대상 서비스
|
||||
|
||||
| 서비스 | 시나리오 수 | 주요 책임 |
|
||||
|--------|------------|----------|
|
||||
| **User** | 4 | 사용자 인증, 프로필 관리 |
|
||||
| **Event** | 10 | 이벤트 생명주기 관리, 오케스트레이션 |
|
||||
| **Participation** | 3 | 참여자 관리, 당첨자 추첨 |
|
||||
| **Analytics** | 5 | 실시간 성과 분석, 대시보드 |
|
||||
| **AI** | 1 | AI 트렌드 분석 및 이벤트 추천 |
|
||||
| **Content** | 1 | SNS 이미지 생성 |
|
||||
| **Distribution** | 2 | 다중 채널 배포 |
|
||||
| **총계** | **26** | - |
|
||||
|
||||
---
|
||||
|
||||
## 2. 서비스별 시나리오 목록
|
||||
|
||||
### 2.1 User 서비스 (4개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 회원가입 | `user-회원가입.puml` | UFR-USER-010 | 사업자번호 검증(Circuit Breaker), 트랜잭션, JWT 발급 |
|
||||
| 로그인 | `user-로그인.puml` | UFR-USER-020 | 비밀번호 검증(bcrypt), JWT 발급, 세션 저장 |
|
||||
| 프로필수정 | `user-프로필수정.puml` | UFR-USER-030 | 기본/매장 정보 수정, 비밀번호 변경, 트랜잭션 |
|
||||
| 로그아웃 | `user-로그아웃.puml` | UFR-USER-040 | JWT 검증, 세션 삭제, Blacklist 추가 |
|
||||
|
||||
**주요 특징**:
|
||||
- **Resilience 패턴**: Circuit Breaker (국세청 API), Retry, Timeout, Fallback
|
||||
- **보안**: bcrypt 해싱, AES-256 암호화, JWT 관리
|
||||
- **캐싱**: 사업자번호 검증 결과 (TTL 7일), 세션 정보 (TTL 7일)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Event 서비스 (10개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 목적선택 | `event-목적선택.puml` | UFR-EVENT-020 | 이벤트 목적 선택 및 저장, EventCreated 발행 |
|
||||
| AI추천요청 | `event-AI추천요청.puml` | UFR-EVENT-030 | Kafka ai-job 발행, Job ID 반환 (202 Accepted) |
|
||||
| 추천결과조회 | `event-추천결과조회.puml` | UFR-EVENT-030 | Redis Job 상태 폴링 조회 |
|
||||
| 이미지생성요청 | `event-이미지생성요청.puml` | UFR-CONT-010 | Kafka image-job 발행, Job ID 반환 (202 Accepted) |
|
||||
| 이미지결과조회 | `event-이미지결과조회.puml` | UFR-CONT-010 | Redis Job 상태 폴링 조회 |
|
||||
| 콘텐츠선택 | `event-콘텐츠선택.puml` | UFR-CONT-020 | 선택한 콘텐츠 저장 |
|
||||
| 최종승인및배포 | `event-최종승인및배포.puml` | UFR-EVENT-050 | Distribution Service 동기 호출, 상태 변경 |
|
||||
| 상세조회 | `event-상세조회.puml` | UFR-EVENT-060 | 이벤트 상세 조회 (캐싱) |
|
||||
| 목록조회 | `event-목록조회.puml` | UFR-EVENT-070 | 이벤트 목록 조회 (필터/검색/페이지네이션) |
|
||||
| 대시보드조회 | `event-대시보드조회.puml` | UFR-EVENT-010 | 대시보드 이벤트 목록 (병렬 쿼리) |
|
||||
|
||||
**주요 특징**:
|
||||
- **Kafka 통합**: Event Topics (EventCreated), Job Topics (ai-job, image-job)
|
||||
- **비동기 처리**: Job 발행 → 폴링 방식 결과 조회
|
||||
- **동기 호출**: Distribution Service REST API 직접 호출
|
||||
- **캐싱 전략**: 목적(30분), 상세(5분), 목록/대시보드(1분)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Participation 서비스 (3개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 이벤트참여 | `participation-이벤트참여.puml` | UFR-PART-010 | 중복 체크, ParticipantRegistered 발행 |
|
||||
| 참여자목록조회 | `participation-참여자목록조회.puml` | UFR-PART-020 | 필터/검색, 페이지네이션, 전화번호 마스킹 |
|
||||
| 당첨자추첨 | `participation-당첨자추첨.puml` | UFR-PART-030 | Fisher-Yates Shuffle, WinnerSelected 발행 |
|
||||
|
||||
**주요 특징**:
|
||||
- **중복 방지**: Redis Cache + DB 2단계 체크
|
||||
- **추첨 알고리즘**: 난수 기반 공정성, 가산점 시스템, Fisher-Yates Shuffle
|
||||
- **Kafka Event**: ParticipantRegistered, WinnerSelected → Analytics Service 구독
|
||||
- **보안**: 전화번호 마스킹 (010-****-1234)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Analytics 서비스 (5개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 대시보드조회-캐시히트 | `analytics-대시보드조회-캐시히트.puml` | UFR-ANAL-010 | Redis 캐시 HIT (0.5초) |
|
||||
| 대시보드조회-캐시미스 | `analytics-대시보드조회-캐시미스.puml` | UFR-ANAL-010 | 외부 API 병렬 호출, ROI 계산 (3초) |
|
||||
| 이벤트생성구독 | `analytics-이벤트생성구독.puml` | - | EventCreated 구독, 통계 초기화 |
|
||||
| 참여자등록구독 | `analytics-참여자등록구독.puml` | - | ParticipantRegistered 구독, 실시간 통계 |
|
||||
| 배포완료구독 | `analytics-배포완료구독.puml` | - | DistributionCompleted 구독, 배포 통계 |
|
||||
|
||||
**주요 특징**:
|
||||
- **Cache-Aside 패턴**: Redis 캐싱 (TTL 5분, 히트율 95%)
|
||||
- **외부 API 병렬 호출**: 우리동네TV, 지니TV, SNS APIs (Circuit Breaker, Timeout, Fallback)
|
||||
- **Kafka 구독**: 3개 Event Topics 실시간 처리
|
||||
- **멱등성 보장**: Redis Set으로 중복 이벤트 방지
|
||||
|
||||
---
|
||||
|
||||
### 2.5 AI 서비스 (1개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 트렌드분석및추천 | `ai-트렌드분석및추천.puml` | UFR-AI-010 | Kafka ai-job 구독, 트렌드 분석, 3가지 추천 병렬 생성 |
|
||||
|
||||
**주요 특징**:
|
||||
- **Kafka Job 구독**: ai-job 토픽 Consumer
|
||||
- **외부 AI API**: Claude/GPT-4 호출 (Circuit Breaker, Timeout 30초)
|
||||
- **캐싱 전략**: 트렌드 분석 결과 (TTL 1시간), 추천 결과 (TTL 24시간)
|
||||
- **3가지 옵션 병렬 생성**: 저비용/중비용/고비용 추천안
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Content 서비스 (1개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 이미지생성 | `content-이미지생성.puml` | UFR-CONT-010 | Kafka image-job 구독, 3가지 스타일 병렬 생성 |
|
||||
|
||||
**주요 특징**:
|
||||
- **Kafka Job 구독**: image-job 토픽 Consumer
|
||||
- **외부 이미지 API**: Stable Diffusion/DALL-E 병렬 호출 (Circuit Breaker, Timeout 20초)
|
||||
- **3가지 스타일 병렬**: 심플/화려한/트렌디 (par 블록)
|
||||
- **CDN 업로드**: 이미지 URL 캐싱 (TTL 7일)
|
||||
- **Fallback 2단계**: Stable Diffusion 실패 → DALL-E → 기본 템플릿
|
||||
|
||||
---
|
||||
|
||||
### 2.7 Distribution 서비스 (2개)
|
||||
|
||||
| 시나리오 | 파일명 | 유저스토리 | 주요 처리 내용 |
|
||||
|----------|--------|-----------|---------------|
|
||||
| 다중채널배포 | `distribution-다중채널배포.puml` | UFR-DIST-010 | REST API 동기 호출, 채널별 병렬 배포, DistributionCompleted 발행 |
|
||||
| 배포상태조회 | `distribution-배포상태조회.puml` | UFR-DIST-020 | 배포 상태 모니터링, 재시도 기능 |
|
||||
|
||||
**주요 특징**:
|
||||
- **동기 호출**: Event Service → Distribution Service REST API
|
||||
- **채널별 병렬 배포**: 우리동네TV, 링고비즈, 지니TV, SNS APIs (par 블록)
|
||||
- **Resilience 패턴**: Circuit Breaker, Retry (3회), Bulkhead (채널별 독립)
|
||||
- **독립 처리**: 하나 실패해도 다른 채널 계속
|
||||
- **Kafka Event**: DistributionCompleted → Analytics Service 구독
|
||||
|
||||
---
|
||||
|
||||
## 3. 설계 원칙
|
||||
|
||||
### 3.1 공통설계원칙 준수
|
||||
|
||||
✅ **PlantUML 표준**
|
||||
- `!theme mono` 테마 적용
|
||||
- 명확한 타이틀 및 참여자 타입 표시
|
||||
- 외부 시스템/인프라 `<<E>>` 표시
|
||||
|
||||
✅ **레이어 아키텍처**
|
||||
```
|
||||
Controller (API Layer)
|
||||
↓
|
||||
Service (Business Layer)
|
||||
↓
|
||||
Repository (Data Layer)
|
||||
↓
|
||||
External Systems (Redis, DB, Kafka, APIs)
|
||||
```
|
||||
|
||||
✅ **동기/비동기 구분**
|
||||
- 실선 화살표 (`→`): 동기 호출
|
||||
- 점선 화살표 (`-->`): 비동기 호출 (Kafka)
|
||||
- `activate`/`deactivate`: 생명선 활성화
|
||||
|
||||
### 3.2 내부시퀀스설계 가이드 준수
|
||||
|
||||
✅ **유저스토리 기반 설계**
|
||||
- 20개 유저스토리와 정확히 매칭
|
||||
- 불필요한 추가 설계 배제
|
||||
|
||||
✅ **외부 시퀀스와 일치**
|
||||
- 외부 시퀀스 다이어그램과 플로우 일치
|
||||
- 서비스 간 통신 방식 동일
|
||||
|
||||
✅ **모든 레이어 표시**
|
||||
- API, 비즈니스, 데이터, 인프라 레이어 명시
|
||||
- 캐시, DB, 외부 API 접근 표시
|
||||
|
||||
---
|
||||
|
||||
## 4. 주요 패턴
|
||||
|
||||
### 4.1 Resilience 패턴
|
||||
|
||||
#### Circuit Breaker
|
||||
- **적용 대상**: 모든 외부 API 호출
|
||||
- **설정**: 실패율 50% 초과 시 Open, 30초 후 Half-Open
|
||||
- **효과**: 빠른 실패로 리소스 보호
|
||||
|
||||
#### Retry Pattern
|
||||
- **적용 대상**: 일시적 장애가 예상되는 외부 API
|
||||
- **설정**: 최대 3회, 지수 백오프 (1초, 2초, 4초)
|
||||
- **효과**: 일시적 장애 자동 복구
|
||||
|
||||
#### Timeout Pattern
|
||||
- **적용 대상**: 모든 외부 API 호출
|
||||
- **설정**: 국세청 5초, AI 30초, 이미지 20초, 배포 10초
|
||||
- **효과**: 리소스 점유 방지
|
||||
|
||||
#### Fallback Pattern
|
||||
- **적용 대상**: 외부 API 장애 시
|
||||
- **전략**: 캐시된 이전 데이터, 기본값, 검증 스킵
|
||||
- **효과**: 서비스 지속성 보장 (Graceful Degradation)
|
||||
|
||||
#### Bulkhead Pattern
|
||||
- **적용 대상**: Distribution Service 다중 채널 배포
|
||||
- **설정**: 채널별 독립 스레드 풀
|
||||
- **효과**: 채널 장애 격리, 장애 전파 차단
|
||||
|
||||
### 4.2 캐싱 전략 (Cache-Aside)
|
||||
|
||||
| 서비스 | 캐시 키 패턴 | TTL | 히트율 목표 | 효과 |
|
||||
|--------|-------------|-----|-----------|------|
|
||||
| User | `user:business:{사업자번호}` | 7일 | 90% | 5초 → 0.1초 (98% 개선) |
|
||||
| AI | `ai:recommendation:{업종}:{지역}:{목적}` | 24시간 | 80% | 10초 → 0.1초 (99% 개선) |
|
||||
| Content | `content:image:{이벤트ID}:{스타일}` | 7일 | 80% | 5초 → 0.1초 (98% 개선) |
|
||||
| Analytics | `analytics:dashboard:{이벤트ID}` | 5분 | 95% | 3초 → 0.5초 (83% 개선) |
|
||||
| Event | `event:detail:{eventId}` | 5분 | 85% | 1초 → 0.2초 (80% 개선) |
|
||||
| Participation | `participation:list:{eventId}:{filter}` | 5분 | 90% | 2초 → 0.3초 (85% 개선) |
|
||||
|
||||
### 4.3 Event-Driven 패턴
|
||||
|
||||
#### Kafka Event Topics (도메인 이벤트)
|
||||
- **EventCreated**: 이벤트 생성 시 → Analytics Service 구독
|
||||
- **ParticipantRegistered**: 참여자 등록 시 → Analytics Service 구독
|
||||
- **WinnerSelected**: 당첨자 선정 시 → (추후 확장)
|
||||
- **DistributionCompleted**: 배포 완료 시 → Analytics Service 구독
|
||||
|
||||
#### Kafka Job Topics (비동기 작업)
|
||||
- **ai-job**: AI 추천 요청 → AI Service 구독
|
||||
- **image-job**: 이미지 생성 요청 → Content Service 구독
|
||||
|
||||
#### 멱등성 보장
|
||||
- Redis Set으로 이벤트 ID 중복 체크
|
||||
- 동일 이벤트 중복 처리 시 무시
|
||||
|
||||
---
|
||||
|
||||
## 5. 파일 구조
|
||||
|
||||
```
|
||||
design/backend/sequence/inner/
|
||||
├── README.md (본 문서)
|
||||
├── user-회원가입.puml
|
||||
├── user-로그인.puml
|
||||
├── user-프로필수정.puml
|
||||
├── user-로그아웃.puml
|
||||
├── event-목적선택.puml
|
||||
├── event-AI추천요청.puml
|
||||
├── event-추천결과조회.puml
|
||||
├── event-이미지생성요청.puml
|
||||
├── event-이미지결과조회.puml
|
||||
├── event-콘텐츠선택.puml
|
||||
├── event-최종승인및배포.puml
|
||||
├── event-상세조회.puml
|
||||
├── event-목록조회.puml
|
||||
├── event-대시보드조회.puml
|
||||
├── participation-이벤트참여.puml
|
||||
├── participation-참여자목록조회.puml
|
||||
├── participation-당첨자추첨.puml
|
||||
├── analytics-대시보드조회-캐시히트.puml
|
||||
├── analytics-대시보드조회-캐시미스.puml
|
||||
├── analytics-이벤트생성구독.puml
|
||||
├── analytics-참여자등록구독.puml
|
||||
├── analytics-배포완료구독.puml
|
||||
├── ai-트렌드분석및추천.puml
|
||||
├── content-이미지생성.puml
|
||||
├── distribution-다중채널배포.puml
|
||||
└── distribution-배포상태조회.puml
|
||||
```
|
||||
|
||||
**총 26개 파일, 약 114KB**
|
||||
|
||||
---
|
||||
|
||||
## 6. PlantUML 다이어그램 확인 방법
|
||||
|
||||
### 6.1 온라인 확인
|
||||
|
||||
#### PlantUML Web Server
|
||||
1. https://www.plantuml.com/plantuml/uml 접속
|
||||
2. 각 `.puml` 파일 내용 복사
|
||||
3. 에디터에 붙여넣기
|
||||
4. 다이어그램 시각적 확인
|
||||
5. PNG/SVG/PDF 다운로드 가능
|
||||
|
||||
#### PlantUML Editor (추천)
|
||||
1. https://plantuml-editor.kkeisuke.com/ 접속
|
||||
2. 실시간 미리보기 제공
|
||||
3. 편집 및 다운로드 지원
|
||||
|
||||
### 6.2 로컬 확인 (Docker)
|
||||
|
||||
#### Docker로 PlantUML 검증
|
||||
```bash
|
||||
# Docker 실행 필요
|
||||
docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:jetty
|
||||
|
||||
# 각 파일 문법 검사
|
||||
cat "user-회원가입.puml" | docker exec -i plantuml java -jar /app/plantuml.jar -syntax
|
||||
```
|
||||
|
||||
### 6.3 IDE 플러그인
|
||||
|
||||
#### IntelliJ IDEA
|
||||
- **PlantUML Integration** 플러그인 설치
|
||||
- `.puml` 파일 우클릭 → "Show PlantUML Diagram"
|
||||
|
||||
#### VS Code
|
||||
- **PlantUML** 확장 설치
|
||||
- `Alt+D`: 미리보기 열기
|
||||
|
||||
---
|
||||
|
||||
## 부록
|
||||
|
||||
### A. 파일 크기 및 통계
|
||||
|
||||
| 서비스 | 시나리오 수 | 총 크기 | 평균 크기 |
|
||||
|--------|------------|---------|----------|
|
||||
| User | 4 | 21.2KB | 5.3KB |
|
||||
| Event | 10 | 20.2KB | 2.0KB |
|
||||
| Participation | 3 | 15.4KB | 5.1KB |
|
||||
| Analytics | 5 | 20.8KB | 4.2KB |
|
||||
| AI | 1 | 12KB | 12KB |
|
||||
| Content | 1 | 8.5KB | 8.5KB |
|
||||
| Distribution | 2 | 17.5KB | 8.8KB |
|
||||
| **총계** | **26** | **115.6KB** | **4.4KB** |
|
||||
|
||||
### B. 주요 기술 스택
|
||||
|
||||
#### Backend
|
||||
- **Framework**: Spring Boot
|
||||
- **ORM**: JPA/Hibernate
|
||||
- **Security**: Spring Security + JWT
|
||||
- **Cache**: Redis
|
||||
- **Database**: PostgreSQL
|
||||
- **Message Queue**: Apache Kafka
|
||||
|
||||
#### Resilience
|
||||
- **Circuit Breaker**: Resilience4j
|
||||
- **Retry**: Resilience4j RetryRegistry
|
||||
- **Timeout**: Resilience4j TimeLimiterRegistry
|
||||
|
||||
#### Utilities
|
||||
- **Password**: bcrypt (Spring Security)
|
||||
- **JWT**: jjwt library
|
||||
- **Encryption**: AES-256 (javax.crypto)
|
||||
|
||||
### C. 참고 문서
|
||||
- [유저스토리](../../userstory.md)
|
||||
- [외부 시퀀스 설계서](../outer/)
|
||||
- [논리 아키텍처](../../logical/logical-architecture.md)
|
||||
- [공통설계원칙](../../../../claude/common-principles.md)
|
||||
- [내부시퀀스설계 가이드](../../../../claude/sequence-inner-design.md)
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**최종 수정일**: 2025-10-22
|
||||
**작성자**: System Architect (박영자)
|
||||
**내부 시퀀스 설계 완료**: ✅ 26개 시나리오 모두 작성 완료
|
||||
@@ -0,0 +1,343 @@
|
||||
@startuml ai-트렌드분석및추천
|
||||
!theme mono
|
||||
|
||||
title AI Service - 트렌드 분석 및 이벤트 추천 (내부 시퀀스)
|
||||
|
||||
actor Client
|
||||
participant "Kafka Consumer" as Consumer <<Component>>
|
||||
participant "JobMessageHandler" as Handler <<Controller>>
|
||||
participant "AIRecommendationService" as Service <<Service>>
|
||||
participant "TrendAnalysisEngine" as TrendEngine <<Component>>
|
||||
participant "RecommendationEngine" as RecommendEngine <<Component>>
|
||||
participant "CacheManager" as Cache <<Component>>
|
||||
participant "CircuitBreakerManager" as CB <<Component>>
|
||||
participant "ExternalAIClient" as AIClient <<Component>>
|
||||
participant "JobStateManager" as JobState <<Component>>
|
||||
participant "Redis" as Redis <<Infrastructure>>
|
||||
participant "External AI API" as ExternalAPI <<External>>
|
||||
participant "Kafka Producer" as Producer <<Component>>
|
||||
|
||||
note over Consumer: Kafka ai-job Topic 구독\nConsumer Group: ai-service-group
|
||||
|
||||
== 1. Job 메시지 수신 ==
|
||||
Consumer -> Handler: onMessage(jobMessage)\n{jobId, eventDraftId, 목적, 업종, 지역, 매장정보}
|
||||
activate Handler
|
||||
|
||||
Handler -> Handler: 메시지 유효성 검증
|
||||
note right
|
||||
검증 항목:
|
||||
- jobId 존재 여부
|
||||
- eventDraftId 유효성
|
||||
- 필수 파라미터 (목적, 업종, 지역)
|
||||
end note
|
||||
|
||||
alt 유효하지 않은 메시지
|
||||
Handler -> Producer: DLQ 발행 (Dead Letter Queue)\n{jobId, error: INVALID_MESSAGE}
|
||||
Handler --> Consumer: ACK (메시지 처리 완료)
|
||||
note over Handler: 잘못된 메시지는 DLQ로 이동\n수동 검토 필요
|
||||
else 유효한 메시지
|
||||
Handler -> JobState: updateJobStatus(jobId, PROCESSING)
|
||||
JobState -> Redis: SET job:{jobId}:status = PROCESSING
|
||||
Redis --> JobState: OK
|
||||
JobState --> Handler: 상태 업데이트 완료
|
||||
|
||||
Handler -> Service: generateRecommendations(\neventDraftId, 목적, 업종, 지역, 매장정보)
|
||||
activate Service
|
||||
|
||||
== 2. 트렌드 분석 ==
|
||||
Service -> TrendEngine: analyzeTrends(업종, 지역, 목적)
|
||||
activate TrendEngine
|
||||
|
||||
TrendEngine -> Cache: getCachedTrend(업종, 지역)
|
||||
Cache -> Redis: GET trend:{업종}:{지역}
|
||||
Redis --> Cache: 캐시 결과
|
||||
|
||||
alt 캐시 히트
|
||||
Cache --> TrendEngine: 캐시된 트렌드 데이터
|
||||
note right
|
||||
캐시 키: trend:{업종}:{지역}
|
||||
TTL: 1시간
|
||||
데이터: {
|
||||
industry_trends,
|
||||
regional_characteristics,
|
||||
seasonal_patterns
|
||||
}
|
||||
end note
|
||||
|
||||
else 캐시 미스
|
||||
note right of TrendEngine
|
||||
**트렌드 분석 입력 데이터**
|
||||
- 업종 정보
|
||||
- 지역 정보
|
||||
- 현재 시즌 (계절, 월)
|
||||
- 이벤트 목적
|
||||
|
||||
**외부 AI API 호출**
|
||||
- 과거 이벤트 데이터 사용 안 함
|
||||
- 실시간 시장 트렌드 분석
|
||||
- 업종별/지역별 일반적 특성
|
||||
end note
|
||||
|
||||
TrendEngine -> CB: executeWithCircuitBreaker(\nAI API 트렌드 분석 호출)
|
||||
activate CB
|
||||
|
||||
CB -> CB: Circuit Breaker 상태 확인
|
||||
note right
|
||||
**Circuit Breaker 설정**
|
||||
- Failure Rate Threshold: 50%
|
||||
- Timeout: 5분 (300초)
|
||||
- Half-Open Wait Duration: 1분 (60초)
|
||||
- Permitted Calls in Half-Open: 3
|
||||
- Sliding Window Size: 10
|
||||
end note
|
||||
|
||||
alt Circuit CLOSED (정상)
|
||||
CB -> AIClient: callAIAPI(\nmethod: "trendAnalysis",\nprompt: 트렌드 분석 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
note right of AIClient
|
||||
**AI 프롬프트 구성**
|
||||
"당신은 마케팅 트렌드 분석 전문가입니다.
|
||||
|
||||
**입력 정보**
|
||||
- 업종: {업종}
|
||||
- 지역: {지역}
|
||||
- 현재 시즌: {계절/월}
|
||||
- 이벤트 목적: {목적}
|
||||
|
||||
**분석 요청사항**
|
||||
1. 업종별 일반적 트렌드
|
||||
(업종 특성 기반 효과적인 이벤트 유형)
|
||||
|
||||
2. 지역별 특성
|
||||
(지역 고객 특성, 선호도)
|
||||
|
||||
3. 시즌별 추천
|
||||
(현재 시기에 적합한 이벤트)"
|
||||
end note
|
||||
|
||||
AIClient -> ExternalAPI: AI API 호출\nPOST /api/v1/analyze\nAuthorization: Bearer {API_KEY}\nTimeout: 5분\nPayload: {업종, 지역, 시즌, 목적}
|
||||
activate ExternalAPI
|
||||
|
||||
ExternalAPI --> AIClient: 200 OK\n{"industryTrend": "...",\n"regionalCharacteristics": "...",\n"seasonalRecommendation": "..."}
|
||||
deactivate ExternalAPI
|
||||
|
||||
AIClient -> AIClient: 응답 검증 및 파싱
|
||||
AIClient --> CB: 분석 결과
|
||||
deactivate AIClient
|
||||
|
||||
CB -> CB: 성공 기록 (Circuit Breaker)
|
||||
CB --> TrendEngine: 트렌드 분석 결과
|
||||
deactivate CB
|
||||
|
||||
TrendEngine -> Cache: cacheTrend(\nkey: trend:{업종}:{지역},\ndata: 분석결과,\nTTL: 1시간)
|
||||
Cache -> Redis: SETEX trend:{업종}:{지역} 3600 {분석결과}
|
||||
Redis --> Cache: OK
|
||||
Cache --> TrendEngine: 캐싱 완료
|
||||
|
||||
else Circuit OPEN (장애)
|
||||
CB --> TrendEngine: CircuitBreakerOpenException
|
||||
TrendEngine -> TrendEngine: Fallback 실행\n(기본 트렌드 데이터 사용)
|
||||
note right
|
||||
Fallback 전략:
|
||||
- 이전 캐시 데이터 반환
|
||||
- 또는 기본 트렌드 템플릿 사용
|
||||
- 클라이언트에 안내 메시지 포함
|
||||
end note
|
||||
|
||||
else Circuit HALF-OPEN (복구 시도)
|
||||
CB -> AIClient: 제한된 요청 허용 (3개)
|
||||
AIClient -> ExternalAPI: POST /api/v1/analyze
|
||||
ExternalAPI --> AIClient: 200 OK
|
||||
AIClient --> CB: 성공
|
||||
CB -> CB: 연속 성공 시 CLOSED로 전환
|
||||
CB --> TrendEngine: 트렌드 분석 결과
|
||||
|
||||
else Timeout (5분 초과)
|
||||
CB --> TrendEngine: TimeoutException
|
||||
note right of TrendEngine
|
||||
**Timeout 처리**
|
||||
- 5분 초과 시 즉시 실패
|
||||
- Fallback: 기본 트렌드 사용
|
||||
- 사용자에게 안내 메시지 제공
|
||||
end note
|
||||
TrendEngine -> TrendEngine: Fallback 실행\n(기본 트렌드 템플릿 사용)
|
||||
end
|
||||
end
|
||||
|
||||
TrendEngine --> Service: 트렌드 분석 완료\n{업종트렌드, 지역특성, 시즌특성}
|
||||
deactivate TrendEngine
|
||||
|
||||
== 3. 이벤트 추천 생성 (3가지 옵션) ==
|
||||
Service -> RecommendEngine: generateRecommendations(\n목적, 트렌드, 매장정보)
|
||||
activate RecommendEngine
|
||||
|
||||
RecommendEngine -> RecommendEngine: 추천 컨텍스트 구성
|
||||
note right
|
||||
추천 입력:
|
||||
- 이벤트 목적 (신규 고객 유치 등)
|
||||
- 트렌드 분석 결과
|
||||
- 매장 정보 (업종, 위치, 크기)
|
||||
- 예산 범위 (저/중/고)
|
||||
end note
|
||||
|
||||
group parallel
|
||||
RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 1: 저비용)
|
||||
activate CB
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 저비용 추천 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
note right
|
||||
옵션 1 프롬프트:
|
||||
"저비용, 높은 참여율 중심 이벤트 기획
|
||||
목적: {목적}
|
||||
트렌드: {트렌드}
|
||||
매장: {매장정보}
|
||||
|
||||
출력 형식:
|
||||
- 이벤트 제목
|
||||
- 추천 경품 (예산: 저)
|
||||
- 참여 방법 (난이도: 낮음)
|
||||
- 예상 참여자 수
|
||||
- 예상 비용
|
||||
- 예상 ROI"
|
||||
end note
|
||||
|
||||
AIClient -> ExternalAPI: POST /api/v1/recommend\n(저비용 옵션)
|
||||
ExternalAPI --> AIClient: 200 OK\n{추천안 1}
|
||||
AIClient --> CB: 추천안 1
|
||||
deactivate AIClient
|
||||
CB --> RecommendEngine: 옵션 1 완료
|
||||
deactivate CB
|
||||
|
||||
RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 2: 중비용)
|
||||
activate CB
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 중비용 추천 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
note right
|
||||
옵션 2 프롬프트:
|
||||
"중비용, 균형잡힌 ROI 이벤트 기획
|
||||
목적: {목적}
|
||||
트렌드: {트렌드}
|
||||
매장: {매장정보}
|
||||
|
||||
출력 형식:
|
||||
- 이벤트 제목
|
||||
- 추천 경품 (예산: 중)
|
||||
- 참여 방법 (난이도: 중간)
|
||||
- 예상 참여자 수
|
||||
- 예상 비용
|
||||
- 예상 ROI"
|
||||
end note
|
||||
|
||||
AIClient -> ExternalAPI: POST /api/v1/recommend\n(중비용 옵션)
|
||||
ExternalAPI --> AIClient: 200 OK\n{추천안 2}
|
||||
AIClient --> CB: 추천안 2
|
||||
deactivate AIClient
|
||||
CB --> RecommendEngine: 옵션 2 완료
|
||||
deactivate CB
|
||||
|
||||
RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 3: 고비용)
|
||||
activate CB
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 고비용 추천 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
note right
|
||||
옵션 3 프롬프트:
|
||||
"고비용, 높은 매출 증대 이벤트 기획
|
||||
목적: {목적}
|
||||
트렌드: {트렌드}
|
||||
매장: {매장정보}
|
||||
|
||||
출력 형식:
|
||||
- 이벤트 제목
|
||||
- 추천 경품 (예산: 고)
|
||||
- 참여 방법 (난이도: 높음)
|
||||
- 예상 참여자 수
|
||||
- 예상 비용
|
||||
- 예상 ROI"
|
||||
end note
|
||||
|
||||
AIClient -> ExternalAPI: POST /api/v1/recommend\n(고비용 옵션)
|
||||
ExternalAPI --> AIClient: 200 OK\n{추천안 3}
|
||||
AIClient --> CB: 추천안 3
|
||||
deactivate AIClient
|
||||
CB --> RecommendEngine: 옵션 3 완료
|
||||
deactivate CB
|
||||
end
|
||||
|
||||
RecommendEngine -> RecommendEngine: 3가지 추천안 통합 및 검증
|
||||
note right
|
||||
검증 항목:
|
||||
- 필수 필드 존재 여부
|
||||
- 예상 성과 계산 (ROI)
|
||||
- 추천안 차별화 확인
|
||||
- 홍보 문구 생성 (각 5개)
|
||||
- SNS 해시태그 자동 생성
|
||||
end note
|
||||
|
||||
RecommendEngine --> Service: 3가지 추천안 생성 완료
|
||||
deactivate RecommendEngine
|
||||
|
||||
== 4. 결과 저장 및 Job 상태 업데이트 ==
|
||||
Service -> Cache: cacheRecommendations(\nkey: ai:recommendation:{eventDraftId},\ndata: {트렌드+추천안},\nTTL: 24시간)
|
||||
Cache -> Redis: SETEX ai:recommendation:{eventDraftId} 86400 {결과}
|
||||
Redis --> Cache: OK
|
||||
Cache --> Service: 캐싱 완료
|
||||
|
||||
Service -> JobState: updateJobStatus(\njobId,\nstatus: COMPLETED,\nresult: {트렌드, 추천안})
|
||||
JobState -> Redis: HSET job:{jobId} status COMPLETED result {JSON}
|
||||
Redis --> JobState: OK
|
||||
JobState --> Service: 상태 업데이트 완료
|
||||
|
||||
Service --> Handler: 추천 생성 완료\n{트렌드분석, 3가지추천안}
|
||||
deactivate Service
|
||||
|
||||
Handler --> Consumer: ACK (메시지 처리 완료)
|
||||
deactivate Handler
|
||||
|
||||
note over Consumer: Job 처리 완료\nRedis에 저장된 결과를\n클라이언트는 폴링으로 조회
|
||||
end
|
||||
|
||||
== 예외 처리 ==
|
||||
note over Handler, Producer
|
||||
**AI API 장애 시**
|
||||
- Circuit Breaker Open
|
||||
- Fallback: 기본 트렌드 템플릿 사용
|
||||
- Job 상태: COMPLETED (안내 메시지 포함)
|
||||
- 사용자에게 "AI 분석이 제한적으로 제공됩니다" 안내
|
||||
|
||||
**Timeout (5분 초과)**
|
||||
- Circuit Breaker로 즉시 실패
|
||||
- Retry 없음 (비동기 Job)
|
||||
- Job 상태: FAILED
|
||||
- 사용자에게 재시도 요청 안내
|
||||
|
||||
**Kafka 메시지 처리 실패**
|
||||
- DLQ(Dead Letter Queue)로 이동
|
||||
- 수동 검토 및 재처리
|
||||
- 에러 로그 기록
|
||||
|
||||
**Redis 장애**
|
||||
- 캐싱 스킵
|
||||
- Job 상태는 메모리에 임시 저장
|
||||
- 성능 저하 가능 (매 요청마다 AI API 호출)
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 2분 이내
|
||||
- P95 응답 시간: 4분 이내
|
||||
- Circuit Breaker Timeout: 5분
|
||||
- Redis 캐시 TTL: 24시간
|
||||
|
||||
**데이터 처리 원칙**
|
||||
- 과거 이벤트 데이터 사용 안 함
|
||||
- 외부 AI API로 실시간 트렌드 분석
|
||||
- 업종/지역 기반 일반적 마케팅 트렌드 활용
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,342 @@
|
||||
@startuml analytics-대시보드조회
|
||||
!theme mono
|
||||
|
||||
title Analytics Service - 대시보드 조회 내부 시퀀스\n(UFR-ANAL-010: 실시간 성과분석 대시보드 조회)
|
||||
|
||||
participant "AnalyticsController" as Controller
|
||||
participant "AnalyticsService" as Service
|
||||
participant "CacheService" as Cache
|
||||
participant "AnalyticsRepository" as Repository
|
||||
participant "ExternalChannelService" as ChannelService
|
||||
participant "ROICalculator" as Calculator
|
||||
participant "CircuitBreaker" as CB
|
||||
participant "Redis<<E>>" as Redis
|
||||
database "Analytics DB<<E>>" as DB
|
||||
|
||||
-> Controller: GET /api/events/{id}/analytics\n+ Authorization: Bearer {token}
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: getDashboardData(eventId, userId)
|
||||
activate Service
|
||||
|
||||
note right of Service
|
||||
**입력 검증**
|
||||
- eventId: UUID 형식 검증
|
||||
- userId: JWT에서 추출
|
||||
- 권한 확인: 매장 소유자 여부
|
||||
end note
|
||||
|
||||
Service -> Cache: get("analytics:dashboard:{eventId}")
|
||||
activate Cache
|
||||
|
||||
note right of Cache
|
||||
**Cache-Aside 패턴**
|
||||
- Redis GET 호출
|
||||
- Cache Key 구조:
|
||||
analytics:dashboard:{eventId}
|
||||
- TTL: 3600초 (1시간)
|
||||
end note
|
||||
|
||||
Cache -> Redis: GET analytics:dashboard:{eventId}
|
||||
activate Redis
|
||||
|
||||
alt Cache HIT
|
||||
|
||||
Redis --> Cache: **Cache HIT**\n캐시된 데이터 반환\n{\n totalParticipants: 1234,\n totalViews: 17200,\n roi: 250,\n channelStats: [...],\n lastUpdated: "2025-10-22T10:30:00Z"\n}
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: Dashboard 데이터 (JSON)
|
||||
deactivate Cache
|
||||
|
||||
note right of Service
|
||||
**응답 데이터 구조**
|
||||
- 4개 요약 카드
|
||||
* 총 참여자 수, 달성률
|
||||
* 총 노출 수, 증감률
|
||||
* 예상 ROI, 업종 평균 대비
|
||||
* 매출 증가율
|
||||
- 채널별 성과
|
||||
- 시간대별 참여 추이
|
||||
- 참여자 프로필 분석
|
||||
- 비교 분석 (업종 평균, 이전 이벤트)
|
||||
end note
|
||||
|
||||
Service --> Controller: DashboardResponse\n(200 OK)
|
||||
deactivate Service
|
||||
|
||||
Controller --> : 200 OK\nDashboard Data (JSON)
|
||||
deactivate Controller
|
||||
|
||||
note over Controller, Redis
|
||||
**Cache HIT 시나리오 성능**
|
||||
- 응답 시간: 약 0.5초
|
||||
- Redis 조회 시간: 0.01초
|
||||
- 직렬화/역직렬화: 0.05초
|
||||
- HTTP 오버헤드: 0.44초
|
||||
- 예상 히트율: 95%
|
||||
end note
|
||||
|
||||
else Cache MISS
|
||||
|
||||
Redis --> Cache: **Cache MISS** (null)
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: null (캐시 미스)
|
||||
deactivate Cache
|
||||
|
||||
note right of Service
|
||||
**Cache MISS 처리**
|
||||
- 데이터 통합 작업 시작
|
||||
- 로컬 DB 조회 + 외부 API 병렬 호출
|
||||
end note
|
||||
|
||||
|||
|
||||
== 1. Analytics DB 조회 (로컬 데이터) ==
|
||||
|
||||
Service -> Repository: getEventStats(eventId)
|
||||
activate Repository
|
||||
|
||||
Repository -> DB: 이벤트 통계 조회\n(이벤트ID로 통계 데이터 조회)
|
||||
activate DB
|
||||
|
||||
DB --> Repository: EventStatsEntity\n- totalParticipants\n- estimatedROI\n- salesGrowthRate
|
||||
deactivate DB
|
||||
|
||||
Repository --> Service: EventStats
|
||||
deactivate Repository
|
||||
|
||||
note right of Service
|
||||
**로컬 데이터 확보**
|
||||
- 총 참여자 수
|
||||
- 예상 ROI (DB 캐시)
|
||||
- 매출 증가율 (POS 연동)
|
||||
end note
|
||||
|
||||
|||
|
||||
== 2. 외부 채널 API 병렬 호출 (Circuit Breaker 적용) ==
|
||||
|
||||
note right of Service
|
||||
**병렬 처리 시작**
|
||||
- CompletableFuture 4개 생성
|
||||
- 우리동네TV, 지니TV, 링고비즈, SNS APIs 동시 호출
|
||||
- Circuit Breaker 적용 (채널별 독립)
|
||||
end note
|
||||
|
||||
par 외부 API 병렬 호출
|
||||
|
||||
Service -> ChannelService: getWooriTVStats(eventId)
|
||||
activate ChannelService
|
||||
|
||||
ChannelService -> CB: execute("wooriTV", () -> callAPI())
|
||||
activate CB
|
||||
|
||||
note right of CB
|
||||
**Circuit Breaker**
|
||||
- State: CLOSED (정상)
|
||||
- Failure Rate: 50% 초과 시 OPEN
|
||||
- Timeout: 10초
|
||||
end note
|
||||
|
||||
CB -> CB: 외부 API 호출\nGET /stats/{eventId}
|
||||
|
||||
alt Circuit Breaker CLOSED (정상)
|
||||
CB --> ChannelService: ChannelStats\n- views: 5000\n- clicks: 1200
|
||||
deactivate CB
|
||||
|
||||
ChannelService --> Service: WooriTVStats
|
||||
deactivate ChannelService
|
||||
else Circuit Breaker OPEN (장애)
|
||||
CB -> CB: **Fallback 실행**\n캐시된 이전 데이터 반환
|
||||
note right of CB
|
||||
Fallback 전략:
|
||||
- Redis에서 이전 통계 조회
|
||||
- 없으면 기본값 (0) 반환
|
||||
- 알림: "일부 채널 데이터 로딩 실패"
|
||||
end note
|
||||
CB --> ChannelService: Fallback 데이터
|
||||
deactivate CB
|
||||
ChannelService --> Service: WooriTVStats (Fallback)
|
||||
deactivate ChannelService
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
Service -> ChannelService: getGenieTVStats(eventId)
|
||||
activate ChannelService
|
||||
|
||||
ChannelService -> CB: execute("genieTV", () -> callAPI())
|
||||
activate CB
|
||||
|
||||
CB -> CB: 외부 API 호출\nGET /campaign/{id}/stats
|
||||
|
||||
alt 정상 응답
|
||||
CB --> ChannelService: ChannelStats\n- adViews: 10000\n- clicks: 500
|
||||
deactivate CB
|
||||
ChannelService --> Service: GenieTVStats
|
||||
deactivate ChannelService
|
||||
else Timeout (10초 초과)
|
||||
CB -> CB: **Timeout 처리**\n기본값 반환
|
||||
note right of CB
|
||||
Timeout 발생:
|
||||
- 리소스 점유 방지
|
||||
- Fallback으로 기본값 (0) 설정
|
||||
- 알림: "지니TV 데이터 로딩 지연"
|
||||
end note
|
||||
CB --> ChannelService: 기본값 (0)
|
||||
deactivate CB
|
||||
ChannelService --> Service: GenieTVStats (기본값)
|
||||
deactivate ChannelService
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
Service -> ChannelService: getRingoBizStats(eventId)
|
||||
activate ChannelService
|
||||
|
||||
ChannelService -> CB: execute("ringoBiz", () -> callAPI())
|
||||
activate CB
|
||||
|
||||
note right of CB
|
||||
**Circuit Breaker**
|
||||
- State: CLOSED (정상)
|
||||
- Failure Rate: 50% 초과 시 OPEN
|
||||
- Timeout: 10초
|
||||
end note
|
||||
|
||||
CB -> CB: 외부 API 호출\nGET /voice-stats/{eventId}
|
||||
|
||||
alt 정상 응답
|
||||
CB --> ChannelService: ChannelStats\n- calls: 3000\n- completed: 2500\n- avgDuration: 45초
|
||||
deactivate CB
|
||||
ChannelService --> Service: RingoBizStats
|
||||
deactivate ChannelService
|
||||
else Timeout 또는 장애
|
||||
CB -> CB: **Fallback 실행**\n기본값 반환
|
||||
note right of CB
|
||||
링고비즈 API 장애:
|
||||
- 기본값 (0) 반환
|
||||
- 알림: "링고비즈 데이터 로딩 실패"
|
||||
end note
|
||||
CB --> ChannelService: 기본값 (0)
|
||||
deactivate CB
|
||||
ChannelService --> Service: RingoBizStats (기본값)
|
||||
deactivate ChannelService
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
Service -> ChannelService: getSNSStats(eventId)
|
||||
activate ChannelService
|
||||
|
||||
ChannelService -> CB: execute("SNS", () -> callAPIs())
|
||||
activate CB
|
||||
|
||||
note right of CB
|
||||
**SNS APIs 통합 호출**
|
||||
- Instagram API
|
||||
- Naver Blog API
|
||||
- Kakao Channel API
|
||||
- 3개 API 병렬 호출
|
||||
end note
|
||||
|
||||
CB -> CB: 외부 APIs 호출\n(Instagram, Naver, Kakao)
|
||||
|
||||
alt 정상 응답
|
||||
CB --> ChannelService: SNSStats\n- Instagram: likes 300, comments 50\n- Naver: views 2000\n- Kakao: shares 100
|
||||
deactivate CB
|
||||
ChannelService --> Service: SNSStats
|
||||
deactivate ChannelService
|
||||
else 장애 또는 Timeout
|
||||
CB -> CB: **Fallback 실행**\n기본값 반환
|
||||
note right of CB
|
||||
SNS API 장애:
|
||||
- 기본값 (0) 반환
|
||||
- 알림: "SNS 데이터 로딩 실패"
|
||||
end note
|
||||
CB --> ChannelService: 기본값 (0)
|
||||
deactivate CB
|
||||
ChannelService --> Service: SNSStats (기본값)
|
||||
deactivate ChannelService
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|||
|
||||
== 3. 데이터 통합 및 ROI 계산 ==
|
||||
|
||||
Service -> Service: mergeChannelStats(\n wooriTV, genieTV, ringoBiz, sns\n)
|
||||
|
||||
note right of Service
|
||||
**데이터 통합**
|
||||
- 총 노출 수 = 외부 채널 노출 합계
|
||||
- 총 참여자 수 = Analytics DB
|
||||
- 채널별 전환율 = 참여자 수 / 노출 수
|
||||
- 링고비즈: 통화 완료 수 포함
|
||||
end note
|
||||
|
||||
Service -> Calculator: calculateROI(\n eventStats, channelStats\n)
|
||||
activate Calculator
|
||||
|
||||
note right of Calculator
|
||||
**ROI 계산 로직**
|
||||
총 비용 = 경품 비용 + 플랫폼 비용
|
||||
예상 수익 = 매출 증가액 + 신규 고객 LTV
|
||||
ROI = (수익 - 비용) / 비용 × 100
|
||||
end note
|
||||
|
||||
Calculator --> Service: ROIData\n- roi: 250%\n- totalCost: 100만원\n- totalRevenue: 350만원\n- breakEvenPoint: 달성
|
||||
deactivate Calculator
|
||||
|
||||
Service -> Service: buildDashboardData(\n eventStats, channelStats, roiData\n)
|
||||
|
||||
note right of Service
|
||||
**대시보드 데이터 구조 생성**
|
||||
- 4개 요약 카드
|
||||
- 채널별 성과 차트 데이터
|
||||
- 시간대별 참여 추이
|
||||
- 참여자 프로필 분석
|
||||
- 비교 분석 (업종 평균, 이전 이벤트)
|
||||
end note
|
||||
|
||||
|||
|
||||
== 4. Redis 캐싱 및 응답 ==
|
||||
|
||||
Service -> Cache: set(\n "analytics:dashboard:{eventId}",\n dashboardData,\n TTL=3600\n)
|
||||
activate Cache
|
||||
|
||||
Cache -> Redis: 캐시 저장\nSET analytics:dashboard:{eventId}\nvalue={통합 데이터}\nEX 3600 (1시간)
|
||||
activate Redis
|
||||
|
||||
Redis --> Cache: OK (저장 완료)
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: OK (캐싱 완료)
|
||||
deactivate Cache
|
||||
|
||||
note right of Service
|
||||
**캐싱 완료**
|
||||
- TTL: 3600초 (1시간)
|
||||
- 다음 조회 시 Cache HIT
|
||||
- 예상 크기: 5KB
|
||||
- 갱신 주기: 1시간마다 새 데이터 조회
|
||||
end note
|
||||
|
||||
Service --> Controller: DashboardResponse\n(200 OK)
|
||||
deactivate Service
|
||||
|
||||
Controller --> : 200 OK\nDashboard Data (JSON)
|
||||
deactivate Controller
|
||||
|
||||
note over Controller, DB
|
||||
**Cache MISS 시나리오 성능**
|
||||
- 응답 시간: 약 3초
|
||||
- Analytics DB 조회: 0.1초
|
||||
- 외부 API 병렬 호출: 2초 (병렬 처리)
|
||||
- ROI 계산: 0.05초
|
||||
- Redis 캐싱: 0.01초
|
||||
- 직렬화/HTTP: 0.84초
|
||||
end note
|
||||
|
||||
end
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,168 @@
|
||||
@startuml analytics-배포완료구독
|
||||
!theme mono
|
||||
|
||||
title Analytics Service - DistributionCompleted 이벤트 구독 처리 내부 시퀀스\n(Kafka Event 구독)
|
||||
|
||||
participant "Kafka Consumer" as Consumer
|
||||
participant "DistributionCompletedListener" as Listener
|
||||
participant "AnalyticsService" as Service
|
||||
participant "AnalyticsRepository" as Repository
|
||||
participant "CacheService" as Cache
|
||||
participant "Redis" as Redis
|
||||
database "Analytics DB" as DB
|
||||
|
||||
note over Consumer
|
||||
**Kafka Consumer 설정**
|
||||
- Topic: DistributionCompleted
|
||||
- Consumer Group: analytics-service
|
||||
- Partition Key: eventId
|
||||
- At-Least-Once Delivery 보장
|
||||
end note
|
||||
|
||||
Kafka -> Consumer: DistributionCompleted 이벤트 수신\n{\n eventId: "uuid",\n distributedChannels: [\n {\n channel: "우리동네TV",\n status: "SUCCESS",\n expectedViews: 5000\n },\n {\n channel: "지니TV",\n status: "SUCCESS",\n expectedViews: 10000\n },\n {\n channel: "Instagram",\n status: "SUCCESS",\n expectedViews: 2000\n }\n ],\n completedAt: "2025-10-22T12:00:00Z"\n}
|
||||
activate Consumer
|
||||
|
||||
Consumer -> Listener: onDistributionCompleted(event)
|
||||
activate Listener
|
||||
|
||||
note right of Listener
|
||||
**멱등성 체크**
|
||||
- Redis Set에 이벤트 ID 존재 여부 확인
|
||||
- 중복 처리 방지
|
||||
- Key: distribution_completed:{eventId}
|
||||
end note
|
||||
|
||||
Listener -> Redis: SISMEMBER distribution_completed {eventId}
|
||||
activate Redis
|
||||
|
||||
alt 이벤트 미처리 (멱등성 보장)
|
||||
Redis --> Listener: false (미처리)
|
||||
deactivate Redis
|
||||
|
||||
Listener -> Service: updateDistributionStats(event)
|
||||
activate Service
|
||||
|
||||
note right of Service
|
||||
**배포 채널 통계 저장**
|
||||
- 채널별 배포 상태 기록
|
||||
- 예상 노출 수 집계
|
||||
- 배포 완료 시각 기록
|
||||
end note
|
||||
|
||||
Service -> Service: parseChannelStats(event)
|
||||
|
||||
note right of Service
|
||||
**채널 데이터 파싱**
|
||||
- distributedChannels 배열 순회
|
||||
- 각 채널별 통계 추출
|
||||
- 총 예상 노출 수 계산
|
||||
end note
|
||||
|
||||
loop 각 채널별로
|
||||
Service -> Repository: saveChannelStats(\n eventId, channel, stats\n)
|
||||
activate Repository
|
||||
|
||||
Repository -> DB: 채널별 통계 저장\n(이벤트ID, 채널명, 상태,\n예상노출수, 배포일시 저장,\n중복 시 업데이트)
|
||||
activate DB
|
||||
|
||||
DB --> Repository: 1 row inserted/updated
|
||||
deactivate DB
|
||||
|
||||
Repository --> Service: ChannelStatsEntity
|
||||
deactivate Repository
|
||||
end
|
||||
|
||||
note right of Service
|
||||
**배포 통계 저장 완료**
|
||||
- 채널별 배포 상태 기록
|
||||
- 예상 노출 수 저장
|
||||
- 향후 외부 API 조회 시 기준 데이터로 활용
|
||||
end note
|
||||
|
||||
Service -> Repository: updateTotalViews(eventId, totalViews)
|
||||
activate Repository
|
||||
|
||||
Repository -> DB: 총 노출 수 업데이트\n(총 예상 노출 수를 설정하고,\n수정일시를 현재 시각으로 업데이트)
|
||||
activate DB
|
||||
|
||||
DB --> Repository: 1 row updated
|
||||
deactivate DB
|
||||
|
||||
Repository --> Service: UpdateResult (success)
|
||||
deactivate Repository
|
||||
|
||||
note right of Service
|
||||
**이벤트 통계 업데이트**
|
||||
- 총 예상 노출 수 업데이트
|
||||
- 다음 대시보드 조회 시 반영
|
||||
end note
|
||||
|
||||
Service -> Cache: delete("analytics:dashboard:{eventId}")
|
||||
activate Cache
|
||||
|
||||
note right of Cache
|
||||
**캐시 무효화**
|
||||
- 기존 캐시 삭제
|
||||
- 다음 조회 시 최신 배포 통계 반영
|
||||
- 채널별 성과 차트 갱신
|
||||
end note
|
||||
|
||||
Cache -> Redis: DEL analytics:dashboard:{eventId}
|
||||
activate Redis
|
||||
|
||||
Redis --> Cache: OK
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: OK
|
||||
deactivate Cache
|
||||
|
||||
Service -> Redis: SADD distribution_completed {eventId}
|
||||
activate Redis
|
||||
|
||||
note right of Redis
|
||||
**멱등성 처리 완료 기록**
|
||||
- Redis Set에 eventId 추가
|
||||
- TTL 설정 (7일)
|
||||
end note
|
||||
|
||||
Redis --> Service: OK
|
||||
deactivate Redis
|
||||
|
||||
Service --> Listener: 배포 통계 업데이트 완료
|
||||
deactivate Service
|
||||
|
||||
Listener -> Consumer: ACK (처리 완료)
|
||||
deactivate Listener
|
||||
|
||||
else 이벤트 이미 처리됨 (중복)
|
||||
Redis --> Listener: true (이미 처리)
|
||||
deactivate Redis
|
||||
|
||||
note right of Listener
|
||||
**중복 이벤트 스킵**
|
||||
- At-Least-Once Delivery로 인한 중복
|
||||
- 멱등성 보장으로 중복 처리 방지
|
||||
end note
|
||||
|
||||
Listener -> Consumer: ACK (스킵)
|
||||
deactivate Listener
|
||||
end
|
||||
|
||||
Consumer --> Kafka: Commit Offset
|
||||
deactivate Consumer
|
||||
|
||||
note over Consumer, DB
|
||||
**처리 시간**
|
||||
- 이벤트 수신 → 통계 업데이트 완료: 약 0.3초
|
||||
- 채널별 DB INSERT (3개): 0.15초
|
||||
- event_stats UPDATE: 0.05초
|
||||
- Redis 캐시 무효화: 0.01초
|
||||
- 멱등성 체크: 0.01초
|
||||
|
||||
**배포 통계 효과**
|
||||
- 배포 완료 즉시 통계 반영
|
||||
- 채널별 성과 추적 가능
|
||||
- 다음 대시보드 조회 시 최신 배포 정보 제공
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,134 @@
|
||||
@startuml analytics-이벤트생성구독
|
||||
!theme mono
|
||||
|
||||
title Analytics Service - EventCreated 이벤트 구독 처리 내부 시퀀스\n(Kafka Event 구독)
|
||||
|
||||
participant "Kafka Consumer" as Consumer
|
||||
participant "EventCreatedListener" as Listener
|
||||
participant "AnalyticsService" as Service
|
||||
participant "AnalyticsRepository" as Repository
|
||||
participant "CacheService" as Cache
|
||||
participant "Redis" as Redis
|
||||
database "Analytics DB" as DB
|
||||
|
||||
note over Consumer
|
||||
**Kafka Consumer 설정**
|
||||
- Topic: EventCreated
|
||||
- Consumer Group: analytics-service
|
||||
- Partition Key: eventId
|
||||
- At-Least-Once Delivery 보장
|
||||
end note
|
||||
|
||||
Kafka -> Consumer: EventCreated 이벤트 수신\n{\n eventId: "uuid",\n storeId: "uuid",\n title: "이벤트 제목",\n objective: "신규 고객 유치",\n createdAt: "2025-10-22T10:00:00Z"\n}
|
||||
activate Consumer
|
||||
|
||||
Consumer -> Listener: onEventCreated(event)
|
||||
activate Listener
|
||||
|
||||
note right of Listener
|
||||
**멱등성 체크**
|
||||
- Redis Set에 이벤트 ID 존재 여부 확인
|
||||
- 중복 처리 방지
|
||||
end note
|
||||
|
||||
Listener -> Redis: SISMEMBER processed_events {eventId}
|
||||
activate Redis
|
||||
|
||||
alt 이벤트 미처리 (멱등성 보장)
|
||||
Redis --> Listener: false (미처리)
|
||||
deactivate Redis
|
||||
|
||||
Listener -> Service: initializeEventStats(event)
|
||||
activate Service
|
||||
|
||||
note right of Service
|
||||
**이벤트 통계 초기화**
|
||||
- 이벤트 기본 정보 저장
|
||||
- 통계 초기값 설정
|
||||
* 총 참여자 수: 0
|
||||
* 총 노출 수: 0
|
||||
* 예상 ROI: 계산 전
|
||||
* 매출 증가율: 0%
|
||||
end note
|
||||
|
||||
Service -> Repository: save(eventStatsEntity)
|
||||
activate Repository
|
||||
|
||||
Repository -> DB: 이벤트 통계 초기화\n(이벤트ID, 매장ID, 제목, 목적,\n참여자수/노출수/ROI/매출증가율을\n0으로 초기화하여 저장)
|
||||
activate DB
|
||||
|
||||
DB --> Repository: 1 row inserted
|
||||
deactivate DB
|
||||
|
||||
Repository --> Service: EventStatsEntity
|
||||
deactivate Repository
|
||||
|
||||
note right of Service
|
||||
**초기화 완료**
|
||||
- 이벤트 통계 DB 생성
|
||||
- 향후 ParticipantRegistered 이벤트 수신 시
|
||||
실시간 증가
|
||||
end note
|
||||
|
||||
Service -> Cache: delete("analytics:dashboard:{eventId}")
|
||||
activate Cache
|
||||
|
||||
note right of Cache
|
||||
**캐시 무효화**
|
||||
- 기존 캐시 삭제
|
||||
- 다음 조회 시 최신 데이터 갱신
|
||||
end note
|
||||
|
||||
Cache -> Redis: DEL analytics:dashboard:{eventId}
|
||||
activate Redis
|
||||
|
||||
Redis --> Cache: OK
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: OK
|
||||
deactivate Cache
|
||||
|
||||
Service -> Redis: SADD processed_events {eventId}
|
||||
activate Redis
|
||||
|
||||
note right of Redis
|
||||
**멱등성 처리 완료 기록**
|
||||
- Redis Set에 eventId 추가
|
||||
- TTL 설정 (7일)
|
||||
end note
|
||||
|
||||
Redis --> Service: OK
|
||||
deactivate Redis
|
||||
|
||||
Service --> Listener: EventStats 초기화 완료
|
||||
deactivate Service
|
||||
|
||||
Listener -> Consumer: ACK (처리 완료)
|
||||
deactivate Listener
|
||||
|
||||
else 이벤트 이미 처리됨 (중복)
|
||||
Redis --> Listener: true (이미 처리)
|
||||
deactivate Redis
|
||||
|
||||
note right of Listener
|
||||
**중복 이벤트 스킵**
|
||||
- At-Least-Once Delivery로 인한 중복
|
||||
- 멱등성 보장으로 중복 처리 방지
|
||||
end note
|
||||
|
||||
Listener -> Consumer: ACK (스킵)
|
||||
deactivate Listener
|
||||
end
|
||||
|
||||
Consumer --> Kafka: Commit Offset
|
||||
deactivate Consumer
|
||||
|
||||
note over Consumer, DB
|
||||
**처리 시간**
|
||||
- 이벤트 수신 → 초기화 완료: 약 0.2초
|
||||
- DB INSERT: 0.05초
|
||||
- Redis 캐시 무효화: 0.01초
|
||||
- 멱등성 체크: 0.01초
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,135 @@
|
||||
@startuml analytics-참여자등록구독
|
||||
!theme mono
|
||||
|
||||
title Analytics Service - ParticipantRegistered 이벤트 구독 처리 내부 시퀀스\n(Kafka Event 구독)
|
||||
|
||||
participant "Kafka Consumer" as Consumer
|
||||
participant "ParticipantRegisteredListener" as Listener
|
||||
participant "AnalyticsService" as Service
|
||||
participant "AnalyticsRepository" as Repository
|
||||
participant "CacheService" as Cache
|
||||
participant "Redis" as Redis
|
||||
database "Analytics DB" as DB
|
||||
|
||||
note over Consumer
|
||||
**Kafka Consumer 설정**
|
||||
- Topic: ParticipantRegistered
|
||||
- Consumer Group: analytics-service
|
||||
- Partition Key: eventId
|
||||
- At-Least-Once Delivery 보장
|
||||
end note
|
||||
|
||||
Kafka -> Consumer: ParticipantRegistered 이벤트 수신\n{\n participantId: "uuid",\n eventId: "uuid",\n phoneNumber: "010-1234-5678",\n registeredAt: "2025-10-22T11:30:00Z"\n}
|
||||
activate Consumer
|
||||
|
||||
Consumer -> Listener: onParticipantRegistered(event)
|
||||
activate Listener
|
||||
|
||||
note right of Listener
|
||||
**멱등성 체크**
|
||||
- Redis Set에 participantId 존재 여부 확인
|
||||
- 중복 처리 방지
|
||||
end note
|
||||
|
||||
Listener -> Redis: SISMEMBER processed_participants {participantId}
|
||||
activate Redis
|
||||
|
||||
alt 이벤트 미처리 (멱등성 보장)
|
||||
Redis --> Listener: false (미처리)
|
||||
deactivate Redis
|
||||
|
||||
Listener -> Service: updateParticipantCount(eventId)
|
||||
activate Service
|
||||
|
||||
note right of Service
|
||||
**참여자 수 실시간 증가**
|
||||
- DB UPDATE로 참여자 수 증가
|
||||
- 캐시 무효화로 다음 조회 시 최신 데이터 반영
|
||||
end note
|
||||
|
||||
Service -> Repository: incrementParticipantCount(eventId)
|
||||
activate Repository
|
||||
|
||||
Repository -> DB: 참여자 수 증가\n(참여자 수를 1 증가시키고,\n수정일시를 현재 시각으로 업데이트)
|
||||
activate DB
|
||||
|
||||
DB --> Repository: 1 row updated
|
||||
deactivate DB
|
||||
|
||||
Repository --> Service: UpdateResult (success)
|
||||
deactivate Repository
|
||||
|
||||
note right of Service
|
||||
**실시간 통계 업데이트 완료**
|
||||
- 참여자 수 +1
|
||||
- 다음 대시보드 조회 시 최신 통계 반영
|
||||
end note
|
||||
|
||||
Service -> Cache: delete("analytics:dashboard:{eventId}")
|
||||
activate Cache
|
||||
|
||||
note right of Cache
|
||||
**캐시 무효화**
|
||||
- 기존 캐시 삭제
|
||||
- 다음 조회 시 최신 참여자 수 반영
|
||||
- Cache MISS 시 DB 조회로 최신 데이터 확보
|
||||
end note
|
||||
|
||||
Cache -> Redis: DEL analytics:dashboard:{eventId}
|
||||
activate Redis
|
||||
|
||||
Redis --> Cache: OK
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: OK
|
||||
deactivate Cache
|
||||
|
||||
Service -> Redis: SADD processed_participants {participantId}
|
||||
activate Redis
|
||||
|
||||
note right of Redis
|
||||
**멱등성 처리 완료 기록**
|
||||
- Redis Set에 participantId 추가
|
||||
- TTL 설정 (7일)
|
||||
end note
|
||||
|
||||
Redis --> Service: OK
|
||||
deactivate Redis
|
||||
|
||||
Service --> Listener: 참여자 수 업데이트 완료
|
||||
deactivate Service
|
||||
|
||||
Listener -> Consumer: ACK (처리 완료)
|
||||
deactivate Listener
|
||||
|
||||
else 이벤트 이미 처리됨 (중복)
|
||||
Redis --> Listener: true (이미 처리)
|
||||
deactivate Redis
|
||||
|
||||
note right of Listener
|
||||
**중복 이벤트 스킵**
|
||||
- At-Least-Once Delivery로 인한 중복
|
||||
- 멱등성 보장으로 중복 처리 방지
|
||||
end note
|
||||
|
||||
Listener -> Consumer: ACK (스킵)
|
||||
deactivate Listener
|
||||
end
|
||||
|
||||
Consumer --> Kafka: Commit Offset
|
||||
deactivate Consumer
|
||||
|
||||
note over Consumer, DB
|
||||
**처리 시간**
|
||||
- 이벤트 수신 → 통계 업데이트 완료: 약 0.15초
|
||||
- DB UPDATE: 0.05초
|
||||
- Redis 캐시 무효화: 0.01초
|
||||
- 멱등성 체크: 0.01초
|
||||
|
||||
**실시간 업데이트 효과**
|
||||
- 참여자 등록 즉시 통계 반영
|
||||
- 다음 대시보드 조회 시 최신 데이터 제공
|
||||
- Cache-Aside 패턴으로 성능 유지
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,140 @@
|
||||
@startuml event-이미지결과조회
|
||||
!theme mono
|
||||
|
||||
title Content Service - 이미지 생성 결과 폴링 조회
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ContentController" as Controller <<API Layer>>
|
||||
participant "ContentService" as Service <<Business Layer>>
|
||||
participant "JobManager" as JobMgr <<Component>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
|
||||
note over Controller, Cache
|
||||
**폴링 방식 Job 상태 조회**
|
||||
- 최대 30초 동안 폴링 (2초 간격)
|
||||
- Job 상태: PENDING → PROCESSING → COMPLETED
|
||||
- 이미지 URL: Redis에 저장 (TTL: 7일)
|
||||
end note
|
||||
|
||||
Client -> Gateway: GET /api/content/jobs/{jobId}/status
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: GET /api/content/jobs/{jobId}/status
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: getJobStatus(jobId)
|
||||
activate Service
|
||||
|
||||
Service -> JobMgr: getJobStatus(jobId)
|
||||
activate JobMgr
|
||||
|
||||
JobMgr -> Cache: Job 상태 조회\nKey: job:{jobId}
|
||||
activate Cache
|
||||
|
||||
alt Job 데이터 존재
|
||||
Cache --> JobMgr: Job 데이터\n{status, eventDraftId,\ntype, createdAt}
|
||||
deactivate Cache
|
||||
|
||||
alt status = COMPLETED
|
||||
JobMgr -> Cache: 이미지 URL 조회\nKey: content:image:{eventDraftId}
|
||||
activate Cache
|
||||
Cache --> JobMgr: 이미지 URL\n{simple, fancy, trendy}
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nimageUrls: {...}}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: COMPLETED, imageUrls}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"imageUrls": {\n "simple": "https://cdn.../simple.png",\n "fancy": "https://cdn.../fancy.png",\n "trendy": "https://cdn.../trendy.png"\n}}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n이미지 URL 반환
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**프론트엔드 처리**
|
||||
- 3가지 스타일 카드 표시
|
||||
- 사용자가 스타일 선택
|
||||
- 이미지 편집 가능
|
||||
end note
|
||||
|
||||
else status = PROCESSING 또는 PENDING
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: PROCESSING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "이미지 생성 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n진행 중 상태
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**폴링 재시도**
|
||||
- 2초 후 재요청
|
||||
- 최대 30초 (15회)
|
||||
end note
|
||||
|
||||
else status = FAILED
|
||||
JobMgr -> Cache: 에러 정보 조회\nKey: job:{jobId}:error
|
||||
activate Cache
|
||||
Cache --> JobMgr: 에러 메시지
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: FAILED, error}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "이미지 생성 실패",\n"message": "다시 시도해주세요"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n실패 상태
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**실패 처리**
|
||||
- 에러 메시지 표시
|
||||
- "다시 생성" 버튼 제공
|
||||
end note
|
||||
end
|
||||
|
||||
else Job 데이터 없음
|
||||
Cache --> JobMgr: null (캐시 미스)
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: throw NotFoundException\n("Job을 찾을 수 없습니다")
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: NotFoundException
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job을 찾을 수 없습니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 404 Not Found
|
||||
deactivate Gateway
|
||||
end
|
||||
|
||||
note over Controller, Cache
|
||||
**폴링 전략**
|
||||
- 간격: 2초
|
||||
- 최대 시간: 30초 (15회)
|
||||
- Timeout 시: 사용자에게 알림 + "다시 생성" 옵션
|
||||
|
||||
**Redis 캐시**
|
||||
- Job 상태: TTL 1시간
|
||||
- 이미지 URL: TTL 7일
|
||||
|
||||
**성능 목표**
|
||||
- 평균 이미지 생성 시간: 20초 이내
|
||||
- P95 이미지 생성 시간: 40초 이내
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,255 @@
|
||||
@startuml content-이미지생성
|
||||
!theme mono
|
||||
|
||||
title Content Service - 이미지 생성 내부 시퀀스 (UFR-CONT-010)
|
||||
|
||||
actor Client
|
||||
participant "Kafka\nimage-job\nConsumer" as Consumer
|
||||
participant "JobHandler" as Handler
|
||||
participant "CacheManager" as Cache
|
||||
participant "ImageGenerator" as Generator
|
||||
participant "ImageStyleFactory" as Factory
|
||||
participant "StableDiffusion\nAPI Client" as SDClient
|
||||
participant "DALL-E\nAPI Client" as DALLEClient
|
||||
participant "Circuit Breaker" as CB
|
||||
participant "BlobStorage\nUploader" as BlobStorage
|
||||
participant "JobStatusManager" as JobStatus
|
||||
database "Redis Cache" as Redis
|
||||
|
||||
note over Consumer: Kafka 구독\nimage-job 토픽
|
||||
|
||||
== Kafka Job 수신 ==
|
||||
Consumer -> Handler: Job Message 수신\n{jobId, eventDraftId, eventInfo}
|
||||
activate Handler
|
||||
|
||||
Handler -> Cache: 캐시 조회\nkey: content:image:{eventDraftId}
|
||||
activate Cache
|
||||
Cache -> Redis: GET content:image:{eventDraftId}
|
||||
Redis --> Cache: 캐시 데이터 또는 NULL
|
||||
Cache --> Handler: 캐시 결과
|
||||
deactivate Cache
|
||||
|
||||
alt 캐시 HIT (기존 이미지 존재)
|
||||
Handler -> JobStatus: Job 상태 업데이트\nstatus: COMPLETED (캐시)
|
||||
activate JobStatus
|
||||
JobStatus -> Redis: SET job:{jobId}\n{status: COMPLETED, imageUrls: [...]}
|
||||
JobStatus --> Handler: 업데이트 완료
|
||||
deactivate JobStatus
|
||||
Handler --> Consumer: 처리 완료 (캐시)
|
||||
else 캐시 MISS (새로운 이미지 생성)
|
||||
Handler -> JobStatus: Job 상태 업데이트\nstatus: PROCESSING
|
||||
activate JobStatus
|
||||
JobStatus -> Redis: SET job:{jobId}\n{status: PROCESSING}
|
||||
JobStatus --> Handler: 업데이트 완료
|
||||
deactivate JobStatus
|
||||
|
||||
Handler -> Generator: 3가지 스타일 이미지 생성 요청\n{eventInfo}
|
||||
activate Generator
|
||||
|
||||
== 3가지 스타일 병렬 생성 (par 블록) ==
|
||||
group parallel
|
||||
Generator -> Factory: 심플 프롬프트 생성\n{eventInfo, style: SIMPLE}
|
||||
activate Factory
|
||||
Factory --> Generator: 심플 프롬프트
|
||||
deactivate Factory
|
||||
|
||||
Generator -> CB: Circuit Breaker 체크
|
||||
activate CB
|
||||
CB --> Generator: State: CLOSED (정상)
|
||||
deactivate CB
|
||||
|
||||
Generator -> SDClient: Stable Diffusion API 호출\n{prompt, style: SIMPLE}\nTimeout: 20초
|
||||
activate SDClient
|
||||
|
||||
note over SDClient: Circuit Breaker 적용\nRetry: 최대 3회\nTimeout: 20초
|
||||
|
||||
alt API 성공
|
||||
SDClient --> Generator: 심플 이미지 데이터
|
||||
deactivate SDClient
|
||||
Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: SIMPLE}\nRetry: 3회, Timeout: 30초
|
||||
activate BlobStorage
|
||||
note right of BlobStorage: SAS Token 생성\n(유효기간 7일)
|
||||
BlobStorage --> Generator: Blob SAS URL (심플)
|
||||
deactivate BlobStorage
|
||||
else API 실패 (Timeout/Error)
|
||||
SDClient --> Generator: 실패 응답
|
||||
deactivate SDClient
|
||||
Generator -> CB: 실패 기록
|
||||
activate CB
|
||||
CB -> CB: 실패율 계산
|
||||
alt 실패율 > 50%
|
||||
CB -> CB: Circuit State: OPEN
|
||||
end
|
||||
CB --> Generator: Circuit State
|
||||
deactivate CB
|
||||
|
||||
Generator -> DALLEClient: Fallback - DALL-E API 호출\n{prompt, style: SIMPLE}\nTimeout: 20초
|
||||
activate DALLEClient
|
||||
alt Fallback 성공
|
||||
DALLEClient --> Generator: 심플 이미지 데이터
|
||||
deactivate DALLEClient
|
||||
Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: SIMPLE}\nRetry: 3회, Timeout: 30초
|
||||
activate BlobStorage
|
||||
note right of BlobStorage: SAS Token 생성\n(유효기간 7일)
|
||||
BlobStorage --> Generator: Blob SAS URL (심플)
|
||||
deactivate BlobStorage
|
||||
else Fallback 실패
|
||||
DALLEClient --> Generator: 실패 응답
|
||||
deactivate DALLEClient
|
||||
Generator -> Generator: 기본 템플릿 사용\n(심플)
|
||||
end
|
||||
end
|
||||
|
||||
Generator -> Factory: 화려한 프롬프트 생성\n{eventInfo, style: FANCY}
|
||||
activate Factory
|
||||
Factory --> Generator: 화려한 프롬프트
|
||||
deactivate Factory
|
||||
|
||||
Generator -> CB: Circuit Breaker 체크
|
||||
activate CB
|
||||
CB --> Generator: State: CLOSED/OPEN
|
||||
deactivate CB
|
||||
|
||||
alt Circuit CLOSED
|
||||
Generator -> SDClient: Stable Diffusion API 호출\n{prompt, style: FANCY}\nTimeout: 20초
|
||||
activate SDClient
|
||||
|
||||
alt API 성공
|
||||
SDClient --> Generator: 화려한 이미지 데이터
|
||||
deactivate SDClient
|
||||
Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: FANCY}\nRetry: 3회, Timeout: 30초
|
||||
activate BlobStorage
|
||||
note right of BlobStorage: SAS Token 생성\n(유효기간 7일)
|
||||
BlobStorage --> Generator: Blob SAS URL (화려한)
|
||||
deactivate BlobStorage
|
||||
else API 실패
|
||||
SDClient --> Generator: 실패 응답
|
||||
deactivate SDClient
|
||||
Generator -> DALLEClient: Fallback - DALL-E API 호출
|
||||
activate DALLEClient
|
||||
alt Fallback 성공
|
||||
DALLEClient --> Generator: 화려한 이미지 데이터
|
||||
deactivate DALLEClient
|
||||
Generator -> BlobStorage: Blob 업로드\n{imageData, eventId, style: FANCY}\nRetry: 3회, Timeout: 30초
|
||||
activate BlobStorage
|
||||
note right of BlobStorage: SAS Token 생성\n(유효기간 7일)
|
||||
BlobStorage --> Generator: Blob SAS URL (화려한)
|
||||
deactivate BlobStorage
|
||||
else Fallback 실패
|
||||
DALLEClient --> Generator: 실패 응답
|
||||
deactivate DALLEClient
|
||||
Generator -> Generator: 기본 템플릿 사용\n(화려한)
|
||||
end
|
||||
end
|
||||
else Circuit OPEN
|
||||
Generator -> Generator: Circuit Open\n즉시 기본 템플릿 사용
|
||||
end
|
||||
|
||||
Generator -> Factory: 트렌디 프롬프트 생성\n{eventInfo, style: TRENDY}
|
||||
activate Factory
|
||||
Factory --> Generator: 트렌디 프롬프트
|
||||
deactivate Factory
|
||||
|
||||
Generator -> CB: Circuit Breaker 체크
|
||||
activate CB
|
||||
CB --> Generator: State: CLOSED/OPEN
|
||||
deactivate CB
|
||||
|
||||
alt Circuit CLOSED
|
||||
Generator -> SDClient: Stable Diffusion API 호출\n{prompt, style: TRENDY}\nTimeout: 20초
|
||||
activate SDClient
|
||||
|
||||
alt API 성공
|
||||
SDClient --> Generator: 트렌디 이미지 데이터
|
||||
deactivate SDClient
|
||||
Generator -> BlobStorage: Blob 업로드 요청\n{imageData, eventId, style: TRENDY}\nRetry: 3회, Timeout: 30초
|
||||
activate BlobStorage
|
||||
note right of BlobStorage: SAS Token 생성\n(유효기간 7일)
|
||||
BlobStorage --> Generator: Blob SAS URL (트렌디)
|
||||
deactivate BlobStorage
|
||||
else API 실패
|
||||
SDClient --> Generator: 실패 응답
|
||||
deactivate SDClient
|
||||
Generator -> DALLEClient: Fallback - DALL-E API 호출
|
||||
activate DALLEClient
|
||||
alt Fallback 성공
|
||||
DALLEClient --> Generator: 트렌디 이미지 데이터
|
||||
deactivate DALLEClient
|
||||
Generator -> BlobStorage: Blob 업로드\n{imageData, eventId, style: TRENDY}\nRetry: 3회, Timeout: 30초
|
||||
activate BlobStorage
|
||||
note right of BlobStorage: SAS Token 생성\n(유효기간 7일)
|
||||
BlobStorage --> Generator: Blob SAS URL (트렌디)
|
||||
deactivate BlobStorage
|
||||
else Fallback 실패
|
||||
DALLEClient --> Generator: 실패 응답
|
||||
deactivate DALLEClient
|
||||
Generator -> Generator: 기본 템플릿 사용\n(트렌디)
|
||||
end
|
||||
end
|
||||
else Circuit OPEN
|
||||
Generator -> Generator: Circuit Open\n즉시 기본 템플릿 사용
|
||||
end
|
||||
end
|
||||
|
||||
Generator --> Handler: 3가지 이미지 URL 반환\n{simple, fancy, trendy}
|
||||
deactivate Generator
|
||||
|
||||
== 결과 캐싱 및 Job 완료 ==
|
||||
Handler -> Cache: Blob SAS URL 캐싱\nkey: content:image:{eventDraftId}\nTTL: 7일
|
||||
activate Cache
|
||||
Cache -> Redis: SET content:image:{eventDraftId}\n{simple: SAS_URL, fancy: SAS_URL, trendy: SAS_URL}\nTTL: 604800 (7일)
|
||||
Redis --> Cache: 저장 완료
|
||||
Cache --> Handler: 캐싱 완료
|
||||
deactivate Cache
|
||||
|
||||
Handler -> JobStatus: Job 상태 업데이트\nstatus: COMPLETED
|
||||
activate JobStatus
|
||||
JobStatus -> Redis: SET job:{jobId}\n{status: COMPLETED, imageUrls: [...]}
|
||||
JobStatus --> Handler: 업데이트 완료
|
||||
deactivate JobStatus
|
||||
|
||||
Handler --> Consumer: 처리 완료
|
||||
note over Handler
|
||||
Blob SAS URL은 Redis에만 저장됨
|
||||
Event Service는 폴링을 통해
|
||||
Redis에서 결과 조회
|
||||
SAS Token 유효기간: 7일
|
||||
end note
|
||||
end
|
||||
|
||||
deactivate Handler
|
||||
|
||||
note over Consumer, Redis
|
||||
**Resilience 패턴 적용**
|
||||
- Circuit Breaker: 실패율 50% 초과 시 Open (AI API용)
|
||||
- AI API Timeout: 20초
|
||||
- Fallback: Stable Diffusion 실패 시 DALL-E, 모두 실패 시 기본 템플릿
|
||||
- Blob Storage Retry: 최대 3회 (Exponential Backoff: 1s, 2s, 4s)
|
||||
- Blob Storage Timeout: 30초 (대용량 이미지 고려)
|
||||
- Cache-Aside: Redis 캐싱 (TTL 7일)
|
||||
|
||||
**처리 시간**
|
||||
- 캐시 HIT: 0.1초
|
||||
- 캐시 MISS: 5.2초 이내 (병렬 처리)
|
||||
└─ AI 생성: 3-5초 + Blob 업로드: 0.15-0.3초
|
||||
|
||||
**병렬 처리**
|
||||
- 3가지 스타일 동시 생성 (par 블록)
|
||||
- 독립적인 스레드 풀 사용
|
||||
|
||||
**Blob Storage 업로드**
|
||||
- Azure Blob Storage (Korea Central)
|
||||
- SAS Token 기반 접근 제어 (읽기 전용)
|
||||
- SAS Token 유효기간: 7일 (Redis TTL과 동기화)
|
||||
- Public Access 비활성화 (보안 강화)
|
||||
- Container: event-images
|
||||
- URL 형식: https://{account}.blob.core.windows.net/event-images/{id}-{style}.png?{sas_token}
|
||||
|
||||
**보안**
|
||||
- Storage Account Public Access 비활성화
|
||||
- SAS Token 기반 URL 생성 (읽기 전용 권한)
|
||||
- Firewall 규칙: K8s Cluster IP만 허용
|
||||
- HTTPS 강제 (TLS 1.2 이상)
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,90 @@
|
||||
@startuml event-이미지생성요청
|
||||
!theme mono
|
||||
|
||||
title Content Service - 이미지 생성 요청 (UFR-CONT-010)
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ContentController" as Controller <<API Layer>>
|
||||
participant "ContentService" as Service <<Business Layer>>
|
||||
participant "JobManager" as JobMgr <<Component>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
|
||||
note over Controller, Cache
|
||||
**UFR-CONT-010: SNS 이미지 생성 요청**
|
||||
- Kafka 사용 안 함 (내부 Job 관리)
|
||||
- 백그라운드 워커가 비동기 처리
|
||||
- Redis에서 AI 추천 데이터 읽기
|
||||
- 3가지 스타일 이미지 생성 (심플, 화려한, 트렌디)
|
||||
end note
|
||||
|
||||
Client -> Gateway: POST /api/content/images/{eventDraftId}/generate
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: POST /api/content/images/{eventDraftId}/generate
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(eventDraftId 유효성)
|
||||
|
||||
Controller -> Service: generateImages(eventDraftId)
|
||||
activate Service
|
||||
|
||||
== 1단계: Redis에서 AI 추천 데이터 확인 ==
|
||||
|
||||
Service -> Cache: AI 추천 데이터 조회\nKey: ai:event:{eventDraftId}
|
||||
activate Cache
|
||||
Cache --> Service: AI 추천 결과\n{선택된 추천안, 이벤트 정보}
|
||||
deactivate Cache
|
||||
|
||||
alt AI 추천 데이터 없음
|
||||
Service --> Controller: throw NotFoundException\n("AI 추천을 먼저 선택해주세요")
|
||||
Controller --> Gateway: 404 Not Found\n{"code": "CONTENT_001",\n"message": "AI 추천을 먼저 선택해주세요"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
Gateway --> Client: 404 Not Found
|
||||
deactivate Gateway
|
||||
|
||||
else AI 추천 데이터 존재
|
||||
|
||||
== 2단계: Job 생성 ==
|
||||
|
||||
Service -> JobMgr: createJob(eventDraftId, imageGeneration)
|
||||
activate JobMgr
|
||||
|
||||
JobMgr -> JobMgr: Job ID 생성 (UUID)
|
||||
|
||||
JobMgr -> Cache: Job 상태 저장\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: IMAGE_GEN,\ncreatedAt}\nTTL: 1시간
|
||||
activate Cache
|
||||
Cache --> JobMgr: 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: Job 생성 완료\n{jobId, status: PENDING}
|
||||
deactivate JobMgr
|
||||
|
||||
== 3단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "이미지 생성 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 202 Accepted\n이미지 생성 시작
|
||||
deactivate Gateway
|
||||
|
||||
note over Service, Cache
|
||||
**백그라운드 워커 처리**
|
||||
- Redis 폴링 또는 스케줄러가 Job 감지
|
||||
- content-이미지생성.puml 참조
|
||||
- 외부 이미지 생성 API 호출 (병렬)
|
||||
- Redis에 이미지 URL 저장
|
||||
|
||||
**상세 내용**
|
||||
- 3가지 스타일 병렬 생성 (심플, 화려한, 트렌디)
|
||||
- Circuit Breaker 적용 (Timeout: 5분)
|
||||
- 결과: Redis Key: content:image:{eventDraftId}
|
||||
- TTL: 7일
|
||||
end note
|
||||
end
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,141 @@
|
||||
@startuml distribution-다중채널배포-sprint2
|
||||
!theme mono
|
||||
|
||||
title Distribution Service - 다중 채널 배포 Sprint 2 (UFR-DIST-010)
|
||||
|
||||
participant "Event Service" as EventSvc
|
||||
participant "Distribution\nREST API" as API
|
||||
participant "Distribution\nController" as Controller
|
||||
participant "Distribution\nService" as Service
|
||||
database "Distribution DB" as DB
|
||||
queue "Kafka" as Kafka
|
||||
|
||||
== REST API 동기 호출 수신 ==
|
||||
EventSvc -> API: POST /api/distribution/distribute\n{eventId, channels[], contentUrls}
|
||||
activate API
|
||||
|
||||
API -> Controller: distributeToChannels(request)
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: executeDistribution(distributionRequest)
|
||||
activate Service
|
||||
|
||||
Service -> DB: 배포 이력 초기화\n(이벤트ID, 상태를 PENDING으로 저장)
|
||||
DB --> Service: 배포 이력 ID
|
||||
|
||||
note over Service: 배포 시작 상태로 변경
|
||||
Service -> DB: 배포 이력 상태 업데이트\n(상태를 IN_PROGRESS로 변경)
|
||||
|
||||
== 다중 채널 배포 로그 기록 (Sprint 2: Mock 처리) ==
|
||||
|
||||
note over Service: Sprint 2: 실제 외부 API 호출 없이\n배포 결과만 기록
|
||||
|
||||
par 우리동네TV 배포
|
||||
alt 채널 선택됨
|
||||
Service -> Service: 우리동네TV 배포 처리\n(Mock: 즉시 성공 반환)
|
||||
activate Service
|
||||
|
||||
note over Service: 배포 요청 검증\n- eventId 유효성\n- contentUrls 존재 여부
|
||||
|
||||
Service -> DB: 배포 채널 로그 저장\n(채널: 우리동네TV,\n상태: 성공, 배포ID,\n예상노출수 저장)
|
||||
|
||||
note over Service: Mock 결과:\n성공 (distributionId 생성)
|
||||
|
||||
deactivate Service
|
||||
end
|
||||
|
||||
alt 링고비즈 선택됨
|
||||
Service -> Service: 링고비즈 배포 처리\n(Mock: 즉시 성공 반환)
|
||||
activate Service
|
||||
|
||||
note over Service: 배포 요청 검증\n- phoneNumber 형식\n- audioUrl 존재 여부
|
||||
|
||||
Service -> DB: 배포 채널 로그 저장\n(채널: 링고비즈,\n상태: 성공,\n업데이트 시각 저장)
|
||||
|
||||
note over Service: Mock 결과:\n성공 (timestamp 기록)
|
||||
|
||||
deactivate Service
|
||||
end
|
||||
|
||||
alt 지니TV 선택됨
|
||||
Service -> Service: 지니TV 배포 처리\n(Mock: 즉시 성공 반환)
|
||||
activate Service
|
||||
|
||||
note over Service: 배포 요청 검증\n- region 유효성\n- schedule 형식\n- budget 범위
|
||||
|
||||
Service -> DB: 배포 채널 로그 저장\n(채널: 지니TV,\n상태: 성공, 광고ID,\n노출 스케줄 저장)
|
||||
|
||||
note over Service: Mock 결과:\n성공 (adId 생성)
|
||||
|
||||
deactivate Service
|
||||
end
|
||||
|
||||
alt Instagram 선택됨
|
||||
Service -> Service: Instagram 배포 처리\n(Mock: 즉시 성공 반환)
|
||||
activate Service
|
||||
|
||||
note over Service: 배포 요청 검증\n- imageUrl 형식\n- caption 길이\n- hashtags 유효성
|
||||
|
||||
Service -> DB: 배포 채널 로그 저장\n(채널: Instagram,\n상태: 성공,\n포스트 URL/ID 저장)
|
||||
|
||||
note over Service: Mock 결과:\n성공 (postUrl, postId 생성)
|
||||
|
||||
deactivate Service
|
||||
end
|
||||
|
||||
alt Naver Blog 선택됨
|
||||
Service -> Service: Naver Blog 배포 처리\n(Mock: 즉시 성공 반환)
|
||||
activate Service
|
||||
|
||||
note over Service: 배포 요청 검증\n- imageUrl 형식\n- content 길이
|
||||
|
||||
Service -> DB: 배포 채널 로그 저장\n(채널: NaverBlog,\n상태: 성공,\n포스트 URL 저장)
|
||||
|
||||
note over Service: Mock 결과:\n성공 (postUrl 생성)
|
||||
|
||||
deactivate Service
|
||||
end
|
||||
|
||||
alt Kakao Channel 선택됨
|
||||
Service -> Service: Kakao Channel 배포 처리\n(Mock: 즉시 성공 반환)
|
||||
activate Service
|
||||
|
||||
note over Service: 배포 요청 검증\n- imageUrl 형식\n- message 길이
|
||||
|
||||
Service -> DB: 배포 채널 로그 저장\n(채널: KakaoChannel,\n상태: 성공,\n메시지 ID 저장)
|
||||
|
||||
note over Service: Mock 결과:\n성공 (messageId 생성)
|
||||
|
||||
deactivate Service
|
||||
end
|
||||
end
|
||||
|
||||
note over Service: 모든 채널 배포 완료\n(즉시 처리 - 외부 API 호출 없음)
|
||||
|
||||
== 배포 결과 집계 및 저장 ==
|
||||
Service -> Service: 채널별 배포 결과 집계\n성공: [선택된 모든 채널]
|
||||
|
||||
note over Service: Sprint 2에서는\n모든 채널 배포가 성공으로 처리됨
|
||||
|
||||
Service -> DB: 배포 이력 상태 업데이트\n(상태를 COMPLETED로,\n완료일시를 현재 시각으로 설정)
|
||||
|
||||
== Kafka 이벤트 발행 ==
|
||||
Service -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt}
|
||||
note over Kafka: Analytics Service 구독\n실시간 통계 업데이트
|
||||
|
||||
== REST API 동기 응답 ==
|
||||
Service --> Controller: 배포 완료 응답\n{status: COMPLETED, successChannels: [all]}
|
||||
deactivate Service
|
||||
|
||||
Controller --> API: DistributionResponse\n{eventId, status: COMPLETED, results: [all success]}
|
||||
deactivate Controller
|
||||
|
||||
API --> EventSvc: 200 OK\n{distributionId, status: COMPLETED, results[]}
|
||||
deactivate API
|
||||
|
||||
note over EventSvc: 배포 완료 응답 수신\n이벤트 상태 업데이트\nAPPROVED → ACTIVE
|
||||
|
||||
== Sprint 2 제약사항 ==
|
||||
note over Service: **Sprint 2 구현 범위**\n- 외부 API 호출 없음 (Mock 처리)\n- 모든 배포 요청은 성공으로 처리\n- 배포 로그만 DB에 기록\n- Circuit Breaker, Retry 미구현\n- 실패 처리 시나리오 미구현\n\n**Sprint 3 이후 구현 예정**\n- 실제 외부 채널 API 연동\n- Circuit Breaker 패턴 적용\n- Retry 로직 구현\n- 실패 처리 및 알림
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,126 @@
|
||||
@startuml event-AI추천요청
|
||||
!theme mono
|
||||
|
||||
title Event Service - AI 추천 요청 (Kafka Job 발행) (UFR-EVENT-030)
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "JobService" as JobSvc <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
participant "Kafka Producer" as Kafka <<E>>
|
||||
|
||||
note over Controller, Kafka
|
||||
**UFR-EVENT-030: AI 이벤트 추천 요청**
|
||||
- Kafka 비동기 Job 발행
|
||||
- AI Service가 Kafka 구독하여 처리
|
||||
- 트렌드 분석 + 3가지 추천안 생성
|
||||
- 처리 시간: 평균 2분 이내
|
||||
end note
|
||||
|
||||
Client -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{"objective": "신규 고객 유치",\n"industry": "음식점",\n"region": "서울 강남구"}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: POST /api/events/{eventDraftId}/ai-recommendations
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(필수 필드, 목적 유효성)
|
||||
|
||||
Controller -> Service: requestAIRecommendation(eventDraftId, userId)
|
||||
activate Service
|
||||
|
||||
== 1단계: 이벤트 초안 조회 및 검증 ==
|
||||
|
||||
Service -> Repo: findById(eventDraftId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID로 이벤트 목적,\n매장 정보 조회)
|
||||
activate DB
|
||||
DB --> Repo: EventDraft 엔티티\n{목적, 매장명, 업종, 주소}
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: 소유권 검증\nvalidateOwnership(userId, eventDraft)
|
||||
|
||||
alt 소유권 없음
|
||||
Service --> Controller: throw ForbiddenException\n("권한이 없습니다")
|
||||
Controller --> Gateway: 403 Forbidden\n{"code": "EVENT_003",\n"message": "권한이 없습니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
Gateway --> Client: 403 Forbidden
|
||||
deactivate Gateway
|
||||
|
||||
else 소유권 확인
|
||||
|
||||
== 2단계: Kafka Job 생성 ==
|
||||
|
||||
Service -> JobSvc: createAIJob(eventDraft)
|
||||
activate JobSvc
|
||||
|
||||
JobSvc -> JobSvc: Job ID 생성 (UUID)
|
||||
|
||||
JobSvc -> Cache: Job 상태 저장\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: AI_RECOMMEND,\ncreatedAt}\nTTL: 1시간
|
||||
activate Cache
|
||||
Cache --> JobSvc: 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
== 3단계: Kafka 이벤트 발행 ==
|
||||
|
||||
JobSvc -> Kafka: 이벤트 발행\nTopic: ai-job-topic\nPayload: {jobId, eventDraftId,\nobjective, industry,\nregion, storeInfo}
|
||||
activate Kafka
|
||||
note right of Kafka
|
||||
**Kafka Topic**
|
||||
- Topic: ai-job-topic
|
||||
- Consumer: AI Service
|
||||
- Consumer Group: ai-service-group
|
||||
|
||||
**Payload**
|
||||
{
|
||||
"jobId": "UUID",
|
||||
"eventDraftId": "UUID",
|
||||
"objective": "신규 고객 유치",
|
||||
"industry": "음식점",
|
||||
"region": "서울 강남구",
|
||||
"storeInfo": {...}
|
||||
}
|
||||
end note
|
||||
Kafka --> JobSvc: ACK (발행 확인)
|
||||
deactivate Kafka
|
||||
|
||||
JobSvc --> Service: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate JobSvc
|
||||
|
||||
== 4단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "AI가 분석 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 202 Accepted\nAI 분석 시작
|
||||
deactivate Gateway
|
||||
|
||||
note over Service, Kafka
|
||||
**AI Service 비동기 처리**
|
||||
- Kafka 구독: ai-job-topic
|
||||
- 트렌드 분석 (업종, 지역 기반)
|
||||
- 3가지 추천안 생성 (저/중/고 비용)
|
||||
- 결과: Redis에 저장 (TTL: 24시간)
|
||||
- 상세: ai-트렌드분석및추천.puml 참조
|
||||
|
||||
**처리 시간**
|
||||
- 평균: 2분 이내
|
||||
- P95: 4분 이내
|
||||
- Timeout: 5분
|
||||
|
||||
**결과 조회**
|
||||
- 폴링 방식: GET /api/jobs/{jobId}/status
|
||||
- 간격: 2초, 최대 30초
|
||||
end note
|
||||
end
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,73 @@
|
||||
@startuml event-대시보드조회
|
||||
!theme mono
|
||||
|
||||
title Event Service - 대시보드 이벤트 목록 (UFR-EVENT-010)
|
||||
|
||||
actor Client
|
||||
participant "EventController" as Controller <<C>>
|
||||
participant "EventService" as Service <<S>>
|
||||
participant "EventRepository" as Repo <<R>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
|
||||
note over Controller: GET /api/events/dashboard
|
||||
Controller -> Service: getDashboard(userId)
|
||||
activate Service
|
||||
|
||||
Service -> Cache: get("dashboard:" + userId)
|
||||
activate Cache
|
||||
|
||||
alt 캐시 히트
|
||||
Cache --> Service: Dashboard data
|
||||
Service --> Controller: DashboardResponse
|
||||
|
||||
else 캐시 미스
|
||||
Cache --> Service: null
|
||||
deactivate Cache
|
||||
|
||||
group parallel
|
||||
Service -> Repo: findTopByStatusAndUserId(ACTIVE, userId, limit=5)
|
||||
activate Repo
|
||||
Repo -> DB: 진행중 이벤트 목록 조회\n(사용자ID로 ACTIVE 상태 이벤트 조회,\n참여자 수 함께 조회,\n생성일 내림차순, 최대 5개)
|
||||
activate DB
|
||||
DB --> Repo: Active events
|
||||
deactivate DB
|
||||
Repo --> Service: List<Event> (active)
|
||||
deactivate Repo
|
||||
|
||||
Service -> Repo: findTopByStatusAndUserId(APPROVED, userId, limit=5)
|
||||
activate Repo
|
||||
Repo -> DB: 예정 이벤트 목록 조회\n(사용자ID로 APPROVED 상태 이벤트 조회,\n승인일 내림차순, 최대 5개)
|
||||
activate DB
|
||||
DB --> Repo: Approved events
|
||||
deactivate DB
|
||||
Repo --> Service: List<Event> (approved)
|
||||
deactivate Repo
|
||||
|
||||
Service -> Repo: findTopByStatusAndUserId(COMPLETED, userId, limit=5)
|
||||
activate Repo
|
||||
Repo -> DB: 종료 이벤트 목록 조회\n(사용자ID로 COMPLETED 상태 이벤트 조회,\n참여자 수 함께 조회,\n종료일 내림차순, 최대 5개)
|
||||
activate DB
|
||||
DB --> Repo: Completed events
|
||||
deactivate DB
|
||||
Repo --> Service: List<Event> (completed)
|
||||
deactivate Repo
|
||||
end
|
||||
|
||||
Service -> Service: buildDashboardResponse(active, approved, completed)
|
||||
note right: 대시보드 데이터 구성:\n- 진행중: 5개\n- 예정: 5개\n- 종료: 5개\n각 카드에 기본 통계 포함
|
||||
|
||||
Service -> Cache: set("dashboard:" + userId,\ndashboard, TTL=1분)
|
||||
activate Cache
|
||||
Cache --> Service: OK
|
||||
deactivate Cache
|
||||
end
|
||||
|
||||
Service --> Controller: DashboardResponse\n{active: [...], approved: [...],\ncompleted: [...]}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 200 OK\n{active: [\n {eventId, title, period, status,\n participantCount, viewCount, ...}\n],\napproved: [...],\ncompleted: [...]}
|
||||
|
||||
note over Controller, DB: 대시보드 카드 정보:\n- 이벤트명\n- 이벤트 기간\n- 진행 상태 뱃지\n- 간단한 통계\n (참여자 수, 조회수 등)\n\n섹션당 최대 5개 표시\n(최신 순)
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,64 @@
|
||||
@startuml event-목록조회
|
||||
!theme mono
|
||||
|
||||
title Event Service - 이벤트 목록 조회 (필터/검색) (UFR-EVENT-070)
|
||||
|
||||
participant "EventController" as Controller <<C>>
|
||||
participant "EventService" as Service <<S>>
|
||||
participant "EventRepository" as Repo <<R>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
|
||||
note over Controller: GET /api/events?status={상태}&keyword={검색어}\n&page={페이지}&size={크기}
|
||||
Controller -> Service: 이벤트 목록 조회(사용자ID, 필터, 페이징)
|
||||
activate Service
|
||||
|
||||
Service -> Cache: 캐시 조회("events:" + 사용자ID + ":" + 필터 + ":" + 페이지)
|
||||
activate Cache
|
||||
|
||||
alt 캐시 히트
|
||||
Cache --> Service: 이벤트 목록 데이터
|
||||
Service --> Controller: 이벤트 목록 응답
|
||||
|
||||
else 캐시 미스
|
||||
Cache --> Service: null
|
||||
deactivate Cache
|
||||
|
||||
Service -> Repo: 사용자별 필터링 이벤트 조회(사용자ID, 필터, 페이징)
|
||||
activate Repo
|
||||
|
||||
alt 필터 있음 (상태별)
|
||||
Repo -> DB: 사용자별 특정 상태 이벤트 조회\n(참여자 수 포함, 생성일 기준 내림차순,\n페이징 적용)
|
||||
else 검색 있음 (키워드)
|
||||
Repo -> DB: 사용자별 이벤트 키워드 검색\n(제목/설명에서 검색, 참여자 수 포함,\n생성일 기준 내림차순, 페이징 적용)
|
||||
else 필터 없음 (전체)
|
||||
Repo -> DB: 사용자별 전체 이벤트 목록 조회\n(참여자 수 포함, 생성일 기준 내림차순,\n페이징 적용)
|
||||
end
|
||||
|
||||
activate DB
|
||||
note right: 인덱스 활용:\n- 사용자ID\n- 상태\n- 생성일시
|
||||
DB --> Repo: 이벤트 목록 및 참여자 수
|
||||
deactivate DB
|
||||
|
||||
Repo -> DB: 전체 이벤트 개수 조회\n(필터 조건 포함, 페이징용)
|
||||
activate DB
|
||||
DB --> Repo: 전체 개수
|
||||
deactivate DB
|
||||
|
||||
Repo --> Service: 페이징된 이벤트 결과
|
||||
deactivate Repo
|
||||
|
||||
Service -> Cache: 캐시 저장("events:" + 사용자ID + ":" + 필터 + ":" + 페이지,\n페이징결과, TTL=1분)
|
||||
activate Cache
|
||||
Cache --> Service: OK
|
||||
deactivate Cache
|
||||
end
|
||||
|
||||
Service --> Controller: 이벤트 목록 응답\n{이벤트목록: [...], 전체개수,\n전체페이지수, 현재페이지}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 200 OK\n{이벤트목록: [\n {이벤트ID, 제목, 기간, 상태,\n 참여자수, ROI, 생성일시},\n ...\n],\n전체개수, 전체페이지수, 현재페이지}
|
||||
|
||||
note over Controller, DB: 필터 옵션:\n- 상태: 임시저장, 진행중, 완료\n- 기간: 최근 1개월/3개월/6개월/1년\n- 정렬: 최신순, 참여자 많은 순,\n ROI 높은 순\n\n페이지네이션:\n- 기본 20개/페이지\n- 페이지 번호 기반
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,110 @@
|
||||
@startuml event-목적선택
|
||||
!theme mono
|
||||
|
||||
title Event Service - 이벤트 목적 선택 및 저장 (UFR-EVENT-020)
|
||||
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
participant "Kafka Producer" as Kafka <<E>>
|
||||
actor Client
|
||||
|
||||
note over Controller, DB
|
||||
**UFR-EVENT-020: 이벤트 목적 선택 및 저장**
|
||||
- 목적 선택: 신규 고객 유치, 재방문 유도, 매출 증대, 인지도 향상
|
||||
- Redis 캐시 사용 (TTL: 30분)
|
||||
- Kafka 이벤트 발행 (EventDraftCreated)
|
||||
- 사용자 및 매장 정보는 User Service에서 조회 후 전달됨
|
||||
end note
|
||||
|
||||
Client -> Controller: POST /api/events/purposes\n{"userId": 123,\n"objective": "신규 고객 유치",\n"storeName": "맛있는집",\n"industry": "음식점",\n"address": "서울시 강남구"}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 입력값 검증\n(필수 필드, 목적 유효성 확인)
|
||||
|
||||
Controller -> Service: createEventDraft(userId, objective, storeInfo)
|
||||
activate Service
|
||||
|
||||
== 1단계: Redis 캐시 확인 ==
|
||||
|
||||
Service -> Cache: 캐시 조회\nKey: draft:event:{userId}\n(기존 작성 중인 이벤트 확인)
|
||||
activate Cache
|
||||
Cache --> Service: null (캐시 미스)
|
||||
deactivate Cache
|
||||
|
||||
== 2단계: 목적 유효성 검증 ==
|
||||
|
||||
Service -> Service: 목적 유효성 검증\n- 신규 고객 유치\n- 재방문 유도\n- 매출 증대\n- 인지도 향상
|
||||
|
||||
Service -> Service: 매장 정보 유효성 검증\n(매장명, 업종, 주소)
|
||||
|
||||
== 3단계: 이벤트 초안 저장 ==
|
||||
|
||||
Service -> Repo: save(eventDraft)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 저장\n(사용자ID, 목적, 매장명,\n업종, 주소, 상태=DRAFT,\n생성일시)\n저장 후 이벤트초안ID 반환
|
||||
activate DB
|
||||
DB --> Repo: 생성된 이벤트초안ID
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft 엔티티\n(eventDraftId 포함)
|
||||
deactivate Repo
|
||||
|
||||
== 4단계: Redis 캐시 저장 ==
|
||||
|
||||
Service -> Cache: 캐시 저장\nKey: draft:event:{eventDraftId}\nValue: {목적, 매장정보, 상태}\nTTL: 24시간
|
||||
activate Cache
|
||||
Cache --> Service: 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
== 5단계: Kafka 이벤트 발행 ==
|
||||
|
||||
Service -> Kafka: 이벤트 발행\nTopic: event-topic\nEvent: EventDraftCreated\nPayload: {eventDraftId,\nuserId, objective,\ncreatedAt}
|
||||
activate Kafka
|
||||
note right of Kafka
|
||||
**Kafka Event Topic**
|
||||
- Topic: event-topic
|
||||
- Event: EventDraftCreated
|
||||
- 목적 선택 시 발행
|
||||
|
||||
**구독자**
|
||||
- Analytics Service (선택적)
|
||||
|
||||
**참고**
|
||||
- EventCreated는
|
||||
최종 승인 시 발행
|
||||
end note
|
||||
Kafka --> Service: ACK (발행 확인)
|
||||
deactivate Kafka
|
||||
|
||||
== 6단계: 응답 반환 ==
|
||||
|
||||
Service -> Service: 응답 DTO 생성
|
||||
Service --> Controller: EventDraftResponse\n{eventDraftId, objective,\nstoreName, status=DRAFT}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 200 OK\n{"eventDraftId": "draft-123",\n"objective": "신규 고객 유치",\n"storeName": "맛있는집",\n"status": "DRAFT"}
|
||||
deactivate Controller
|
||||
|
||||
note over Controller, Kafka
|
||||
**캐시 전략**
|
||||
- Key: draft:event:{eventDraftId}
|
||||
- TTL: 24시간
|
||||
- 캐시 히트 시: DB 조회 생략, 즉시 반환
|
||||
|
||||
**이벤트 발행 전략**
|
||||
- EventDraftCreated: 목적 선택 시 발행 (Analytics Service 선택적 구독)
|
||||
- EventCreated: 최종 승인 시 발행 (통계 초기화 시작)
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 0.3초 이내
|
||||
- P95 응답 시간: 0.5초 이내
|
||||
- Redis 캐시 조회: 0.05초 이내
|
||||
|
||||
**에러 코드**
|
||||
- EVENT_001: 유효하지 않은 목적
|
||||
- EVENT_002: 매장 정보 누락
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,54 @@
|
||||
@startuml event-상세조회
|
||||
!theme mono
|
||||
|
||||
title Event Service - 이벤트 상세 조회 (UFR-EVENT-060)
|
||||
|
||||
participant "EventController" as Controller <<C>>
|
||||
participant "EventService" as Service <<S>>
|
||||
participant "EventRepository" as Repo <<R>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
|
||||
note over Controller: GET /api/events/{id}
|
||||
Controller -> Service: getEventDetail(eventId, userId)
|
||||
activate Service
|
||||
|
||||
Service -> Cache: get("event:" + eventId)
|
||||
activate Cache
|
||||
|
||||
alt 캐시 히트
|
||||
Cache --> Service: Event data
|
||||
Service -> Service: validateAccess(userId, event)
|
||||
note right: 사용자 권한 검증
|
||||
Service --> Controller: EventDetailResponse
|
||||
|
||||
else 캐시 미스
|
||||
Cache --> Service: null
|
||||
deactivate Cache
|
||||
|
||||
Service -> Repo: findById(eventId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 상세 정보 조회\n(이벤트ID로 이벤트 정보,\n경품 정보, 배포 이력을\nJOIN하여 함께 조회)
|
||||
activate DB
|
||||
note right: JOIN으로\n경품 정보 및\n배포 이력 조회
|
||||
DB --> Repo: Event with prizes and distributions
|
||||
deactivate DB
|
||||
Repo --> Service: Event entity (with relations)
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: validateAccess(userId, event)
|
||||
|
||||
Service -> Cache: set("event:" + eventId, event, TTL=5분)
|
||||
activate Cache
|
||||
Cache --> Service: OK
|
||||
deactivate Cache
|
||||
end
|
||||
|
||||
Service --> Controller: EventDetailResponse\n{eventId, title, objective,\nprizes, period, status,\nchannels, distributionStatus,\ncreatedAt, publishedAt}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 200 OK\n{event: {...},\nprizes: [...],\ndistributionStatus: {...}}
|
||||
|
||||
note over Controller, DB: 상세 정보 포함:\n- 기본 정보 (제목, 목적, 기간, 상태)\n- 경품 정보\n- 참여 방법\n- 배포 채널 현황\n- 실시간 통계 (Analytics Service)\n\nAnalytics 통계는\n별도 API 호출
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,157 @@
|
||||
@startuml event-최종승인및배포
|
||||
!theme mono
|
||||
|
||||
title Event Service - 최종 승인 및 Distribution Service 동기 호출 (UFR-EVENT-050)
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
participant "Distribution Service" as DistSvc <<E>>
|
||||
participant "Kafka Producer" as Kafka <<E>>
|
||||
|
||||
note over Controller, Kafka
|
||||
**UFR-EVENT-050: 이벤트 최종 승인 및 배포**
|
||||
- 이벤트 준비 상태 검증
|
||||
- 이벤트 승인 및 Kafka 이벤트 발행
|
||||
- Distribution Service 동기 호출 (다중 채널 배포)
|
||||
- 이벤트 상태를 ACTIVE로 변경
|
||||
end note
|
||||
|
||||
Client -> Gateway: POST /api/events/{eventDraftId}/publish\n{"userId": 123,\n"selectedChannels": [\n "우리동네TV",\n "지니TV",\n "Instagram"\n]}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: POST /api/events/{eventDraftId}/publish
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(필수 필드, 채널 유효성)
|
||||
|
||||
Controller -> Service: publishEvent(eventDraftId, userId, selectedChannels)
|
||||
activate Service
|
||||
|
||||
== 1단계: 이벤트 초안 조회 및 검증 ==
|
||||
|
||||
Service -> Repo: findById(eventDraftId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID로 조회)
|
||||
activate DB
|
||||
DB --> Repo: EventDraft 엔티티\n{목적, 추천안, 콘텐츠, 상태}
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: validateOwnership(userId, eventDraft)
|
||||
note right
|
||||
소유권 검증:
|
||||
- 사용자ID와 초안 소유자 일치 확인
|
||||
- 권한 없으면 403 Forbidden
|
||||
end note
|
||||
|
||||
Service -> Service: validatePublishReady()
|
||||
note right
|
||||
발행 준비 검증:
|
||||
- 목적 선택 완료
|
||||
- AI 추천 선택 완료
|
||||
- 콘텐츠 선택 완료
|
||||
- 배포 채널 최소 1개 선택
|
||||
end note
|
||||
|
||||
== 2단계: 이벤트 승인 ==
|
||||
|
||||
Service -> Repo: updateStatus(eventDraftId, APPROVED)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 상태 업데이트\n(상태를 APPROVED로,\n승인일시를 현재 시각으로 저장)
|
||||
activate DB
|
||||
DB --> Repo: 업데이트 완료
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
== 3단계: Kafka 이벤트 발행 ==
|
||||
|
||||
Service -> Kafka: 이벤트 발행\nTopic: event-topic\nPayload: {eventId, userId, title,\nobjective, createdAt}
|
||||
activate Kafka
|
||||
note right
|
||||
Kafka Event Topic:
|
||||
- Topic: event-topic
|
||||
- Consumer: Analytics Service
|
||||
- Event Type: EventCreated
|
||||
end note
|
||||
Kafka --> Service: ACK (발행 확인)
|
||||
deactivate Kafka
|
||||
|
||||
== 4단계: Distribution Service 동기 호출 ==
|
||||
|
||||
Service -> DistSvc: POST /api/distribution/distribute\n{"eventId": 123,\n"channels": [...],\n"content": {...}}
|
||||
activate DistSvc
|
||||
note right
|
||||
동기 호출 (Circuit Breaker 적용):
|
||||
- Timeout: 70초
|
||||
- 다중 채널 병렬 배포
|
||||
- Failure Rate: 50% 초과 시 OPEN
|
||||
end note
|
||||
|
||||
DistSvc -> DistSvc: distributeToChannels(eventId, channels)
|
||||
note right
|
||||
다중 채널 병렬 배포:
|
||||
- 우리동네TV
|
||||
- 링고비즈 (음성 안내)
|
||||
- 지니TV
|
||||
- Instagram
|
||||
- Naver Blog
|
||||
- Kakao Channel
|
||||
end note
|
||||
|
||||
DistSvc --> Service: DistributionResponse\n{"distributionId": "dist-123",\n"channelResults": [\n {"channel": "우리동네TV", "status": "SUCCESS"},\n {"channel": "지니TV", "status": "SUCCESS"},\n {"channel": "Instagram", "status": "SUCCESS"}\n]}
|
||||
deactivate DistSvc
|
||||
|
||||
== 5단계: 이벤트 활성화 ==
|
||||
|
||||
Service -> Repo: updateStatus(eventDraftId, ACTIVE)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 상태 업데이트\n(상태를 ACTIVE로,\n배포일시를 현재 시각으로 저장)
|
||||
activate DB
|
||||
DB --> Repo: 업데이트 완료
|
||||
deactivate DB
|
||||
Repo --> Service: Event entity
|
||||
deactivate Repo
|
||||
|
||||
== 6단계: 캐시 무효화 ==
|
||||
|
||||
Service -> Cache: 캐시 삭제\nKey: draft:event:{eventDraftId}
|
||||
activate Cache
|
||||
note right
|
||||
Redis 캐시 무효화:
|
||||
- 이벤트 초안 캐시 삭제
|
||||
- 활성화 완료 후 불필요
|
||||
end note
|
||||
Cache --> Service: 삭제 완료
|
||||
deactivate Cache
|
||||
|
||||
== 7단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: PublishResponse\n{eventId, status: ACTIVE,\ndistributionResults}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"eventId": 123,\n"status": "ACTIVE",\n"distributionResults": [...]}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n이벤트 배포 완료
|
||||
deactivate Gateway
|
||||
|
||||
note over Client, Kafka
|
||||
**배포 완료 후 처리**
|
||||
- Distribution Service는 배포 완료 후 Kafka에\n DistributionCompleted 이벤트 발행
|
||||
- Analytics Service가 구독하여 초기 통계 생성
|
||||
- 이벤트 상태: ACTIVE (참여자 접수 시작)
|
||||
|
||||
**성능 목표**
|
||||
- 응답 시간: 60초 이내 (Distribution Service 포함)
|
||||
- Distribution Service 타임아웃: 70초
|
||||
- 채널별 배포: 병렬 처리로 최적화
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,140 @@
|
||||
@startuml event-추천결과조회
|
||||
!theme mono
|
||||
|
||||
title Event Service - AI 추천 결과 폴링 조회
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "JobManager" as JobMgr <<Component>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
|
||||
note over Controller, Cache
|
||||
**폴링 방식 Job 상태 조회**
|
||||
- 최대 30초 동안 폴링 (2초 간격)
|
||||
- Job 상태: PENDING → PROCESSING → COMPLETED
|
||||
- AI 추천 결과: Redis에 저장 (TTL: 24시간)
|
||||
end note
|
||||
|
||||
Client -> Gateway: GET /api/events/jobs/{jobId}/status
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: GET /api/events/jobs/{jobId}/status
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: getJobStatus(jobId)
|
||||
activate Service
|
||||
|
||||
Service -> JobMgr: getJobStatus(jobId)
|
||||
activate JobMgr
|
||||
|
||||
JobMgr -> Cache: Job 상태 조회\nKey: job:{jobId}
|
||||
activate Cache
|
||||
|
||||
alt Job 데이터 존재
|
||||
Cache --> JobMgr: Job 데이터\n{status, eventDraftId,\ntype, createdAt}
|
||||
deactivate Cache
|
||||
|
||||
alt status = COMPLETED
|
||||
JobMgr -> Cache: AI 추천 결과 조회\nKey: ai:recommendation:{eventDraftId}
|
||||
activate Cache
|
||||
Cache --> JobMgr: AI 추천 결과\n{트렌드분석, 3가지추천안}
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nrecommendations: {...}}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: COMPLETED, recommendations}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"recommendations": [\n {"title": "저비용 추천안",\n "prize": "커피쿠폰",\n "method": "QR코드 스캔",\n "cost": "50만원",\n "roi": "150%"},\n {"title": "중비용 추천안",\n "prize": "상품권",\n "method": "SNS 공유",\n "cost": "100만원",\n "roi": "200%"},\n {"title": "고비용 추천안",\n "prize": "경품 추첨",\n "method": "설문 참여",\n "cost": "200만원",\n "roi": "300%"}\n]}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\nAI 추천 결과 반환
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**프론트엔드 처리**
|
||||
- 3가지 추천안 카드 표시
|
||||
- 사용자가 추천안 선택
|
||||
- 트렌드 분석 정보 표시
|
||||
end note
|
||||
|
||||
else status = PROCESSING 또는 PENDING
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: PROCESSING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "AI가 분석 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n진행 중 상태
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**폴링 재시도**
|
||||
- 2초 후 재요청
|
||||
- 최대 30초 (15회)
|
||||
end note
|
||||
|
||||
else status = FAILED
|
||||
JobMgr -> Cache: 에러 정보 조회\nKey: job:{jobId}:error
|
||||
activate Cache
|
||||
Cache --> JobMgr: 에러 메시지
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: FAILED, error}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "AI 분석 실패",\n"message": "다시 시도해주세요"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n실패 상태
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**실패 처리**
|
||||
- 에러 메시지 표시
|
||||
- "다시 분석" 버튼 제공
|
||||
end note
|
||||
end
|
||||
|
||||
else Job 데이터 없음
|
||||
Cache --> JobMgr: null (캐시 미스)
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: throw NotFoundException\n("Job을 찾을 수 없습니다")
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: NotFoundException
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job을 찾을 수 없습니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 404 Not Found
|
||||
deactivate Gateway
|
||||
end
|
||||
|
||||
note over Controller, Cache
|
||||
**폴링 전략**
|
||||
- 간격: 2초
|
||||
- 최대 시간: 30초 (15회)
|
||||
- Timeout 시: 사용자에게 알림 + "다시 분석" 옵션
|
||||
|
||||
**Redis 캐시**
|
||||
- Job 상태: TTL 1시간
|
||||
- AI 추천 결과: TTL 24시간
|
||||
|
||||
**성능 목표**
|
||||
- 평균 AI 분석 시간: 2분 이내
|
||||
- P95 AI 분석 시간: 4분 이내
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,116 @@
|
||||
@startuml event-추천안선택
|
||||
!theme mono
|
||||
|
||||
title Event Service - 선택한 AI 추천안 저장 (UFR-EVENT-040)
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
|
||||
note over Controller, Cache
|
||||
**UFR-EVENT-040: AI 추천안 선택 및 저장**
|
||||
- 사용자가 3가지 추천안 중 하나를 선택
|
||||
- 선택된 추천안을 이벤트 초안에 적용
|
||||
- Redis 캐시에서 AI 추천 결과 삭제
|
||||
end note
|
||||
|
||||
Client -> Gateway: PUT /api/events/drafts/{eventDraftId}/recommendation\n{"userId": 123,\n"selectedIndex": 1,\n"recommendation": {...}}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: PUT /api/events/drafts/{eventDraftId}/recommendation
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(필수 필드, 추천안 유효성)
|
||||
|
||||
Controller -> Service: updateEventRecommendation(eventDraftId, userId,\nselectedRecommendation)
|
||||
activate Service
|
||||
|
||||
== 1단계: 이벤트 초안 조회 및 검증 ==
|
||||
|
||||
Service -> Repo: findById(eventDraftId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID로 조회)
|
||||
activate DB
|
||||
DB --> Repo: EventDraft 엔티티\n{목적, 매장정보, 상태}
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: validateOwnership(userId, eventDraft)
|
||||
note right
|
||||
소유권 검증:
|
||||
- 사용자ID와 초안 소유자 일치 확인
|
||||
- 권한 없으면 403 Forbidden
|
||||
end note
|
||||
|
||||
Service -> Service: validateRecommendation(selectedRecommendation)
|
||||
note right
|
||||
추천안 유효성 검증:
|
||||
- 필수 필드 존재 여부
|
||||
- 비용/ROI 값 타당성
|
||||
end note
|
||||
|
||||
== 2단계: 추천안 적용 ==
|
||||
|
||||
Service -> Service: applyRecommendation(eventDraft, selectedRecommendation)
|
||||
note right
|
||||
추천안 적용:
|
||||
- 이벤트 제목
|
||||
- 경품 정보
|
||||
- 참여 방법
|
||||
- 예상 비용
|
||||
- 예상 ROI
|
||||
- 홍보 문구
|
||||
end note
|
||||
|
||||
== 3단계: DB 저장 ==
|
||||
|
||||
Service -> Repo: update(eventDraft)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 업데이트\n(선택된 추천안 정보 저장:\n제목, 경품, 참여방법,\n예상비용, 예상ROI,\n수정일시)
|
||||
activate DB
|
||||
DB --> Repo: 업데이트 완료
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
== 4단계: 캐시 무효화 ==
|
||||
|
||||
Service -> Cache: 캐시 삭제\nKey: ai:recommendation:{eventDraftId}
|
||||
activate Cache
|
||||
note right
|
||||
Redis 캐시 무효화:
|
||||
- AI 추천 결과 삭제
|
||||
- 선택 완료 후 불필요
|
||||
end note
|
||||
Cache --> Service: 삭제 완료
|
||||
deactivate Cache
|
||||
|
||||
== 5단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: EventRecommendationResponse\n{eventDraftId, selectedRecommendation}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"eventDraftId": 123,\n"status": "추천안 선택 완료",\n"selectedRecommendation": {...}}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n추천안 선택 완료
|
||||
deactivate Gateway
|
||||
|
||||
note over Client, Cache
|
||||
**저장 내용**
|
||||
- 최종 선택된 추천안만 Event DB에 저장
|
||||
- Redis에 저장된 3가지 추천안은 선택 후 삭제
|
||||
- 다음 단계: 콘텐츠 생성 (이미지 선택)
|
||||
|
||||
**성능 목표**
|
||||
- 응답 시간: 0.5초 이내
|
||||
- DB 업데이트: 0.1초
|
||||
- 캐시 삭제: 0.01초
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,118 @@
|
||||
@startuml event-콘텐츠선택
|
||||
!theme mono
|
||||
|
||||
title Content Service - 선택한 콘텐츠 저장 (UFR-CONT-020)
|
||||
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ContentController" as Controller <<API Layer>>
|
||||
participant "ContentService" as Service <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
|
||||
note over Controller, Cache
|
||||
**UFR-CONT-020: 콘텐츠 선택 및 편집 저장**
|
||||
- 사용자가 3가지 이미지 스타일 중 하나 선택
|
||||
- 선택된 이미지에 텍스트/색상 편집 적용
|
||||
- 편집된 콘텐츠를 이벤트 초안에 저장
|
||||
end note
|
||||
|
||||
Client -> Gateway: PUT /api/content/{eventDraftId}/select\n{"userId": 123,\n"selectedImageUrl": "https://cdn.../fancy.png",\n"editedContent": {...}}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: PUT /api/content/{eventDraftId}/select
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(필수 필드, URL 유효성)
|
||||
|
||||
Controller -> Service: updateEventContent(eventDraftId, userId,\nselectedImageUrl, editedContent)
|
||||
activate Service
|
||||
|
||||
== 1단계: 이벤트 초안 조회 및 검증 ==
|
||||
|
||||
Service -> Repo: findById(eventDraftId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID로 조회)
|
||||
activate DB
|
||||
DB --> Repo: EventDraft 엔티티\n{목적, 추천안, 상태}
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: validateOwnership(userId, eventDraft)
|
||||
note right
|
||||
소유권 검증:
|
||||
- 사용자ID와 초안 소유자 일치 확인
|
||||
- 권한 없으면 403 Forbidden
|
||||
end note
|
||||
|
||||
Service -> Service: validateImageUrl(selectedImageUrl)
|
||||
note right
|
||||
이미지 URL 검증:
|
||||
- URL 형식 유효성
|
||||
- CDN 경로 확인
|
||||
- 이미지 존재 여부
|
||||
end note
|
||||
|
||||
== 2단계: 콘텐츠 편집 적용 ==
|
||||
|
||||
Service -> Service: applyContentEdits(eventDraft, editedContent)
|
||||
note right
|
||||
편집 내용 적용:
|
||||
- 제목 텍스트
|
||||
- 경품 정보 텍스트
|
||||
- 참여 안내 텍스트
|
||||
- 배경색
|
||||
- 텍스트 색상
|
||||
- 강조 색상
|
||||
end note
|
||||
|
||||
== 3단계: DB 저장 ==
|
||||
|
||||
Service -> Repo: update(eventDraft)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 업데이트\n(선택된 이미지 URL,\n편집된 제목/텍스트,\n배경색/텍스트색,\n수정일시 저장)
|
||||
activate DB
|
||||
DB --> Repo: 업데이트 완료
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
== 4단계: 캐시 무효화 ==
|
||||
|
||||
Service -> Cache: 캐시 삭제\nKey: content:image:{eventDraftId}
|
||||
activate Cache
|
||||
note right
|
||||
Redis 캐시 무효화:
|
||||
- 이미지 URL 캐시 삭제
|
||||
- 선택 완료 후 불필요
|
||||
end note
|
||||
Cache --> Service: 삭제 완료
|
||||
deactivate Cache
|
||||
|
||||
== 5단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: EventContentResponse\n{eventDraftId, selectedImageUrl,\neditedContent}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"eventDraftId": 123,\n"status": "콘텐츠 선택 완료",\n"selectedImageUrl": "...",\n"editedContent": {...}}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n콘텐츠 선택 완료
|
||||
deactivate Gateway
|
||||
|
||||
note over Client, Cache
|
||||
**저장 내용**
|
||||
- 선택된 이미지 URL
|
||||
- 편집된 텍스트 (제목, 경품 정보, 참여 안내)
|
||||
- 편집된 색상 (배경색, 텍스트색, 강조색)
|
||||
- 다음 단계: 최종 승인 및 배포
|
||||
|
||||
**성능 목표**
|
||||
- 응답 시간: 0.5초 이내
|
||||
- DB 업데이트: 0.1초
|
||||
- 캐시 삭제: 0.01초
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,164 @@
|
||||
@startuml participation-당첨자추첨
|
||||
!theme mono
|
||||
|
||||
title Participation Service - 당첨자 추첨 내부 시퀀스
|
||||
|
||||
actor "사장님" as Owner
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ParticipationController" as Controller
|
||||
participant "ParticipationService" as Service
|
||||
participant "LotteryAlgorithm" as Lottery
|
||||
participant "ParticipantRepository" as Repo
|
||||
participant "DrawLogRepository" as LogRepo
|
||||
database "Participation DB" as DB
|
||||
|
||||
== UFR-PART-030: 당첨자 추첨 ==
|
||||
|
||||
Owner -> Gateway: POST /api/v1/events/{eventId}/draw-winners\n{winnerCount, visitBonus, algorithm}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Gateway: JWT 토큰 검증\n- 토큰 유효성 확인\n- 사장님 권한 확인
|
||||
|
||||
alt JWT 검증 실패
|
||||
Gateway --> Owner: 401 Unauthorized
|
||||
deactivate Gateway
|
||||
else JWT 검증 성공
|
||||
|
||||
Gateway -> Controller: POST /participations/draw-winners\n{eventId, winnerCount, visitBonus}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 데이터 유효성 검증\n- eventId 필수\n- winnerCount > 0\n- winnerCount <= 참여자 수
|
||||
|
||||
alt 유효성 검증 실패
|
||||
Controller --> Gateway: 400 Bad Request
|
||||
Gateway --> Owner: 400 Bad Request
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 유효성 검증 성공
|
||||
|
||||
Controller -> Service: drawWinners(eventId, winnerCount, visitBonus)
|
||||
activate Service
|
||||
|
||||
Service -> Service: 이벤트 상태 확인\n- 이벤트 종료 여부\n- 이미 추첨 완료 여부
|
||||
|
||||
Service -> LogRepo: findByEventId(eventId)
|
||||
activate LogRepo
|
||||
LogRepo -> DB: 추첨 로그 조회\n(이벤트ID로 조회)
|
||||
activate DB
|
||||
DB --> LogRepo: 추첨 로그 조회
|
||||
deactivate DB
|
||||
LogRepo --> Service: Optional<DrawLog>
|
||||
deactivate LogRepo
|
||||
|
||||
alt 이미 추첨 완료
|
||||
Service --> Controller: AlreadyDrawnException
|
||||
Controller --> Gateway: 409 Conflict\n{message: "이미 추첨이 완료된 이벤트입니다"}
|
||||
Gateway --> Owner: 409 Conflict
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 추첨 가능 상태
|
||||
|
||||
Service -> Repo: findAllByEventIdAndIsWinner(eventId, false)
|
||||
activate Repo
|
||||
Repo -> DB: 미당첨 참여자 목록 조회\n(이벤트ID로 당첨되지 않은\n참여자 전체 조회,\n참여일시 오름차순 정렬)
|
||||
activate DB
|
||||
DB --> Repo: 전체 참여자 목록
|
||||
deactivate DB
|
||||
Repo --> Service: List<Participant>
|
||||
deactivate Repo
|
||||
|
||||
alt 참여자 수 부족
|
||||
Service --> Controller: InsufficientParticipantsException
|
||||
Controller --> Gateway: 400 Bad Request\n{message: "참여자 수가 부족합니다"}
|
||||
Gateway --> Owner: 400 Bad Request
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 추첨 진행
|
||||
|
||||
Service -> Lottery: executeLottery(participants, winnerCount, visitBonus)
|
||||
activate Lottery
|
||||
|
||||
note right of Lottery
|
||||
추첨 알고리즘:
|
||||
시간 복잡도: O(n log n)
|
||||
공간 복잡도: O(n)
|
||||
|
||||
1. 난수 생성 (Crypto.randomBytes)
|
||||
2. 매장 방문 가산점 적용 (옵션)
|
||||
- 방문 고객: 가중치 2배
|
||||
- 비방문 고객: 가중치 1배
|
||||
3. Fisher-Yates Shuffle
|
||||
- 가중치 기반 확률 분포
|
||||
- 무작위 섞기
|
||||
4. 상위 N명 선정
|
||||
end note
|
||||
|
||||
Lottery -> Lottery: Step 1: 난수 시드 생성\n- Crypto.randomBytes(32)\n- 예측 불가능한 난수 보장
|
||||
|
||||
Lottery -> Lottery: Step 2: 가산점 적용\n- visitBonus = true일 경우\n- 매장 방문 경로 참여자 가중치 증가
|
||||
|
||||
Lottery -> Lottery: Step 3: Fisher-Yates Shuffle\n- 가중치 기반 확률 분포\n- O(n) 시간 복잡도
|
||||
|
||||
Lottery -> Lottery: Step 4: 당첨자 선정\n- 상위 winnerCount명 추출
|
||||
|
||||
Lottery --> Service: List<Participant> 당첨자 목록
|
||||
deactivate Lottery
|
||||
|
||||
Service -> Service: DB 트랜잭션 시작
|
||||
|
||||
alt DB 저장 실패 시
|
||||
note right of Service
|
||||
트랜잭션 롤백 처리:
|
||||
- 당첨자 업데이트 취소
|
||||
- 추첨 로그 저장 취소
|
||||
- 재시도 가능 상태 유지
|
||||
end note
|
||||
end
|
||||
|
||||
Service -> Repo: updateWinners(winnerIds)
|
||||
activate Repo
|
||||
Repo -> DB: 당첨자 정보 업데이트\n(당첨 여부를 true로,\n당첨 일시를 현재 시각으로 설정,\n대상: 선정된 참여자ID 목록)
|
||||
activate DB
|
||||
DB --> Repo: 업데이트 완료
|
||||
deactivate DB
|
||||
Repo --> Service: 업데이트 건수
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: DrawLog 엔티티 생성\n- drawLogId (UUID)\n- eventId\n- drawMethod: "RANDOM"\n- algorithm: "FISHER_YATES_SHUFFLE"\n- visitBonusApplied\n- winnerCount\n- drawnAt (현재시각)
|
||||
|
||||
Service -> LogRepo: save(drawLog)
|
||||
activate LogRepo
|
||||
LogRepo -> DB: 추첨 로그 저장\n(추첨로그ID, 이벤트ID,\n추첨방법, 알고리즘,\n가산점적용여부, 당첨인원,\n추첨일시 저장)
|
||||
activate DB
|
||||
note right of DB
|
||||
추첨 로그 저장:
|
||||
- 추첨 일시 기록
|
||||
- 알고리즘 버전 기록
|
||||
- 가산점 적용 여부
|
||||
- 감사 추적 목적
|
||||
end note
|
||||
DB --> LogRepo: 로그 저장 완료
|
||||
deactivate DB
|
||||
LogRepo --> Service: DrawLog 엔티티
|
||||
deactivate LogRepo
|
||||
|
||||
Service -> Service: DB 트랜잭션 커밋
|
||||
|
||||
Service --> Controller: DrawWinnersResponse\n{당첨자목록, 추첨로그ID}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{winners[], drawLogId, message}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Owner: 200 OK
|
||||
deactivate Gateway
|
||||
|
||||
Owner -> Owner: 당첨자 목록 화면 표시\n- 당첨자 정보 테이블\n- 재추첨 버튼\n- 추첨 완료 메시지
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,129 @@
|
||||
@startuml participation-이벤트참여
|
||||
!theme mono
|
||||
|
||||
title Participation Service - 이벤트 참여 내부 시퀀스
|
||||
|
||||
actor "고객" as Customer
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ParticipationController" as Controller
|
||||
participant "ParticipationService" as Service
|
||||
participant "ParticipantRepository" as Repo
|
||||
database "Participation DB" as DB
|
||||
participant "KafkaProducer" as Kafka
|
||||
database "Redis Cache<<E>>" as Cache
|
||||
|
||||
== UFR-PART-010: 이벤트 참여 ==
|
||||
|
||||
Customer -> Gateway: POST /api/v1/participations\n{name, phone, eventId, entryPath, consent}
|
||||
activate Gateway
|
||||
|
||||
note right of Gateway
|
||||
비회원 참여 가능
|
||||
JWT 검증 불필요
|
||||
end note
|
||||
|
||||
Gateway -> Controller: POST /participations/register\n{name, phone, eventId, entryPath, consent}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 데이터 유효성 검증\n- 이름 2자 이상\n- 전화번호 형식 (정규식)\n- 개인정보 동의 필수
|
||||
|
||||
alt 유효성 검증 실패
|
||||
Controller --> Gateway: 400 Bad Request\n{message: "유효성 오류"}
|
||||
Gateway --> Customer: 400 Bad Request
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 유효성 검증 성공
|
||||
|
||||
Controller -> Service: registerParticipant(request)
|
||||
activate Service
|
||||
|
||||
Service -> Cache: GET duplicate_check:{eventId}:{phone}
|
||||
activate Cache
|
||||
Cache --> Service: 캐시 확인 결과
|
||||
deactivate Cache
|
||||
|
||||
alt 캐시 HIT: 중복 참여
|
||||
Service --> Controller: DuplicateParticipationException
|
||||
Controller --> Gateway: 409 Conflict\n{message: "이미 참여하신 이벤트입니다"}
|
||||
Gateway --> Customer: 409 Conflict
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 캐시 MISS: DB 조회
|
||||
|
||||
Service -> Repo: findByEventIdAndPhoneNumber(eventId, phone)
|
||||
activate Repo
|
||||
Repo -> DB: 참여자 중복 확인\n(이벤트ID, 전화번호로 조회)
|
||||
activate DB
|
||||
DB --> Repo: 조회 결과
|
||||
deactivate DB
|
||||
Repo --> Service: Optional<Participant>
|
||||
deactivate Repo
|
||||
|
||||
alt DB에 중복 참여 존재
|
||||
Service -> Cache: SET duplicate_check:{eventId}:{phone} = true\nTTL: 7일
|
||||
activate Cache
|
||||
Cache --> Service: 캐시 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
Service --> Controller: DuplicateParticipationException
|
||||
Controller --> Gateway: 409 Conflict\n{message: "이미 참여하신 이벤트입니다"}
|
||||
Gateway --> Customer: 409 Conflict
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 신규 참여: 저장 진행
|
||||
|
||||
Service -> Service: 응모 번호 생성\n- UUID 기반\n- 형식: EVT-{timestamp}-{random}
|
||||
|
||||
Service -> Service: Participant 엔티티 생성\n- participantId (UUID)\n- eventId\n- name, phoneNumber\n- entryPath\n- applicationNumber (응모번호)\n- participatedAt (현재시각)
|
||||
|
||||
Service -> Repo: save(participant)
|
||||
activate Repo
|
||||
Repo -> DB: 참여자 정보 저장\n(참여자ID, 이벤트ID, 이름, 전화번호,\n참여경로, 응모번호, 참여일시,\n마케팅동의여부)
|
||||
activate DB
|
||||
DB --> Repo: 저장 완료
|
||||
deactivate DB
|
||||
Repo --> Service: Participant 엔티티 반환
|
||||
deactivate Repo
|
||||
|
||||
note right of Service
|
||||
참여자 등록 완료 후
|
||||
캐싱 및 이벤트 발행
|
||||
end note
|
||||
|
||||
Service -> Cache: SET duplicate_check:{eventId}:{phone} = true\nTTL: 7일
|
||||
activate Cache
|
||||
Cache --> Service: 캐시 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
Service -> Kafka: Publish Event\n"ParticipantRegistered"\nTopic: participant-events
|
||||
activate Kafka
|
||||
note right of Kafka
|
||||
Event Payload:
|
||||
{
|
||||
"participantId": "UUID",
|
||||
"eventId": "UUID",
|
||||
"phoneNumber": "010-1234-5678",
|
||||
"entryPath": "SNS",
|
||||
"registeredAt": "2025-10-22T10:30:00Z"
|
||||
}
|
||||
end note
|
||||
Kafka --> Service: 이벤트 발행 완료
|
||||
deactivate Kafka
|
||||
|
||||
Service -> Service: 당첨 발표일 계산\n- 이벤트 종료일 + 3일
|
||||
|
||||
Service --> Controller: ParticipationResponse\n{응모번호, 당첨발표일, 참여완료메시지}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 201 Created\n{applicationNumber, drawDate, message}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Customer: 201 Created\n참여 완료 화면 표시
|
||||
deactivate Gateway
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,120 @@
|
||||
@startuml participation-참여자목록조회
|
||||
!theme mono
|
||||
|
||||
title Participation Service - 참여자 목록 조회 내부 시퀀스
|
||||
|
||||
actor "사장님" as Owner
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ParticipationController" as Controller
|
||||
participant "ParticipationService" as Service
|
||||
participant "ParticipantRepository" as Repo
|
||||
database "Participation DB" as DB
|
||||
database "Redis Cache<<E>>" as Cache
|
||||
|
||||
== UFR-PART-020: 참여자 목록 조회 ==
|
||||
|
||||
Owner -> Gateway: GET /api/v1/events/{eventId}/participants\n?entryPath={경로}&isWinner={당첨여부}\n&name={이름}&phone={전화번호}\n&page={페이지}&size={크기}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Gateway: JWT 토큰 검증\n- 토큰 유효성 확인\n- 사장님 권한 확인
|
||||
|
||||
alt JWT 검증 실패
|
||||
Gateway --> Owner: 401 Unauthorized
|
||||
deactivate Gateway
|
||||
else JWT 검증 성공
|
||||
|
||||
Gateway -> Controller: GET /participants\n{eventId, filters, pagination}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 파라미터 유효성 검증\n- eventId 필수\n- page >= 0\n- size: 10~100
|
||||
|
||||
alt 유효성 검증 실패
|
||||
Controller --> Gateway: 400 Bad Request
|
||||
Gateway --> Owner: 400 Bad Request
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 유효성 검증 성공
|
||||
|
||||
Controller -> Service: getParticipantList(eventId, filters, pageable)
|
||||
activate Service
|
||||
|
||||
Service -> Service: 캐시 키 생성\n- participant_list:{eventId}:{filters}:{page}
|
||||
|
||||
Service -> Cache: GET participant_list:{key}
|
||||
activate Cache
|
||||
Cache --> Service: 캐시 조회 결과
|
||||
deactivate Cache
|
||||
|
||||
alt 캐시 HIT
|
||||
Service --> Controller: ParticipantListResponse\n(캐시된 데이터)
|
||||
note right of Service
|
||||
캐시된 데이터 반환
|
||||
- TTL: 10분
|
||||
- 실시간 정확도 vs 성능 트레이드오프
|
||||
end note
|
||||
Controller --> Gateway: 200 OK\n{participants, totalElements, totalPages}
|
||||
Gateway --> Owner: 200 OK\n참여자 목록 표시
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
deactivate Gateway
|
||||
else 캐시 MISS: DB 조회
|
||||
|
||||
Service -> Service: 동적 쿼리 생성\n- 참여 경로 필터\n- 당첨 여부 필터\n- 이름/전화번호 검색
|
||||
|
||||
Service -> Repo: findParticipants(eventId, filters, pageable)
|
||||
activate Repo
|
||||
|
||||
Repo -> DB: 참여자 목록 조회\n(이벤트ID, 참여경로, 당첨여부,\n이름/전화번호 검색조건으로 필터링하여\n참여일시 내림차순으로 페이징 조회)
|
||||
activate DB
|
||||
|
||||
note right of DB
|
||||
동적 쿼리 조건:
|
||||
- entryPath 필터 (선택)
|
||||
- isWinner 필터 (선택)
|
||||
- name/phone 검색 (선택)
|
||||
- 페이지네이션 (필수)
|
||||
|
||||
필요 인덱스:
|
||||
idx_participants_event_filters
|
||||
(event_id, entry_path, is_winner, participated_at DESC)
|
||||
end note
|
||||
|
||||
DB --> Repo: 참여자 목록 결과셋
|
||||
deactivate DB
|
||||
|
||||
Repo -> DB: 전체 참여자 수 조회\n(동일한 필터 조건 적용)
|
||||
activate DB
|
||||
DB --> Repo: 전체 건수
|
||||
deactivate DB
|
||||
|
||||
Repo --> Service: Page<Participant>
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: DTO 변환\n- 전화번호 마스킹 (010-****-1234)\n- 응모번호 형식화\n- 당첨 여부 라벨 변환
|
||||
|
||||
Service -> Cache: SET participant_list:{key} = data\nTTL: 10분
|
||||
activate Cache
|
||||
note right of Cache
|
||||
캐시 저장:
|
||||
- TTL: 10분
|
||||
- 실시간 참여 반영과 성능 균형
|
||||
- 이벤트 참여 빈도 고려
|
||||
end note
|
||||
Cache --> Service: 캐시 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
Service --> Controller: ParticipantListResponse\n{participants[], totalElements, totalPages, currentPage}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{data, pagination}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Owner: 200 OK
|
||||
deactivate Gateway
|
||||
|
||||
Owner -> Owner: 참여자 목록 화면 표시\n- 테이블 형태\n- 페이지네이션\n- 필터/검색 UI
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,155 @@
|
||||
@startuml user-로그아웃
|
||||
!theme mono
|
||||
|
||||
title User Service - 로그아웃 내부 시퀀스 (UFR-USER-040)
|
||||
|
||||
actor Client
|
||||
participant "UserController" as Controller <<API Layer>>
|
||||
participant "AuthenticationService" as AuthService <<Business Layer>>
|
||||
participant "JwtTokenProvider" as JwtProvider <<Utility>>
|
||||
participant "Redis\nCache" as Redis <<E>>
|
||||
|
||||
note over Controller, Redis
|
||||
**UFR-USER-040: 로그아웃**
|
||||
- JWT 토큰 검증
|
||||
- Redis 세션 삭제
|
||||
- 클라이언트 측 토큰 삭제 (프론트엔드 처리)
|
||||
end note
|
||||
|
||||
Client -> Controller: POST /api/users/logout\nAuthorization: Bearer {JWT}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: @AuthenticationPrincipal\n(JWT에서 userId 추출)
|
||||
|
||||
Controller -> Controller: JWT 토큰 추출\n(Authorization 헤더에서)
|
||||
|
||||
Controller -> AuthService: logout(token, userId)
|
||||
activate AuthService
|
||||
|
||||
== 1단계: JWT 토큰 검증 ==
|
||||
|
||||
AuthService -> JwtProvider: validateToken(token)
|
||||
activate JwtProvider
|
||||
JwtProvider -> JwtProvider: JWT 서명 검증\n(만료 시간 확인)
|
||||
JwtProvider --> AuthService: boolean (유효 여부)
|
||||
deactivate JwtProvider
|
||||
|
||||
alt JWT 토큰 무효
|
||||
AuthService --> Controller: throw InvalidTokenException\n("유효하지 않은 토큰입니다")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_002",\n"error": "유효하지 않은 토큰입니다"}
|
||||
deactivate AuthService
|
||||
deactivate Controller
|
||||
|
||||
else JWT 토큰 유효
|
||||
|
||||
== 2단계: Redis 세션 삭제 ==
|
||||
|
||||
AuthService -> Redis: 세션 삭제\n(캐시키: user:session:{token})
|
||||
activate Redis
|
||||
Redis --> AuthService: 삭제된 키 개수 (0 또는 1)
|
||||
deactivate Redis
|
||||
|
||||
alt 세션 없음 (이미 로그아웃됨)
|
||||
note right of AuthService
|
||||
**멱등성 보장**
|
||||
- 세션이 없어도 로그아웃 성공으로 처리
|
||||
- 중복 로그아웃 요청에 안전
|
||||
end note
|
||||
else 세션 있음 (정상 로그아웃)
|
||||
note right of AuthService
|
||||
**세션 삭제 완료**
|
||||
- Redis에서 세션 정보 제거
|
||||
- JWT 토큰 무효화 (Blacklist 방식)
|
||||
end note
|
||||
end
|
||||
|
||||
== 3단계: JWT 토큰 Blacklist 추가 (선택적) ==
|
||||
|
||||
note right of AuthService
|
||||
**JWT Blacklist 전략**
|
||||
- 만료되지 않은 JWT 토큰을 강제로 무효화
|
||||
- Redis에 토큰을 Blacklist에 추가 (TTL: 남은 만료 시간)
|
||||
- API Gateway에서 Blacklist 확인
|
||||
|
||||
**API Gateway 연계 시나리오**
|
||||
1. 로그아웃: AuthService가 Blacklist에 토큰 추가
|
||||
2. 후속 API 요청: API Gateway가 Blacklist 확인
|
||||
- Redis GET jwt:blacklist:{token}
|
||||
- 존재하면: 401 Unauthorized 즉시 반환
|
||||
- 존재하지 않으면: 백엔드 서비스로 라우팅
|
||||
3. 만료 시간 도달: Redis TTL 만료로 자동 삭제
|
||||
end note
|
||||
|
||||
AuthService -> JwtProvider: getRemainingExpiration(token)
|
||||
activate JwtProvider
|
||||
JwtProvider -> JwtProvider: JWT Claims에서\nexp(만료 시간) 추출\n(현재 시간과 비교)
|
||||
JwtProvider --> AuthService: remainingSeconds
|
||||
deactivate JwtProvider
|
||||
|
||||
alt 남은 만료 시간 > 0
|
||||
AuthService -> Redis: 블랙리스트에 토큰 추가\n(캐시키: jwt:blacklist:{token},\n값: "revoked", TTL: 남은초)
|
||||
activate Redis
|
||||
Redis --> AuthService: Blacklist 추가 완료
|
||||
deactivate Redis
|
||||
end
|
||||
|
||||
== 4단계: 로그아웃 이벤트 발행 (선택적) ==
|
||||
|
||||
note right of AuthService
|
||||
**로그아웃 로깅 및 이벤트**
|
||||
- 감사 로그 기록: userId, timestamp, IP
|
||||
- 이벤트 발행: LOGOUT_SUCCESS
|
||||
- 분석 데이터 수집: 세션 지속 시간, 활동 통계
|
||||
end note
|
||||
|
||||
AuthService -> AuthService: 로그아웃 성공 로그 기록\n(userId, timestamp, sessionDuration)
|
||||
|
||||
AuthService ->> AuthService: publishEvent(LOGOUT_SUCCESS)
|
||||
note right of AuthService
|
||||
**이벤트 활용**
|
||||
- 비동기 이벤트 처리
|
||||
- 분석 시스템 연동
|
||||
- 감사 로그 저장소 전송
|
||||
end note
|
||||
|
||||
== 5단계: 응답 반환 ==
|
||||
|
||||
AuthService --> Controller: LogoutResponse\n(success: true)
|
||||
deactivate AuthService
|
||||
|
||||
Controller --> Client: 200 OK\n{"success": true,\n"message": "안전하게 로그아웃되었습니다"}
|
||||
deactivate Controller
|
||||
end
|
||||
|
||||
note over Controller, Redis
|
||||
**보안 처리**
|
||||
- JWT 토큰 Blacklist: 만료 전 토큰 강제 무효화
|
||||
- 멱등성 보장: 중복 로그아웃 요청에 안전
|
||||
- 세션 완전 삭제: Redis에서 세션 정보 제거
|
||||
- 감사 로그: userId, timestamp, IP, sessionDuration 기록
|
||||
|
||||
**API Gateway 연계**
|
||||
- Blacklist 확인: GET jwt:blacklist:{token}
|
||||
- 존재 시: 401 Unauthorized 즉시 반환
|
||||
- TTL 자동 관리: 만료 시간 도달 시 자동 삭제
|
||||
|
||||
**클라이언트 측 처리**
|
||||
- 프론트엔드: LocalStorage 또는 Cookie에서 JWT 토큰 삭제
|
||||
- 로그인 화면으로 리다이렉트
|
||||
- 모든 인증 헤더 제거
|
||||
|
||||
**성능 목표**
|
||||
- Redis 삭제 연산: O(1) 시간 복잡도
|
||||
- 평균 응답 시간: 0.1초 이내
|
||||
- P95 응답 시간: 0.2초 이내
|
||||
|
||||
**이벤트 처리**
|
||||
- LOGOUT_SUCCESS 이벤트 발행 (비동기)
|
||||
- 감사 로그 저장소 전송
|
||||
- 분석 데이터 수집
|
||||
|
||||
**에러 코드**
|
||||
- AUTH_002: JWT 토큰 무효
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,147 @@
|
||||
@startuml user-로그인
|
||||
!theme mono
|
||||
|
||||
title User Service - 로그인 내부 시퀀스 (UFR-USER-020)
|
||||
|
||||
actor Client
|
||||
participant "UserController" as Controller <<API Layer>>
|
||||
participant "UserService" as Service <<Business Layer>>
|
||||
participant "AuthenticationService" as AuthService <<Business Layer>>
|
||||
participant "UserRepository" as UserRepo <<Data Layer>>
|
||||
participant "PasswordEncoder" as PwdEncoder <<Utility>>
|
||||
participant "JwtTokenProvider" as JwtProvider <<Utility>>
|
||||
participant "Redis\nCache" as Redis <<E>>
|
||||
participant "User DB\n(PostgreSQL)" as UserDB <<E>>
|
||||
|
||||
note over Controller, UserDB
|
||||
**UFR-USER-020: 로그인**
|
||||
- 입력: 이메일, 비밀번호
|
||||
- 비밀번호 검증 (bcrypt compare)
|
||||
- JWT 토큰 발급
|
||||
- 세션 저장 (Redis)
|
||||
- 최종 로그인 시각 업데이트
|
||||
end note
|
||||
|
||||
Client -> Controller: POST /api/users/login\n{"email": "user@example.com",\n"password": "password123"}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 입력값 검증\n(필수 필드, 이메일 형식 확인)
|
||||
|
||||
Controller -> AuthService: authenticate(email, password)
|
||||
activate AuthService
|
||||
|
||||
== 1단계: 사용자 조회 ==
|
||||
|
||||
AuthService -> Service: findByEmail(email)
|
||||
activate Service
|
||||
Service -> UserRepo: findByEmail(email)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 이메일로 사용자 조회\n(사용자ID, 비밀번호해시, 역할,\n이름, 전화번호 조회)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 사용자 정보 반환 또는 없음
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: Optional<User>
|
||||
deactivate UserRepo
|
||||
Service --> AuthService: Optional<User>
|
||||
deactivate Service
|
||||
|
||||
alt 사용자 없음
|
||||
AuthService --> Controller: throw AuthenticationFailedException\n("이메일 또는 비밀번호를 확인해주세요")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "이메일 또는 비밀번호를\n확인해주세요"}
|
||||
deactivate AuthService
|
||||
deactivate Controller
|
||||
|
||||
else 사용자 존재
|
||||
|
||||
== 2단계: 비밀번호 검증 ==
|
||||
|
||||
AuthService -> PwdEncoder: matches(rawPassword, passwordHash)
|
||||
activate PwdEncoder
|
||||
PwdEncoder -> PwdEncoder: bcrypt compare\n(입력 비밀번호 vs 저장된 해시)
|
||||
PwdEncoder --> AuthService: boolean (일치 여부)
|
||||
deactivate PwdEncoder
|
||||
|
||||
alt 비밀번호 불일치
|
||||
AuthService --> Controller: throw AuthenticationFailedException\n("이메일 또는 비밀번호를 확인해주세요")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "이메일 또는 비밀번호를\n확인해주세요"}
|
||||
deactivate AuthService
|
||||
deactivate Controller
|
||||
|
||||
else 비밀번호 일치
|
||||
|
||||
== 3단계: JWT 토큰 생성 ==
|
||||
|
||||
AuthService -> JwtProvider: generateToken(userId, role)
|
||||
activate JwtProvider
|
||||
JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일)
|
||||
JwtProvider --> AuthService: JWT 토큰
|
||||
deactivate JwtProvider
|
||||
|
||||
== 4단계: 세션 저장 ==
|
||||
|
||||
AuthService -> Redis: 세션 정보 저장\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7일
|
||||
activate Redis
|
||||
Redis --> AuthService: 저장 완료
|
||||
deactivate Redis
|
||||
|
||||
== 5단계: 최종 로그인 시각 업데이트 (비동기) ==
|
||||
|
||||
AuthService ->> Service: updateLastLoginAt(userId)
|
||||
activate Service
|
||||
note right of Service
|
||||
**비동기 처리**
|
||||
- @Async 어노테이션 사용
|
||||
- 로그인 응답 지연 방지
|
||||
- 별도 스레드풀에서 실행
|
||||
end note
|
||||
Service ->> UserRepo: updateLastLoginAt(userId)
|
||||
activate UserRepo
|
||||
UserRepo ->> UserDB: 사용자 최종 로그인 시각 갱신\n(현재 시각으로 업데이트)
|
||||
activate UserDB
|
||||
UserDB -->> UserRepo: 업데이트 완료
|
||||
deactivate UserDB
|
||||
UserRepo -->> Service: void
|
||||
deactivate UserRepo
|
||||
Service -->> AuthService: void (비동기 완료)
|
||||
deactivate Service
|
||||
|
||||
note over AuthService, Service
|
||||
**비동기 화살표 설명**
|
||||
- `->>`: 비동기 호출 (호출 후 즉시 반환)
|
||||
- `-->>`: 비동기 응답 (별도 스레드에서 완료)
|
||||
end note
|
||||
|
||||
== 6단계: 응답 반환 ==
|
||||
|
||||
AuthService -> AuthService: 로그인 응답 DTO 생성
|
||||
AuthService --> Controller: LoginResponse\n{token, userId, userName, role, email}
|
||||
deactivate AuthService
|
||||
|
||||
Controller --> Client: 200 OK\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"}
|
||||
deactivate Controller
|
||||
end
|
||||
end
|
||||
|
||||
note over Controller, UserDB
|
||||
**보안 처리**
|
||||
- 비밀번호: bcrypt compare (원본 노출 안 됨)
|
||||
- 에러 메시지: 이메일/비밀번호 구분 없이 동일 메시지 반환 (Timing Attack 방어)
|
||||
- JWT 토큰: 7일 만료, 서버 세션과 동기화
|
||||
|
||||
**보안 강화 (향후 구현)**
|
||||
- Rate Limiting: IP당 5분에 5회 로그인 실패 시 임시 차단 (15분)
|
||||
- Account Lockout: 동일 계정 10회 실패 시 계정 잠금 (관리자 해제)
|
||||
- MFA: 2단계 인증 추가 (SMS/TOTP)
|
||||
- Anomaly Detection: 비정상 로그인 패턴 감지 (지역, 디바이스 변경)
|
||||
|
||||
**성능 최적화**
|
||||
- 최종 로그인 시각 업데이트: 비동기 처리 (@Async)
|
||||
- 평균 응답 시간: 0.5초 이내
|
||||
- P95 응답 시간: 1.0초 이내
|
||||
- Redis 세션 조회: 0.1초 이내
|
||||
|
||||
**에러 코드**
|
||||
- AUTH_001: 인증 실패 (이메일 또는 비밀번호 불일치)
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,233 @@
|
||||
@startuml user-프로필수정
|
||||
!theme mono
|
||||
|
||||
title User Service - 프로필 수정 내부 시퀀스 (UFR-USER-030)
|
||||
|
||||
actor Client
|
||||
participant "UserController" as Controller <<API Layer>>
|
||||
participant "UserService" as Service <<Business Layer>>
|
||||
participant "UserRepository" as UserRepo <<Data Layer>>
|
||||
participant "StoreRepository" as StoreRepo <<Data Layer>>
|
||||
participant "PasswordEncoder" as PwdEncoder <<Utility>>
|
||||
participant "Redis\nCache" as Redis <<E>>
|
||||
participant "User DB\n(PostgreSQL)" as UserDB <<E>>
|
||||
|
||||
note over Controller, UserDB
|
||||
**UFR-USER-030: 프로필 수정**
|
||||
- 기본 정보: 이름, 전화번호, 이메일
|
||||
- 매장 정보: 매장명, 업종, 주소, 영업시간
|
||||
- 비밀번호 변경 (현재 비밀번호 확인 필수)
|
||||
- 전화번호 변경 시 재인증 필요 (향후 구현)
|
||||
end note
|
||||
|
||||
Client -> Controller: PUT /api/users/profile\nAuthorization: Bearer {JWT}\n(UpdateProfileRequest DTO)
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: @AuthenticationPrincipal\n(JWT에서 userId 추출)
|
||||
|
||||
Controller -> Controller: @Valid 어노테이션 검증\n(이메일 형식, 필드 길이 등)
|
||||
|
||||
Controller -> Service: updateProfile(userId, UpdateProfileRequest)
|
||||
activate Service
|
||||
|
||||
== 1단계: 기존 사용자 정보 조회 ==
|
||||
|
||||
Service -> UserRepo: findById(userId)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 사용자ID로 사용자 조회\n(사용자 정보 조회)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 사용자 정보
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: User 엔티티
|
||||
deactivate UserRepo
|
||||
|
||||
alt 사용자 없음
|
||||
Service --> Controller: throw UserNotFoundException\n("사용자를 찾을 수 없습니다")
|
||||
Controller --> Client: 404 Not Found\n{"code": "USER_003",\n"error": "사용자를 찾을 수 없습니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
else 사용자 존재
|
||||
|
||||
== 2단계: 비밀번호 변경 요청 처리 ==
|
||||
|
||||
alt 비밀번호 변경 요청 O
|
||||
Service -> Service: 현재 비밀번호 검증 필요 확인
|
||||
|
||||
Service -> PwdEncoder: matches(currentPassword,\nuser.getPasswordHash())
|
||||
activate PwdEncoder
|
||||
PwdEncoder -> PwdEncoder: bcrypt compare
|
||||
PwdEncoder --> Service: boolean (일치 여부)
|
||||
deactivate PwdEncoder
|
||||
|
||||
alt 현재 비밀번호 불일치
|
||||
Service --> Controller: throw InvalidPasswordException\n("현재 비밀번호가 일치하지 않습니다")
|
||||
Controller --> Client: 400 Bad Request\n{"code": "USER_004",\n"error": "현재 비밀번호가\n일치하지 않습니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
else 현재 비밀번호 일치
|
||||
Service -> Service: 새 비밀번호 유효성 검증\n(8자 이상, 영문/숫자/특수문자)
|
||||
|
||||
Service -> PwdEncoder: encode(newPassword)
|
||||
activate PwdEncoder
|
||||
PwdEncoder -> PwdEncoder: bcrypt 해싱\n(Cost Factor 10)
|
||||
PwdEncoder --> Service: newPasswordHash
|
||||
deactivate PwdEncoder
|
||||
|
||||
Service -> Service: user.setPasswordHash(newPasswordHash)
|
||||
end
|
||||
end
|
||||
|
||||
== 3단계: 엔티티 수정 준비 (메모리상 변경) ==
|
||||
|
||||
note right of Service
|
||||
**JPA Dirty Checking**
|
||||
- 트랜잭션 시작 전 엔티티 수정 (메모리상)
|
||||
- 트랜잭션 커밋 시 변경 감지하여 UPDATE 자동 실행
|
||||
- 변경된 필드만 UPDATE 쿼리에 포함
|
||||
end note
|
||||
|
||||
alt 이름 변경
|
||||
Service -> Service: user.setName(newName)
|
||||
end
|
||||
|
||||
alt 전화번호 변경
|
||||
Service -> Service: user.setPhoneNumber(newPhoneNumber)
|
||||
note right of Service
|
||||
**향후 구현: 재인증 필요**
|
||||
- SMS 인증 또는 이메일 인증
|
||||
- 인증 완료 후에만 변경 반영
|
||||
end note
|
||||
end
|
||||
|
||||
alt 이메일 변경
|
||||
Service -> Service: user.setEmail(newEmail)
|
||||
end
|
||||
|
||||
== 4단계: 매장 정보 수정 준비 (메모리상 변경) ==
|
||||
|
||||
Service -> StoreRepo: findByUserId(userId)
|
||||
activate StoreRepo
|
||||
StoreRepo -> UserDB: 사용자ID로 매장 조회\n(매장 정보 조회)
|
||||
activate UserDB
|
||||
UserDB --> StoreRepo: 매장 정보
|
||||
deactivate UserDB
|
||||
StoreRepo --> Service: Store 엔티티
|
||||
deactivate StoreRepo
|
||||
|
||||
alt 매장명 변경
|
||||
Service -> Service: store.setStoreName(newStoreName)
|
||||
end
|
||||
|
||||
alt 업종 변경
|
||||
Service -> Service: store.setIndustry(newIndustry)
|
||||
end
|
||||
|
||||
alt 주소 변경
|
||||
Service -> Service: store.setAddress(newAddress)
|
||||
end
|
||||
|
||||
alt 영업시간 변경
|
||||
Service -> Service: store.setBusinessHours(newBusinessHours)
|
||||
end
|
||||
|
||||
== 5단계: 데이터베이스 트랜잭션 ==
|
||||
|
||||
Service -> UserDB: 트랜잭션 시작
|
||||
activate UserDB
|
||||
|
||||
note right of Service
|
||||
**Optimistic Locking**
|
||||
- @Version 필드로 동시 수정 감지
|
||||
- 다른 트랜잭션이 먼저 수정한 경우
|
||||
- OptimisticLockException 발생
|
||||
end note
|
||||
|
||||
Service -> UserRepo: save(user)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 사용자 정보 업데이트\n(이름, 전화번호, 이메일,\n비밀번호해시, 수정일시,\n버전 증가)\nOptimistic Lock 적용
|
||||
UserDB --> UserRepo: 업데이트 완료 (1 row affected)
|
||||
UserRepo --> Service: User 엔티티
|
||||
deactivate UserRepo
|
||||
|
||||
alt 동시성 충돌 (version 불일치)
|
||||
UserRepo --> Service: throw OptimisticLockException
|
||||
Service --> Controller: throw ConcurrentModificationException\n("다른 사용자가 수정 중입니다")
|
||||
Controller --> Client: 409 Conflict\n{"code": "USER_005",\n"error": "다른 세션에서 프로필을\n수정했습니다.\n새로고침 후 다시 시도하세요"}
|
||||
Service -> UserDB: 트랜잭션 롤백
|
||||
deactivate UserDB
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
else 정상 업데이트
|
||||
|
||||
Service -> StoreRepo: save(store)
|
||||
activate StoreRepo
|
||||
StoreRepo -> UserDB: 매장 정보 업데이트\n(매장명, 업종, 주소,\n영업시간, 수정일시,\n버전 증가)\nOptimistic Lock 적용
|
||||
UserDB --> StoreRepo: 업데이트 완료 (1 row affected)
|
||||
StoreRepo --> Service: Store 엔티티
|
||||
deactivate StoreRepo
|
||||
|
||||
Service -> UserDB: 트랜잭션 커밋
|
||||
UserDB --> Service: 트랜잭션 커밋 완료
|
||||
deactivate UserDB
|
||||
|
||||
== 6단계: 캐시 무효화 (선택적) ==
|
||||
|
||||
note right of Service
|
||||
**캐시 무효화 전략**
|
||||
- 세션 정보는 변경 없음 (JWT 유지)
|
||||
- 프로필 캐시가 있다면 무효화
|
||||
end note
|
||||
|
||||
alt 프로필 캐시 사용 중
|
||||
Service -> Redis: 프로필 캐시 삭제\n(캐시키: user:profile:{userId})
|
||||
activate Redis
|
||||
Redis --> Service: 캐시 삭제 완료
|
||||
deactivate Redis
|
||||
end
|
||||
|
||||
== 7단계: 응답 반환 ==
|
||||
|
||||
Service -> Service: 응답 DTO 생성\n(UpdateProfileResponse)
|
||||
Service --> Controller: UpdateProfileResponse\n(userId, userName, email,\nstoreId, storeName)
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 200 OK\n{"userId": 123,\n"userName": "홍길동",\n"email": "hong@example.com",\n"storeId": 456,\n"storeName": "맛있는집"}
|
||||
deactivate Controller
|
||||
end
|
||||
end
|
||||
|
||||
note over Controller, UserDB
|
||||
**Transaction Rollback 처리**
|
||||
- 트랜잭션 실패 시 자동 Rollback
|
||||
- User/Store UPDATE 중 하나라도 실패 시 전체 롤백
|
||||
- OptimisticLockException 발생 시 409 Conflict 반환
|
||||
|
||||
**동시성 제어**
|
||||
- Optimistic Locking: @Version 필드로 동시 수정 감지
|
||||
- 충돌 감지 시: 409 Conflict 반환 (사용자에게 재시도 안내)
|
||||
- Lost Update 방지: version 필드 자동 증가
|
||||
|
||||
**보안 처리**
|
||||
- 비밀번호 변경: 현재 비밀번호 확인 필수
|
||||
- JWT 인증: Controller에서 @AuthenticationPrincipal로 userId 추출
|
||||
- 권한 검증: 본인만 수정 가능
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 0.3초 이내
|
||||
- P95 응답 시간: 0.5초 이내
|
||||
- 트랜잭션 격리 수준: READ_COMMITTED
|
||||
|
||||
**향후 개선사항**
|
||||
- 전화번호 변경: SMS/이메일 재인증 구현
|
||||
- 이메일 변경: 이메일 인증 구현
|
||||
- 변경 이력 추적: Audit Log 기록
|
||||
|
||||
**에러 코드**
|
||||
- USER_003: 사용자 없음
|
||||
- USER_004: 현재 비밀번호 불일치
|
||||
- USER_005: 동시성 충돌 (다른 세션에서 수정)
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,149 @@
|
||||
@startuml user-회원가입
|
||||
!theme mono
|
||||
|
||||
title User Service - 회원가입 내부 시퀀스 (UFR-USER-010)
|
||||
|
||||
participant "UserController" as Controller <<API Layer>>
|
||||
participant "UserService" as Service <<Business Layer>>
|
||||
participant "UserRepository" as UserRepo <<Data Layer>>
|
||||
participant "StoreRepository" as StoreRepo <<Data Layer>>
|
||||
participant "PasswordEncoder" as PwdEncoder <<Utility>>
|
||||
participant "JwtTokenProvider" as JwtProvider <<Utility>>
|
||||
participant "Redis\nCache" as Redis <<E>>
|
||||
participant "User DB\n(PostgreSQL)" as UserDB <<E>>
|
||||
actor Client
|
||||
|
||||
note over Controller, UserDB
|
||||
**UFR-USER-010: 회원가입**
|
||||
- 기본 정보: 이름, 전화번호, 이메일, 비밀번호
|
||||
- 매장 정보: 매장명, 업종, 주소, 영업시간, 사업자번호
|
||||
- 이메일/전화번호 중복 검사
|
||||
- 트랜잭션 처리
|
||||
- JWT 토큰 발급
|
||||
end note
|
||||
|
||||
Client -> Controller: POST /api/users/register\n{"name": "홍길동",\n"phoneNumber": "01012345678",\n"email": "hong@example.com",\n"password": "password123"}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 입력값 검증\n(이메일 형식, 비밀번호 8자 이상 등)
|
||||
|
||||
Controller -> Service: register(RegisterRequest)
|
||||
activate Service
|
||||
|
||||
== 1단계: 이메일 중복 확인 ==
|
||||
|
||||
Service -> UserRepo: findByEmail(email)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 이메일로 사용자 조회\n(중복 가입 확인)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 조회 결과 반환 또는 없음
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: Optional<User>
|
||||
deactivate UserRepo
|
||||
|
||||
alt 이메일 중복 존재
|
||||
Service --> Controller: throw DuplicateEmailException\n("이미 가입된 이메일입니다")
|
||||
Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"message": "이미 가입된 이메일입니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
else 이메일 신규
|
||||
|
||||
== 2단계: 전화번호 중복 확인 ==
|
||||
|
||||
Service -> UserRepo: findByPhoneNumber(phoneNumber)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 조회 결과 반환 또는 없음
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: Optional<User>
|
||||
deactivate UserRepo
|
||||
|
||||
alt 전화번호 중복 존재
|
||||
Service --> Controller: throw DuplicatePhoneException\n("이미 가입된 전화번호입니다")
|
||||
Controller --> Client: 400 Bad Request\n{"code": "USER_002",\n"message": "이미 가입된 전화번호입니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
else 신규 사용자
|
||||
|
||||
== 3단계: 비밀번호 해싱 ==
|
||||
|
||||
Service -> PwdEncoder: encode(rawPassword)
|
||||
activate PwdEncoder
|
||||
PwdEncoder -> PwdEncoder: bcrypt 해싱\n(Cost Factor 10)
|
||||
PwdEncoder --> Service: passwordHash
|
||||
deactivate PwdEncoder
|
||||
|
||||
== 4단계: 데이터베이스 트랜잭션 ==
|
||||
|
||||
Service -> UserDB: 트랜잭션 시작
|
||||
activate UserDB
|
||||
|
||||
Service -> UserRepo: save(User)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시)\n저장 후 사용자ID 반환
|
||||
UserDB --> UserRepo: 생성된 사용자ID
|
||||
UserRepo --> Service: User 엔티티\n(userId 포함)
|
||||
deactivate UserRepo
|
||||
|
||||
Service -> StoreRepo: save(Store)
|
||||
activate StoreRepo
|
||||
StoreRepo -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 사업자번호, 영업시간)\n저장 후 매장ID 반환
|
||||
UserDB --> StoreRepo: 생성된 매장ID
|
||||
StoreRepo --> Service: Store 엔티티\n(storeId 포함)
|
||||
deactivate StoreRepo
|
||||
|
||||
Service -> UserDB: 트랜잭션 커밋
|
||||
UserDB --> Service: 커밋 완료
|
||||
deactivate UserDB
|
||||
|
||||
== 5단계: JWT 토큰 생성 ==
|
||||
|
||||
Service -> JwtProvider: generateToken(userId, role)
|
||||
activate JwtProvider
|
||||
JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일)
|
||||
JwtProvider --> Service: JWT 토큰
|
||||
deactivate JwtProvider
|
||||
|
||||
== 6단계: 세션 저장 ==
|
||||
|
||||
Service -> Redis: 세션 정보 저장\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7일
|
||||
activate Redis
|
||||
Redis --> Service: 저장 완료
|
||||
deactivate Redis
|
||||
|
||||
== 7단계: 응답 반환 ==
|
||||
|
||||
Service -> Service: 회원가입 응답 DTO 생성
|
||||
Service --> Controller: RegisterResponse\n{token, userId, userName, storeId, storeName}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 201 Created\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "홍길동",\n"storeId": 456,\n"storeName": "맛있는집"}
|
||||
deactivate Controller
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
note over Controller, UserDB
|
||||
**Transaction Rollback 처리**
|
||||
- 트랜잭션 실패 시 자동 Rollback
|
||||
- User/Store INSERT 중 하나라도 실패 시 전체 롤백
|
||||
- 예외: DataAccessException, ConstraintViolationException
|
||||
|
||||
**보안 처리**
|
||||
- 비밀번호: bcrypt 해싱 (Cost Factor 10)
|
||||
- JWT 토큰: 7일 만료, 서버 세션과 동기화
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 1.0초 이내
|
||||
- P95 응답 시간: 1.5초 이내
|
||||
- 트랜잭션 처리: 0.5초 이내
|
||||
|
||||
**에러 코드**
|
||||
- USER_001: 이메일 중복
|
||||
- USER_002: 전화번호 중복
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,304 @@
|
||||
# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 외부 시퀀스 설계
|
||||
|
||||
## 문서 정보
|
||||
- **작성일**: 2025-10-22
|
||||
- **작성자**: System Architect
|
||||
- **버전**: 1.0
|
||||
- **관련 문서**:
|
||||
- [유저스토리](../../../userstory.md)
|
||||
- [논리 아키텍처](../../logical/logical-architecture.md)
|
||||
- [UI/UX 설계서](../../../uiux/uiux.md)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
본 문서는 KT AI 기반 소상공인 이벤트 자동 생성 서비스의 **외부 시퀀스 설계**를 정의합니다.
|
||||
외부 시퀀스는 서비스 간의 상호작용과 데이터 흐름을 표현하며, 유저스토리와 논리 아키텍처를 기반으로 설계되었습니다.
|
||||
|
||||
### 설계 원칙
|
||||
1. **유저스토리 기반**: 20개 유저스토리와 정확히 매칭
|
||||
2. **Event-Driven 아키텍처**: Kafka를 통한 비동기 이벤트 발행/구독
|
||||
3. **Resilience 패턴**: Circuit Breaker, Retry, Timeout, Fallback 적용
|
||||
4. **Cache-Aside 패턴**: Redis 캐싱을 통한 성능 최적화
|
||||
5. **서비스 독립성**: 느슨한 결합과 장애 격리
|
||||
|
||||
---
|
||||
|
||||
## 외부 시퀀스 플로우 목록
|
||||
|
||||
총 **4개의 주요 비즈니스 플로우**로 구성되어 있습니다:
|
||||
|
||||
### 1. 사용자 인증 플로우
|
||||
**파일**: `사용자인증플로우.puml`
|
||||
|
||||
**포함된 유저스토리**:
|
||||
- UFR-USER-010: 회원가입
|
||||
- UFR-USER-020: 로그인
|
||||
- UFR-USER-040: 로그아웃
|
||||
|
||||
**주요 참여자**:
|
||||
- Frontend (Web/Mobile)
|
||||
- API Gateway
|
||||
- User Service
|
||||
- Redis Cache
|
||||
- User DB (PostgreSQL)
|
||||
- 국세청 API (외부)
|
||||
|
||||
**핵심 기능**:
|
||||
- JWT 기반 인증
|
||||
- 사업자번호 검증 (Circuit Breaker 적용)
|
||||
- Redis 캐싱 (사업자번호 검증 결과, TTL 7일)
|
||||
- 비밀번호 해싱 (bcrypt)
|
||||
- 사업자번호 암호화 (AES-256)
|
||||
|
||||
**Resilience 패턴**:
|
||||
- Circuit Breaker: 국세청 API (실패율 50% 초과 시 Open)
|
||||
- Retry: 최대 3회 재시도 (지수 백오프: 1초, 2초, 4초)
|
||||
- Timeout: 5초
|
||||
- Fallback: 사업자번호 검증 스킵 (수동 확인 안내)
|
||||
|
||||
---
|
||||
|
||||
### 2. 이벤트 생성 플로우
|
||||
**파일**: `이벤트생성플로우.puml`
|
||||
|
||||
**포함된 유저스토리**:
|
||||
- UFR-EVENT-020: 이벤트 목적 선택
|
||||
- UFR-EVENT-030: AI 이벤트 추천
|
||||
- UFR-CONT-010: SNS 이미지 생성
|
||||
- UFR-EVENT-050: 최종 승인 및 배포
|
||||
|
||||
**주요 참여자**:
|
||||
- Frontend
|
||||
- API Gateway
|
||||
- Event Service
|
||||
- AI Service (Kafka 구독)
|
||||
- Content Service (Kafka 구독)
|
||||
- Distribution Service (동기 호출)
|
||||
- Kafka (Event Topics + Job Topics)
|
||||
- Redis Cache
|
||||
- Event DB
|
||||
- 외부 API (AI API, 이미지 생성 API, 배포 채널 APIs)
|
||||
|
||||
**핵심 기능**:
|
||||
1. **이벤트 목적 선택** (동기)
|
||||
- Event DB에 목적 저장
|
||||
- EventCreated 이벤트 발행
|
||||
|
||||
2. **AI 이벤트 추천** (비동기)
|
||||
- Kafka ai-job 토픽 발행
|
||||
- AI Service 구독 및 처리
|
||||
- Polling 패턴으로 Job 상태 확인 (최대 30초)
|
||||
- Redis 캐싱 (TTL 24시간)
|
||||
|
||||
3. **SNS 이미지 생성** (비동기)
|
||||
- Kafka image-job 토픽 발행
|
||||
- Content Service 구독 및 처리
|
||||
- Polling 패턴으로 Job 상태 확인 (최대 20초)
|
||||
- CDN 업로드 및 Redis 캐싱 (TTL 7일)
|
||||
|
||||
4. **최종 승인 및 배포** (동기)
|
||||
- Distribution Service REST API 직접 호출
|
||||
- 다중 채널 병렬 배포 (1분 이내)
|
||||
- DistributionCompleted 이벤트 발행
|
||||
|
||||
**Resilience 패턴**:
|
||||
- Circuit Breaker: 모든 외부 API 호출 시 적용
|
||||
- Retry: 최대 3회 재시도 (지수 백오프)
|
||||
- Timeout: AI API 30초, 이미지 API 20초, 배포 API 10초
|
||||
- Bulkhead: 채널별 스레드 풀 격리
|
||||
- Fallback: AI 추천 시 캐시된 이전 결과, 이미지 생성 시 기본 템플릿
|
||||
|
||||
---
|
||||
|
||||
### 3. 고객 참여 플로우
|
||||
**파일**: `고객참여플로우.puml`
|
||||
|
||||
**포함된 유저스토리**:
|
||||
- UFR-PART-010: 이벤트 참여
|
||||
- UFR-PART-030: 당첨자 추첨
|
||||
|
||||
**주요 참여자**:
|
||||
- Frontend (고객용 / 사장님용)
|
||||
- API Gateway
|
||||
- Participation Service
|
||||
- Kafka (Event Topics)
|
||||
- Participation DB
|
||||
- Analytics Service (이벤트 구독)
|
||||
|
||||
**핵심 기능**:
|
||||
1. **이벤트 참여**
|
||||
- 중복 참여 체크 (전화번호 기반)
|
||||
- 응모 번호 발급
|
||||
- ParticipantRegistered 이벤트 발행 → Analytics Service 구독
|
||||
|
||||
2. **당첨자 추첨**
|
||||
- 난수 기반 무작위 추첨 (Crypto.randomBytes)
|
||||
- Fisher-Yates Shuffle 알고리즘
|
||||
- 매장 방문 고객 가산점 적용 (선택 옵션)
|
||||
- WinnerSelected 이벤트 발행
|
||||
|
||||
**Event-Driven 특징**:
|
||||
- Analytics Service가 ParticipantRegistered 이벤트 구독하여 실시간 통계 업데이트
|
||||
- 서비스 간 직접 의존성 없이 이벤트로 느슨한 결합
|
||||
|
||||
---
|
||||
|
||||
### 4. 성과 분석 플로우
|
||||
**파일**: `성과분석플로우.puml`
|
||||
|
||||
**포함된 유저스토리**:
|
||||
- UFR-ANAL-010: 실시간 성과분석 대시보드 조회
|
||||
|
||||
**주요 참여자**:
|
||||
- Frontend
|
||||
- API Gateway
|
||||
- Analytics Service
|
||||
- Redis Cache (TTL 5분)
|
||||
- Analytics DB
|
||||
- Kafka (Event Topics 구독)
|
||||
- 외부 API (우리동네TV, 지니TV, SNS APIs)
|
||||
|
||||
**핵심 기능**:
|
||||
1. **대시보드 조회 - Cache HIT** (0.5초)
|
||||
- Redis에서 캐시된 데이터 즉시 반환
|
||||
- 히트율 목표: 95%
|
||||
|
||||
2. **대시보드 조회 - Cache MISS** (3초)
|
||||
- Analytics DB 로컬 데이터 조회
|
||||
- 외부 채널 API 병렬 호출 (Circuit Breaker 적용)
|
||||
- 데이터 통합 및 ROI 계산
|
||||
- Redis 캐싱 (TTL 5분)
|
||||
|
||||
3. **실시간 업데이트 (Background)**
|
||||
- EventCreated 구독: 이벤트 기본 정보 초기화
|
||||
- ParticipantRegistered 구독: 참여자 수 실시간 증가
|
||||
- DistributionCompleted 구독: 배포 채널 통계 업데이트
|
||||
|
||||
**Resilience 패턴**:
|
||||
- Circuit Breaker: 외부 채널 API 조회 시 (실패율 50% 초과 시 Open)
|
||||
- Timeout: 10초
|
||||
- Fallback: 캐시된 이전 데이터 반환 또는 기본값 설정
|
||||
- 병렬 처리: 외부 채널 API 동시 호출
|
||||
|
||||
---
|
||||
|
||||
## 설계 특징
|
||||
|
||||
### 1. Event-Driven 아키텍처
|
||||
- **Kafka 통합**: Event Topics와 Job Topics를 Kafka로 통합
|
||||
- **느슨한 결합**: 서비스 간 직접 의존성 제거
|
||||
- **장애 격리**: 한 서비스 장애가 다른 서비스에 영향 없음
|
||||
- **확장 용이**: 새로운 구독자 추가로 기능 확장
|
||||
|
||||
### 2. 비동기 처리 패턴
|
||||
- **Kafka Job Topics**: ai-job, image-job
|
||||
- **Polling 패턴**: Job 상태 확인 (2-5초 간격)
|
||||
- **처리 시간**: AI 추천 10초, 이미지 생성 5초
|
||||
|
||||
### 3. 동기 처리 패턴
|
||||
- **Distribution Service**: REST API 직접 호출
|
||||
- **다중 채널 배포**: 병렬 처리 (1분 이내)
|
||||
- **Circuit Breaker**: 장애 전파 방지
|
||||
|
||||
### 4. Resilience 패턴 전면 적용
|
||||
- **Circuit Breaker**: 모든 외부 API 호출
|
||||
- **Retry**: 일시적 장애 자동 복구
|
||||
- **Timeout**: 응답 시간 제한
|
||||
- **Bulkhead**: 리소스 격리
|
||||
- **Fallback**: 장애 시 대체 로직
|
||||
|
||||
### 5. Cache-Aside 패턴
|
||||
- **Redis 캐싱**: 성능 최적화
|
||||
- **TTL 설정**: 데이터 유효성 관리
|
||||
- **히트율 목표**: 80-95%
|
||||
- **응답 시간 개선**: 90-99%
|
||||
|
||||
---
|
||||
|
||||
## 파일 구조
|
||||
|
||||
```
|
||||
design/backend/sequence/outer/
|
||||
├── README.md (본 문서)
|
||||
├── 사용자인증플로우.puml
|
||||
├── 이벤트생성플로우.puml
|
||||
├── 고객참여플로우.puml
|
||||
└── 성과분석플로우.puml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 다이어그램 확인 방법
|
||||
|
||||
### 1. Online PlantUML Viewer
|
||||
1. https://www.plantuml.com/plantuml/uml 접속
|
||||
2. `.puml` 파일 내용 붙여넣기
|
||||
3. 다이어그램 시각적 확인
|
||||
|
||||
### 2. VSCode Extension
|
||||
1. "PlantUML" 확장 프로그램 설치
|
||||
2. `.puml` 파일 열기
|
||||
3. `Alt+D` 또는 `Cmd+D`로 미리보기
|
||||
|
||||
### 3. IntelliJ IDEA Plugin
|
||||
1. "PlantUML integration" 플러그인 설치
|
||||
2. `.puml` 파일 열기
|
||||
3. 우측 미리보기 패널에서 확인
|
||||
|
||||
---
|
||||
|
||||
## 주요 결정사항
|
||||
|
||||
1. **Kafka 통합**: Event Bus와 Job Queue를 Kafka로 통합하여 운영 복잡도 감소
|
||||
2. **비동기 처리**: AI 추천 및 이미지 생성은 Kafka Job Topics를 통한 비동기 처리
|
||||
3. **동기 배포**: Distribution Service는 REST API 직접 호출하여 동기 처리 (1분 이내)
|
||||
4. **Resilience 패턴**: 모든 외부 API 호출 시 Circuit Breaker, Retry, Timeout, Fallback 적용
|
||||
5. **Cache-Aside 패턴**: Redis 캐싱으로 응답 시간 90-99% 개선
|
||||
6. **Event Topics**: EventCreated, ParticipantRegistered, WinnerSelected, DistributionCompleted
|
||||
7. **Job Topics**: ai-job, image-job
|
||||
|
||||
---
|
||||
|
||||
## 검증 사항
|
||||
|
||||
### 1. 유저스토리 매칭
|
||||
✅ 모든 유저스토리가 외부 시퀀스에 정확히 반영됨
|
||||
- User 서비스: 4개 유저스토리
|
||||
- Event 서비스: 4개 유저스토리
|
||||
- Participation 서비스: 2개 유저스토리
|
||||
- Analytics 서비스: 1개 유저스토리
|
||||
|
||||
### 2. 논리 아키텍처 일치성
|
||||
✅ 논리 아키텍처의 모든 컴포넌트와 통신 패턴 반영
|
||||
- Core Services: User, Event, Participation, Analytics
|
||||
- Async Services: AI, Content, Distribution
|
||||
- Kafka: Event Topics + Job Topics
|
||||
- External Systems: 국세청 API, AI API, 이미지 생성 API, 배포 채널 APIs
|
||||
|
||||
### 3. Resilience 패턴 적용
|
||||
✅ 모든 외부 API 호출에 Resilience 패턴 적용
|
||||
- Circuit Breaker, Retry, Timeout, Bulkhead, Fallback
|
||||
|
||||
### 4. PlantUML 문법 검증
|
||||
✅ PlantUML 기본 문법 검증 완료
|
||||
- `!theme mono` 적용
|
||||
- 동기/비동기 화살표 구분
|
||||
- 한글 설명 추가
|
||||
- 참여자 및 플로우 명확히 표현
|
||||
|
||||
---
|
||||
|
||||
## 향후 개선 방안
|
||||
|
||||
1. **WebSocket 기반 실시간 푸시**: 대시보드 실시간 업데이트 (폴링 대체)
|
||||
2. **Saga 패턴 적용**: 복잡한 분산 트랜잭션 보상 로직 체계화
|
||||
3. **Service Mesh 도입**: Istio를 통한 서비스 간 통신 관찰성 및 보안 강화
|
||||
4. **Dead Letter Queue 고도화**: 실패 이벤트 재처리 및 알림 자동화
|
||||
|
||||
---
|
||||
|
||||
**문서 버전**: 1.0
|
||||
**최종 수정일**: 2025-10-22
|
||||
**작성자**: System Architect
|
||||
@@ -0,0 +1,164 @@
|
||||
@startuml 고객참여플로우
|
||||
!theme mono
|
||||
|
||||
title 고객 참여 플로우 - 외부 시퀀스 다이어그램
|
||||
|
||||
actor "고객" as Customer
|
||||
participant "Frontend\n(고객용)" as CustomerFE
|
||||
participant "API Gateway" as Gateway
|
||||
participant "Participation\nService" as PartService
|
||||
participant "Kafka\n(Event Topics)" as Kafka
|
||||
database "Participation\nDB" as PartDB
|
||||
participant "Analytics\nService" as Analytics
|
||||
|
||||
actor "사장님" as Owner
|
||||
participant "Frontend\n(사장님용)" as OwnerFE
|
||||
|
||||
== UFR-PART-010: 이벤트 참여 ==
|
||||
|
||||
Customer -> CustomerFE: 이벤트 참여 화면 접근\n(우리동네TV/SNS/링고비즈)
|
||||
activate CustomerFE
|
||||
|
||||
CustomerFE -> Customer: 참여 정보 입력 폼 표시\n(이름, 전화번호, 참여경로)
|
||||
|
||||
Customer -> CustomerFE: 참여 정보 입력 및\n참여 버튼 클릭
|
||||
CustomerFE -> CustomerFE: 클라이언트 유효성 검증\n(이름 2자 이상, 전화번호 형식)
|
||||
|
||||
CustomerFE -> Gateway: POST /api/v1/participations\n{이름, 전화번호, 참여경로, 개인정보동의}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> PartService: POST /participations/register\n{이름, 전화번호, 참여경로, 개인정보동의}
|
||||
activate PartService
|
||||
|
||||
PartService -> PartDB: 참여자 중복 확인\n(전화번호, 이벤트ID로 조회)
|
||||
activate PartDB
|
||||
PartDB --> PartService: 중복 참여 여부 반환
|
||||
deactivate PartDB
|
||||
|
||||
alt 중복 참여인 경우
|
||||
PartService --> Gateway: 409 Conflict\n{message: "이미 참여하신 이벤트입니다"}
|
||||
Gateway --> CustomerFE: 409 Conflict
|
||||
CustomerFE -> Customer: 중복 참여 오류 메시지 표시
|
||||
deactivate PartService
|
||||
deactivate Gateway
|
||||
deactivate CustomerFE
|
||||
else 신규 참여인 경우
|
||||
PartService -> PartService: 응모 번호 생성\n(UUID 또는 시퀀스 기반)
|
||||
|
||||
PartService -> PartDB: 참여자 정보 저장\n(이름, 전화번호, 참여경로,\n응모번호, 참여일시)
|
||||
activate PartDB
|
||||
PartDB --> PartService: 저장 완료
|
||||
deactivate PartDB
|
||||
|
||||
PartService -> Kafka: Publish Event\n"ParticipantRegistered"\n{participantId, eventId,\nentryPath, timestamp}
|
||||
activate Kafka
|
||||
note right of Kafka
|
||||
Topic: participant-events
|
||||
Event: ParticipantRegistered
|
||||
Data: {
|
||||
participantId: UUID,
|
||||
eventId: UUID,
|
||||
entryPath: string,
|
||||
timestamp: datetime
|
||||
}
|
||||
end note
|
||||
|
||||
Kafka --> Analytics: Subscribe Event\n"ParticipantRegistered"
|
||||
activate Analytics
|
||||
Analytics -> Analytics: 참여자 데이터 집계\n- 채널별 참여자 수\n- 시간대별 참여 추이\n- 실시간 통계 업데이트
|
||||
deactivate Analytics
|
||||
deactivate Kafka
|
||||
|
||||
PartService --> Gateway: 201 Created\n{응모번호, 당첨발표일, 참여완료메시지}
|
||||
deactivate PartService
|
||||
|
||||
Gateway --> CustomerFE: 201 Created
|
||||
deactivate Gateway
|
||||
|
||||
CustomerFE -> Customer: 참여 완료 화면 표시\n- 응모번호\n- 당첨 발표일\n- "참여해주셔서 감사합니다"
|
||||
deactivate CustomerFE
|
||||
end
|
||||
|
||||
== UFR-PART-020: 참여자 목록 조회 ==
|
||||
|
||||
Owner -> OwnerFE: 이벤트 상세 화면에서\n"참여자 목록" 탭 클릭
|
||||
activate OwnerFE
|
||||
|
||||
OwnerFE -> Gateway: GET /api/v1/events/{eventId}/participants\n?page=1&size=20
|
||||
activate Gateway
|
||||
|
||||
Gateway -> PartService: GET /events/{eventId}/participants\n?page=1&size=20
|
||||
activate PartService
|
||||
|
||||
PartService -> PartDB: 참여자 목록 조회\n(이벤트ID, 페이지네이션)\nORDER BY 참여일시 DESC
|
||||
activate PartDB
|
||||
PartDB --> PartService: 참여자 목록 반환\n(이름, 전화번호, 참여경로,\n응모번호, 참여일시)\n+ 총 참여자 수
|
||||
deactivate PartDB
|
||||
|
||||
PartService --> Gateway: 200 OK\n{participants[], totalCount, page, size}
|
||||
deactivate PartService
|
||||
|
||||
Gateway --> OwnerFE: 200 OK
|
||||
deactivate Gateway
|
||||
|
||||
OwnerFE -> Owner: 참여자 목록 화면 표시\n- 참여자 정보 테이블\n- 페이지네이션\n- 총 참여자 수\n- CSV 다운로드 버튼
|
||||
deactivate OwnerFE
|
||||
|
||||
note right of Owner
|
||||
참여자 정보:
|
||||
- 이름 (마스킹: 김**)
|
||||
- 전화번호 (마스킹: 010-****-1234)
|
||||
- 참여경로 (우리동네TV, Instagram 등)
|
||||
- 응모번호
|
||||
- 참여일시
|
||||
end note
|
||||
|
||||
== UFR-PART-030: 당첨자 추첨 ==
|
||||
|
||||
Owner -> OwnerFE: 이벤트 상세 화면에서\n"당첨자 추첨" 버튼 클릭
|
||||
activate OwnerFE
|
||||
|
||||
OwnerFE -> Owner: 추첨 확인 다이얼로그 표시\n"당첨자를 추첨하시겠습니까?"
|
||||
|
||||
Owner -> OwnerFE: 확인 버튼 클릭
|
||||
|
||||
OwnerFE -> Gateway: POST /api/v1/events/{eventId}/draw-winners\n{당첨인원, 매장방문가산점옵션}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> PartService: POST /events/{eventId}/draw-winners\n{winnerCount, visitBonus}
|
||||
activate PartService
|
||||
|
||||
PartService -> PartDB: 미당첨 참여자 목록 조회\n(이벤트ID로 당첨되지 않은 참여자 조회)
|
||||
activate PartDB
|
||||
PartDB --> PartService: 전체 참여자 목록 반환
|
||||
deactivate PartDB
|
||||
|
||||
PartService -> PartService: 당첨자 추첨 알고리즘 실행\n1. 난수 생성 (Crypto.randomBytes)\n2. 매장방문 가산점 적용 (옵션)\n3. Fisher-Yates Shuffle\n4. 당첨인원만큼 선정
|
||||
|
||||
PartService -> PartDB: 당첨자 정보 업데이트\n(당첨 여부를 true로 설정, 당첨 일시 기록)
|
||||
activate PartDB
|
||||
PartDB --> PartService: 업데이트 완료
|
||||
deactivate PartDB
|
||||
|
||||
PartService -> PartDB: 추첨 로그 저장\n(이벤트ID, 추첨방법, 당첨인원,\n알고리즘, 추첨일시)
|
||||
activate PartDB
|
||||
note right of PartDB
|
||||
추첨 로그 저장:
|
||||
- 추첨 일시
|
||||
- 추첨 방법
|
||||
- 알고리즘 버전
|
||||
- 가산점 적용 여부
|
||||
end note
|
||||
PartDB --> PartService: 로그 저장 완료
|
||||
deactivate PartDB
|
||||
|
||||
PartService --> Gateway: 200 OK\n{당첨자목록, 추첨로그ID}
|
||||
deactivate PartService
|
||||
|
||||
Gateway --> OwnerFE: 200 OK
|
||||
deactivate Gateway
|
||||
|
||||
OwnerFE -> Owner: 당첨자 목록 화면 표시\n- 당첨자 정보 (이름, 전화번호, 응모번호)\n- 추첨 완료 메시지
|
||||
deactivate OwnerFE
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,177 @@
|
||||
@startuml 사용자인증플로우
|
||||
!theme mono
|
||||
|
||||
title KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 사용자 인증 플로우 (외부 시퀀스)
|
||||
|
||||
actor "사용자\n(소상공인)" as User
|
||||
participant "Frontend\n(Web/Mobile)" as Frontend
|
||||
participant "API Gateway" as Gateway
|
||||
participant "User Service" as UserService
|
||||
database "User DB\n(PostgreSQL)" as UserDB
|
||||
|
||||
== UFR-USER-010: 회원가입 플로우 ==
|
||||
|
||||
User -> Frontend: 회원가입 화면 접근
|
||||
activate Frontend
|
||||
|
||||
User -> Frontend: 회원 정보 입력\n(이름, 전화번호, 이메일, 비밀번호,\n매장명, 업종, 주소, 사업자번호)
|
||||
Frontend -> Frontend: 클라이언트 측 유효성 검증\n(이메일 형식, 비밀번호 8자 이상 등)
|
||||
|
||||
Frontend -> Gateway: POST /api/users/register\n(회원 정보)
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Gateway: Request 검증\n(필수 필드, 데이터 타입)
|
||||
|
||||
Gateway -> UserService: POST /api/users/register\n(회원 정보)
|
||||
activate UserService
|
||||
|
||||
UserService -> UserService: 서버 측 유효성 검증\n(이름 2자 이상, 전화번호 형식 등)
|
||||
|
||||
UserService -> UserDB: 이메일로 사용자 조회\n(중복 가입 확인)
|
||||
activate UserDB
|
||||
UserDB --> UserService: 기존 사용자 확인 결과
|
||||
deactivate UserDB
|
||||
|
||||
alt 이메일 중복 존재
|
||||
UserService --> Gateway: 400 Bad Request\n(이미 등록된 이메일)
|
||||
Gateway --> Frontend: 400 Bad Request
|
||||
Frontend --> User: "이미 가입된 이메일입니다"
|
||||
else 이메일 신규
|
||||
|
||||
UserService -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인)
|
||||
activate UserDB
|
||||
UserDB --> UserService: 기존 사용자 확인 결과
|
||||
deactivate UserDB
|
||||
|
||||
alt 전화번호 중복 존재
|
||||
UserService --> Gateway: 400 Bad Request\n(이미 등록된 전화번호)
|
||||
Gateway --> Frontend: 400 Bad Request
|
||||
Frontend --> User: "이미 가입된 전화번호입니다"
|
||||
else 신규 사용자
|
||||
|
||||
UserService -> UserService: 비밀번호 해싱\n(bcrypt, Cost Factor 10)
|
||||
|
||||
UserService -> UserService: 사업자번호 암호화\n(AES-256)
|
||||
|
||||
UserService -> UserDB: 트랜잭션 시작
|
||||
activate UserDB
|
||||
|
||||
UserService -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시)
|
||||
UserDB --> UserService: user_id 반환
|
||||
|
||||
UserService -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 암호화된사업자번호,\n영업시간)
|
||||
UserDB --> UserService: store_id 반환
|
||||
|
||||
UserService -> UserDB: 트랜잭션 커밋
|
||||
deactivate UserDB
|
||||
|
||||
UserService -> UserService: JWT 토큰 생성\n(user_id, role=OWNER,\nexp=7일)
|
||||
|
||||
UserService --> Gateway: 201 Created\n(JWT 토큰, 사용자 정보)
|
||||
deactivate UserService
|
||||
|
||||
Gateway --> Frontend: 201 Created\n(JWT 토큰, 사용자 정보)
|
||||
deactivate Gateway
|
||||
|
||||
Frontend -> Frontend: JWT 토큰 저장\n(LocalStorage 또는 Cookie)
|
||||
|
||||
Frontend --> User: "회원가입이 완료되었습니다"
|
||||
|
||||
Frontend -> Gateway: 대시보드 화면으로 이동
|
||||
deactivate Frontend
|
||||
end
|
||||
end
|
||||
|
||||
== UFR-USER-020: 로그인 플로우 ==
|
||||
|
||||
User -> Frontend: 로그인 화면 접근
|
||||
activate Frontend
|
||||
|
||||
User -> Frontend: 이메일, 비밀번호 입력
|
||||
|
||||
Frontend -> Frontend: 클라이언트 측 유효성 검증\n(필수 필드 확인, 이메일 형식)
|
||||
|
||||
Frontend -> Gateway: POST /api/users/login\n(이메일, 비밀번호)
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Gateway: Request 검증
|
||||
|
||||
Gateway -> UserService: POST /api/users/login\n(이메일, 비밀번호)
|
||||
activate UserService
|
||||
|
||||
UserService -> UserDB: 이메일로 사용자 조회\n(로그인 인증용)
|
||||
activate UserDB
|
||||
UserDB --> UserService: 사용자 정보\n(user_id, password_hash, role)
|
||||
deactivate UserDB
|
||||
|
||||
alt 사용자 없음
|
||||
UserService --> Gateway: 401 Unauthorized\n(인증 실패)
|
||||
Gateway --> Frontend: 401 Unauthorized
|
||||
Frontend --> User: "이메일 또는 비밀번호를\n확인해주세요"
|
||||
else 사용자 존재
|
||||
|
||||
UserService -> UserService: 비밀번호 검증\n(bcrypt compare)
|
||||
|
||||
alt 비밀번호 불일치
|
||||
UserService --> Gateway: 401 Unauthorized\n(인증 실패)
|
||||
Gateway --> Frontend: 401 Unauthorized
|
||||
Frontend --> User: "이메일 또는 비밀번호를\n확인해주세요"
|
||||
else 비밀번호 일치
|
||||
|
||||
UserService -> UserService: JWT 토큰 생성\n(user_id, role=OWNER,\nexp=7일)
|
||||
|
||||
UserService -> UserDB: 최종 로그인 시각 업데이트\n(현재 시각으로 갱신)
|
||||
activate UserDB
|
||||
UserDB --> UserService: 업데이트 완료
|
||||
deactivate UserDB
|
||||
|
||||
UserService --> Gateway: 200 OK\n(JWT 토큰, 사용자 정보)
|
||||
deactivate UserService
|
||||
|
||||
Gateway --> Frontend: 200 OK\n(JWT 토큰, 사용자 정보)
|
||||
deactivate Gateway
|
||||
|
||||
Frontend -> Frontend: JWT 토큰 저장\n(LocalStorage 또는 Cookie)
|
||||
|
||||
Frontend --> User: 로그인 성공
|
||||
|
||||
Frontend -> Gateway: 대시보드 화면으로 이동
|
||||
deactivate Frontend
|
||||
end
|
||||
end
|
||||
|
||||
== UFR-USER-040: 로그아웃 플로우 ==
|
||||
|
||||
User -> Frontend: 프로필 탭 접근
|
||||
activate Frontend
|
||||
|
||||
User -> Frontend: "로그아웃" 버튼 클릭
|
||||
|
||||
Frontend -> Frontend: 확인 다이얼로그 표시\n"로그아웃 하시겠습니까?"
|
||||
|
||||
User -> Frontend: "확인" 클릭
|
||||
|
||||
Frontend -> Gateway: POST /api/users/logout\nAuthorization: Bearer {JWT}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Gateway: JWT 토큰 검증
|
||||
|
||||
Gateway -> UserService: POST /api/users/logout\n(JWT 토큰)
|
||||
activate UserService
|
||||
|
||||
UserService -> UserService: JWT 토큰 블랙리스트에 추가\n(만료 시까지 유효)
|
||||
|
||||
UserService --> Gateway: 200 OK\n(로그아웃 성공)
|
||||
deactivate UserService
|
||||
|
||||
Gateway --> Frontend: 200 OK
|
||||
deactivate Gateway
|
||||
|
||||
Frontend -> Frontend: JWT 토큰 삭제\n(LocalStorage 또는 Cookie)
|
||||
|
||||
Frontend --> User: "안전하게 로그아웃되었습니다"
|
||||
|
||||
Frontend -> Gateway: 로그인 화면으로 이동
|
||||
deactivate Frontend
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,198 @@
|
||||
@startuml 성과분석플로우_외부시퀀스
|
||||
!theme mono
|
||||
|
||||
title 성과 분석 플로우 - 외부 시퀀스 다이어그램\n(UFR-ANAL-010: 실시간 성과분석 대시보드 조회)
|
||||
|
||||
actor "소상공인" as User
|
||||
participant "Frontend" as FE
|
||||
participant "API Gateway" as GW
|
||||
participant "Analytics Service" as Analytics
|
||||
participant "Redis Cache\n(TTL 1시간)" as Redis
|
||||
participant "Analytics DB" as AnalyticsDB
|
||||
participant "Kafka\n(Event Topics)" as Kafka
|
||||
|
||||
note over AnalyticsDB
|
||||
**배치 처리로 수집된 데이터**
|
||||
- 외부 채널 통계는 배치 작업으로
|
||||
주기적으로 수집하여 DB에 저장
|
||||
- 목업 데이터로 시작, 점진적으로 실제 API 연동
|
||||
end note
|
||||
|
||||
== 1. 대시보드 조회 - Cache HIT 시나리오 ==
|
||||
|
||||
User -> FE: 성과분석 대시보드 접근\n(Bottom Nav "분석" 탭 클릭)
|
||||
activate FE
|
||||
|
||||
FE -> GW: GET /api/events/{id}/analytics\n+ Authorization: Bearer {token}
|
||||
activate GW
|
||||
|
||||
GW -> GW: JWT 토큰 검증
|
||||
GW -> Analytics: GET /api/events/{id}/analytics
|
||||
activate Analytics
|
||||
|
||||
Analytics -> Redis: 대시보드 캐시 조회\n(캐시키: analytics:dashboard:{eventId})
|
||||
activate Redis
|
||||
Redis --> Analytics: **Cache HIT**\n캐시된 대시보드 데이터 반환
|
||||
deactivate Redis
|
||||
|
||||
note right of Analytics
|
||||
**Cache-Aside 패턴**
|
||||
- TTL: 1시간
|
||||
- 예상 크기: 5KB
|
||||
- 히트율 목표: 95%
|
||||
- 응답 시간: 0.5초
|
||||
end note
|
||||
|
||||
Analytics --> GW: 200 OK\n대시보드 데이터 (JSON)
|
||||
deactivate Analytics
|
||||
|
||||
GW --> FE: 200 OK\n대시보드 데이터
|
||||
deactivate GW
|
||||
|
||||
FE -> FE: 대시보드 렌더링\n- 4개 요약 카드\n- 채널별 성과 차트\n- 시간대별 참여 추이
|
||||
FE --> User: 실시간 대시보드 표시
|
||||
deactivate FE
|
||||
|
||||
== 2. 대시보드 조회 - Cache MISS 시나리오 ==
|
||||
|
||||
User -> FE: 대시보드 새로고침\n또는 첫 조회
|
||||
activate FE
|
||||
|
||||
FE -> GW: GET /api/events/{id}/analytics
|
||||
activate GW
|
||||
|
||||
GW -> Analytics: GET /api/events/{id}/analytics
|
||||
activate Analytics
|
||||
|
||||
Analytics -> Redis: 대시보드 캐시 조회\n(캐시키: analytics:dashboard:{eventId})
|
||||
activate Redis
|
||||
Redis --> Analytics: **Cache MISS**\nnull 반환
|
||||
deactivate Redis
|
||||
|
||||
note right of Analytics
|
||||
**데이터 통합 작업 시작**
|
||||
- Analytics DB 조회
|
||||
- 외부 채널 API 병렬 호출
|
||||
- Circuit Breaker 적용
|
||||
end note
|
||||
|
||||
|||
|
||||
== 2.1. Analytics DB 조회 (로컬 데이터) ==
|
||||
|
||||
Analytics -> AnalyticsDB: 이벤트 통계 조회\n(이벤트ID로 통계 데이터 조회)
|
||||
activate AnalyticsDB
|
||||
AnalyticsDB --> Analytics: 이벤트 통계\n- 총 참여자 수\n- 예상 ROI\n- 매출 증가율
|
||||
deactivate AnalyticsDB
|
||||
|
||||
|||
|
||||
== 2.2. 배치 수집된 채널 통계 데이터 조회 ==
|
||||
|
||||
Analytics -> AnalyticsDB: 채널별 통계 조회\n(배치로 수집된 채널 데이터 조회)
|
||||
activate AnalyticsDB
|
||||
|
||||
note right of Analytics
|
||||
**배치 처리 방식**
|
||||
- 외부 API는 별도 배치 작업으로 주기적 수집
|
||||
- 수집된 데이터는 DB에 저장
|
||||
- 대시보드에서는 DB 데이터만 조회
|
||||
- 응답 시간 단축 및 외부 API 의존성 제거
|
||||
end note
|
||||
|
||||
AnalyticsDB --> Analytics: 채널별 통계 데이터\n- 우리동네TV: 노출 5,000, 조회 1,200\n- 지니TV: 노출 10,000, 클릭 500\n- Instagram: 좋아요 300, 댓글 50\n- Naver: 조회 2,000\n- Kakao: 공유 100
|
||||
deactivate AnalyticsDB
|
||||
|
||||
note right of Analytics
|
||||
**목업 데이터 활용**
|
||||
- 초기에는 목업 데이터로 시작
|
||||
- 점진적으로 실제 배치 작업 구현
|
||||
- 배치 주기: 5분마다 수집
|
||||
end note
|
||||
|
||||
|||
|
||||
== 2.3. 데이터 통합 및 ROI 계산 ==
|
||||
|
||||
Analytics -> Analytics: 데이터 통합 및 계산\n- 총 노출 수 = 외부 채널 노출 합계\n- 총 참여자 수 = Analytics DB\n- ROI 계산 = (수익 - 비용) / 비용 × 100\n- 채널별 전환율 계산
|
||||
|
||||
note right of Analytics
|
||||
**ROI 계산 로직**
|
||||
총 비용 = 경품 비용 + 플랫폼 비용
|
||||
예상 수익 = 매출 증가액 + 신규 고객 LTV
|
||||
투자 대비 수익률 = (수익 - 비용) / 비용 × 100
|
||||
end note
|
||||
|
||||
|||
|
||||
== 2.4. Redis 캐싱 및 응답 ==
|
||||
|
||||
Analytics -> Redis: 대시보드 데이터 캐시 저장\n(캐시키: analytics:dashboard:{eventId},\n값: 통합 데이터, TTL: 1시간)
|
||||
activate Redis
|
||||
Redis --> Analytics: OK
|
||||
deactivate Redis
|
||||
|
||||
Analytics --> GW: 200 OK\n대시보드 데이터 (JSON)\n{\n 총참여자: 1,234,\n 총노출: 17,200,\n ROI: 250%,\n 채널별성과: [...]\n}
|
||||
deactivate Analytics
|
||||
|
||||
GW --> FE: 200 OK\n대시보드 데이터
|
||||
deactivate GW
|
||||
|
||||
FE -> FE: 대시보드 렌더링\n- 4개 요약 카드 표시\n- 채널별 성과 차트\n- 시간대별 참여 추이\n- 참여자 프로필 분석
|
||||
FE --> User: 실시간 대시보드 표시\n(응답 시간: 3초)
|
||||
deactivate FE
|
||||
|
||||
|||
|
||||
== 3. 실시간 업데이트 (Background Event 구독) ==
|
||||
|
||||
note over Analytics, Kafka
|
||||
**Analytics Service는 항상 Background에서
|
||||
Kafka Event Topics를 구독하여
|
||||
실시간으로 통계를 업데이트합니다**
|
||||
end note
|
||||
|
||||
Kafka -> Analytics: **EventCreated** 이벤트\n{eventId, storeId, title, objective}
|
||||
activate Analytics
|
||||
Analytics -> AnalyticsDB: 이벤트 통계 초기화\n(이벤트 기본 정보 저장)
|
||||
activate AnalyticsDB
|
||||
AnalyticsDB --> Analytics: OK
|
||||
deactivate AnalyticsDB
|
||||
Analytics -> Redis: 캐시 무효화\n(캐시키 삭제: analytics:dashboard:{eventId})
|
||||
activate Redis
|
||||
Redis --> Analytics: OK
|
||||
deactivate Redis
|
||||
deactivate Analytics
|
||||
|
||||
...참여자 등록 시...
|
||||
|
||||
Kafka -> Analytics: **ParticipantRegistered** 이벤트\n{participantId, eventId, phoneNumber}
|
||||
activate Analytics
|
||||
Analytics -> AnalyticsDB: 참여자 수 업데이트\n(참여자 수 1 증가)
|
||||
activate AnalyticsDB
|
||||
AnalyticsDB --> Analytics: OK
|
||||
deactivate AnalyticsDB
|
||||
Analytics -> Redis: 캐시 무효화\n(캐시키 삭제: analytics:dashboard:{eventId})
|
||||
activate Redis
|
||||
Redis --> Analytics: OK
|
||||
deactivate Redis
|
||||
deactivate Analytics
|
||||
|
||||
...배포 완료 시...
|
||||
|
||||
Kafka -> Analytics: **DistributionCompleted** 이벤트\n{eventId, distributedChannels, completedAt}
|
||||
activate Analytics
|
||||
Analytics -> AnalyticsDB: 채널 통계 저장\n(배포 완료된 채널 정보 저장)
|
||||
activate AnalyticsDB
|
||||
AnalyticsDB --> Analytics: OK
|
||||
deactivate AnalyticsDB
|
||||
Analytics -> Redis: 캐시 무효화\n(캐시키 삭제: analytics:dashboard:{eventId})
|
||||
activate Redis
|
||||
Redis --> Analytics: OK
|
||||
deactivate Redis
|
||||
deactivate Analytics
|
||||
|
||||
note right of Analytics
|
||||
**실시간 업데이트 메커니즘**
|
||||
- EventCreated: 이벤트 기본 정보 초기화
|
||||
- ParticipantRegistered: 참여자 수 실시간 증가
|
||||
- DistributionCompleted: 배포 채널 통계 업데이트
|
||||
- 캐시 무효화: 다음 조회 시 최신 데이터 갱신
|
||||
end note
|
||||
|
||||
@enduml
|
||||
@@ -0,0 +1,250 @@
|
||||
@startuml 이벤트생성플로우
|
||||
!theme mono
|
||||
|
||||
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
|
||||
|
||||
actor "소상공인" as User
|
||||
participant "Frontend" as FE
|
||||
participant "API Gateway" as Gateway
|
||||
participant "Event Service" as Event
|
||||
participant "User Service" as UserSvc
|
||||
participant "AI Service" as AI
|
||||
participant "Content Service" as Content
|
||||
participant "Distribution Service" as Dist
|
||||
participant "Kafka" as Kafka
|
||||
database "Event DB" as EventDB
|
||||
database "User DB" as UserDB
|
||||
database "Redis" as Redis
|
||||
participant "외부 AI API" as AIApi
|
||||
participant "이미지 생성 API" as ImageApi
|
||||
participant "배포 채널 APIs" as ChannelApis
|
||||
|
||||
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
|
||||
User -> FE: 이벤트 목적 선택
|
||||
FE -> Gateway: GET /api/users/{userId}/store\n회원 및 매장정보 조회
|
||||
activate Gateway
|
||||
Gateway -> UserSvc: GET /api/users/{userId}/store\n회원 및 매장정보 조회
|
||||
activate UserSvc
|
||||
UserSvc -> UserDB: 사용자 및 매장 정보 조회
|
||||
activate UserDB
|
||||
UserDB --> UserSvc: 사용자, 매장 정보 반환
|
||||
deactivate UserDB
|
||||
UserSvc --> Gateway: 200 OK\n{userId, storeName, industry, address}
|
||||
deactivate UserSvc
|
||||
Gateway --> FE: 200 OK\n{userId, storeName, industry, address}
|
||||
deactivate Gateway
|
||||
FE -> Gateway: POST /events/purposes\n{목적, userId, storeName, industry, address}
|
||||
Gateway -> Event: 이벤트 목적 저장 요청
|
||||
Event -> Redis: 이벤트 목적 정보 저장\nKey: draft:event:{eventDraftId}\n(목적, 매장정보 저장)\nTTL: 24시간
|
||||
activate Redis
|
||||
Redis --> Event: 저장 완료
|
||||
deactivate Redis
|
||||
Event --> Gateway: 저장 완료\n{eventDraftId}
|
||||
Gateway --> FE: 200 OK
|
||||
FE --> User: AI 추천 화면으로 이동
|
||||
|
||||
== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) ==
|
||||
User -> FE: AI 추천 요청
|
||||
FE -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{목적, 업종, 지역}
|
||||
Gateway -> Event: AI 추천 요청 전달
|
||||
Event -> Kafka: Publish to ai-job-topic\n{jobId, eventDraftId, 목적, 업종, 지역}
|
||||
Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
|
||||
Gateway --> FE: 202 Accepted\n{jobId}
|
||||
FE --> User: "AI가 분석 중입니다..." (로딩)
|
||||
|
||||
note over AI: Kafka Consumer\nai 이벤트 생성 topic 구독
|
||||
Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...}
|
||||
|
||||
AI -> AIApi: 트렌드 분석 및 이벤트 추천 요청\n{목적, 업종, 지역, 매장정보}\n[Circuit Breaker, Timeout: 5분]
|
||||
AIApi --> AI: 3가지 추천안 + 트렌드 요약\n(예: "여름철 시원한 음료 선호도 증가")
|
||||
AI -> Redis: AI 추천 결과 저장\nKey: ai:event:{eventDraftId}\n(3가지 추천안, 트렌드 요약)\nTTL: 24시간
|
||||
Redis --> AI: 저장 완료
|
||||
AI -> Redis: Job 상태 업데이트\n(상태를 COMPLETED로 변경)
|
||||
note over AI, Redis: AI 추천 정보는 Redis에 저장\n- Content Service가 읽기 위함\n- 최종 승인 시 Event DB에 영구 저장
|
||||
|
||||
group Polling으로 상태 확인
|
||||
loop 상태 확인 (최대 30초)
|
||||
FE -> Gateway: GET /jobs/{jobId}/status
|
||||
Gateway -> Event: Job 상태 조회
|
||||
Event -> Redis: Job 상태 조회\n(jobId로 상태 및 결과 조회)
|
||||
Redis --> Event: {status, result}
|
||||
|
||||
alt Job 완료
|
||||
Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations, trendSummary}
|
||||
Gateway --> FE: 추천 결과 및 트렌드 요약 반환
|
||||
FE --> User: 트렌드 요약 표시\n3가지 추천안 표시\n(제목/경품 수정 가능)
|
||||
else Job 진행중
|
||||
Event --> Gateway: 200 OK\n{status: PENDING/PROCESSING}
|
||||
Gateway --> FE: 진행중 상태
|
||||
note over FE: 2초 후 재요청
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
User -> FE: 추천안 선택\n(제목/경품 커스텀)
|
||||
FE -> Gateway: PUT /events/drafts/{eventDraftId}\n{선택한 추천안, 커스텀 정보}
|
||||
Gateway -> Event: 선택 저장
|
||||
Event -> Redis: 선택한 추천안 저장\nKey: draft:event:{eventDraftId}\n(이벤트 초안 업데이트)\nTTL: 24시간
|
||||
activate Redis
|
||||
Redis --> Event: 업데이트 완료
|
||||
deactivate Redis
|
||||
Event --> Gateway: 200 OK
|
||||
Gateway --> FE: 저장 완료
|
||||
FE --> User: 콘텐츠 생성 화면으로 이동
|
||||
|
||||
== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) ==
|
||||
User -> FE: 이미지 생성 요청
|
||||
FE -> Gateway: POST /api/content/images/{eventDraftId}/generate
|
||||
Gateway -> Content: 이미지 생성 요청
|
||||
Content -> Content: Job 생성\n{jobId, eventDraftId, status: PENDING}
|
||||
Content --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
|
||||
Gateway --> FE: 202 Accepted\n{jobId}
|
||||
FE --> User: "이미지 생성 중..." (로딩)
|
||||
|
||||
note over Content: 백그라운드 워커\nRedis 폴링 또는 스케줄러
|
||||
|
||||
Content -> Redis: AI 이벤트 데이터 읽기\nKey: ai:event:{eventDraftId}
|
||||
activate Redis
|
||||
Redis --> Content: AI 추천 결과\n{선택된 추천안, 이벤트 정보}
|
||||
deactivate Redis
|
||||
|
||||
note over Content: inner sequence 참조:\ncontent-이미지생성.puml
|
||||
|
||||
par 심플 스타일
|
||||
Content -> ImageApi: 심플 스타일 생성 요청\n[Circuit Breaker, Timeout: 5분]
|
||||
ImageApi --> Content: 심플 이미지 URL
|
||||
else 화려한 스타일
|
||||
Content -> ImageApi: 화려한 스타일 생성 요청\n[Circuit Breaker, Timeout: 5분]
|
||||
ImageApi --> Content: 화려한 이미지 URL
|
||||
else 트렌디 스타일
|
||||
Content -> ImageApi: 트렌디 스타일 생성 요청\n[Circuit Breaker, Timeout: 5분]
|
||||
ImageApi --> Content: 트렌디 이미지 URL
|
||||
end
|
||||
|
||||
Content -> Redis: 이미지 URL 저장\nKey: content:image:{eventDraftId}\n{심플, 화려, 트렌디 URL}\nTTL: 7일
|
||||
activate Redis
|
||||
Redis --> Content: 저장 완료
|
||||
deactivate Redis
|
||||
Content -> Redis: Job 상태 업데이트\n(상태를 COMPLETED로 변경)
|
||||
note over Content, Redis: 이미지 URL은 Redis에 저장\n- 최종 승인 시 Event DB에 영구 저장
|
||||
|
||||
group Polling으로 상태 확인
|
||||
loop 상태 확인 (최대 30초)
|
||||
FE -> Gateway: GET /api/content/jobs/{jobId}/status
|
||||
Gateway -> Content: Job 상태 조회
|
||||
Content -> Redis: Job 상태 조회\n(jobId로 상태 및 이미지 URL 조회)
|
||||
Redis --> Content: {status, imageUrls}
|
||||
|
||||
alt Job 완료
|
||||
Content --> Gateway: 200 OK\n{status: COMPLETED, imageUrls}
|
||||
Gateway --> FE: 이미지 URL 반환
|
||||
FE --> User: 3가지 스타일 카드 표시
|
||||
else Job 진행중
|
||||
Content --> Gateway: 200 OK\n{status: PENDING/PROCESSING}
|
||||
Gateway --> FE: 진행중 상태
|
||||
note over FE: 2초 후 재요청
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
User -> FE: 스타일 선택 및 편집
|
||||
FE -> Gateway: PUT /events/drafts/{eventDraftId}/content\n{선택한 이미지, 편집내용}
|
||||
Gateway -> Event: 콘텐츠 선택 저장
|
||||
Event -> Redis: 선택한 콘텐츠 저장\nKey: draft:event:{eventDraftId}\n(이벤트 초안 업데이트)\nTTL: 24시간
|
||||
activate Redis
|
||||
Redis --> Event: 업데이트 완료
|
||||
deactivate Redis
|
||||
Event --> Gateway: 200 OK
|
||||
Gateway --> FE: 저장 완료
|
||||
FE --> User: 배포 채널 선택 화면으로 이동
|
||||
|
||||
== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) ==
|
||||
User -> FE: 배포 채널 선택\n최종 승인 요청
|
||||
FE -> Gateway: POST /api/events/{eventDraftId}/publish\n{선택 채널 목록}
|
||||
Gateway -> Event: 최종 승인 및 배포 처리
|
||||
|
||||
note over Event: Redis 데이터를 Event DB에 영구 저장
|
||||
|
||||
Event -> Redis: 이벤트 초안 조회\nKey: draft:event:{eventDraftId}
|
||||
activate Redis
|
||||
Redis --> Event: 이벤트 초안 데이터\n(목적, 매장정보, 추천안, 콘텐츠)
|
||||
deactivate Redis
|
||||
|
||||
Event -> Redis: AI 추천 결과 조회\nKey: ai:event:{eventDraftId}
|
||||
activate Redis
|
||||
Redis --> Event: AI 추천 결과
|
||||
deactivate Redis
|
||||
|
||||
Event -> Redis: 이미지 URL 조회\nKey: content:image:{eventDraftId}
|
||||
activate Redis
|
||||
Redis --> Event: 이미지 URL 목록
|
||||
deactivate Redis
|
||||
|
||||
Event -> EventDB: 이벤트 정보 영구 저장\n(목적, 매장정보, AI 추천, 이미지 URL, 배포 채널 포함)
|
||||
EventDB --> Event: 저장 완료
|
||||
|
||||
Event -> EventDB: 이벤트 상태 변경\n(DRAFT → APPROVED로 업데이트)
|
||||
EventDB --> Event: 상태 변경 완료
|
||||
Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보}
|
||||
|
||||
note over Event: 동기 호출로 배포 진행\ninner sequence 참조:\ndistribution-다중채널배포.puml
|
||||
Event -> Dist: REST API - 배포 요청\nPOST /api/distribution/distribute\n{eventId, channels[], contentUrls}
|
||||
|
||||
note over Dist: Sprint 2: Mock 처리\n- 외부 API 호출 없음\n- 모든 배포 즉시 성공 처리\n- 배포 로그만 DB 기록
|
||||
|
||||
Dist -> EventDB: 배포 이력 초기화\n(이벤트ID, 상태: PENDING)
|
||||
EventDB --> Dist: 배포 이력 ID
|
||||
Dist -> EventDB: 배포 이력 상태 업데이트\n(상태: IN_PROGRESS)
|
||||
|
||||
note over Dist: 다중 채널 Mock 배포\n(내부 처리 상세는 inner sequence 참조)
|
||||
|
||||
par 우리동네TV
|
||||
alt 우리동네TV 선택
|
||||
Dist -> Dist: Mock 처리\n(즉시 성공)
|
||||
Dist -> EventDB: 채널 로그 저장\n(우리동네TV, 성공,\n배포ID, 예상노출수)
|
||||
end
|
||||
else 링고비즈
|
||||
alt 링고비즈 선택
|
||||
Dist -> Dist: Mock 처리\n(즉시 성공)
|
||||
Dist -> EventDB: 채널 로그 저장\n(링고비즈, 성공,\n업데이트 시각)
|
||||
end
|
||||
else 지니TV
|
||||
alt 지니TV 선택
|
||||
Dist -> Dist: Mock 처리\n(즉시 성공)
|
||||
Dist -> EventDB: 채널 로그 저장\n(지니TV, 성공,\n광고ID, 스케줄)
|
||||
end
|
||||
else Instagram
|
||||
alt Instagram 선택
|
||||
Dist -> Dist: Mock 처리\n(즉시 성공)
|
||||
Dist -> EventDB: 채널 로그 저장\n(Instagram, 성공,\npostUrl, postId)
|
||||
end
|
||||
else Naver Blog
|
||||
alt Naver Blog 선택
|
||||
Dist -> Dist: Mock 처리\n(즉시 성공)
|
||||
Dist -> EventDB: 채널 로그 저장\n(NaverBlog, 성공,\npostUrl)
|
||||
end
|
||||
else Kakao Channel
|
||||
alt Kakao Channel 선택
|
||||
Dist -> Dist: Mock 처리\n(즉시 성공)
|
||||
Dist -> EventDB: 채널 로그 저장\n(KakaoChannel, 성공,\nmessageId)
|
||||
end
|
||||
end
|
||||
|
||||
note over Dist: 모든 채널 배포 완료 (즉시 처리)
|
||||
|
||||
Dist -> EventDB: 배포 이력 상태 업데이트\n(상태: COMPLETED, 완료일시)
|
||||
|
||||
Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt}
|
||||
|
||||
Dist --> Event: REST API 동기 응답\n200 OK\n{distributionId, status: COMPLETED, results[]}
|
||||
|
||||
Event -> EventDB: 이벤트 상태 업데이트\n(APPROVED → ACTIVE로 변경)
|
||||
EventDB --> Event: 업데이트 완료
|
||||
|
||||
Event --> Gateway: 200 OK\n{eventId, 배포결과}
|
||||
Gateway --> FE: 배포 완료
|
||||
FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동
|
||||
|
||||
note over Event, Dist: Sprint 2 제약사항\n- 외부 API 호출 없음 (Mock)\n- 모든 배포 즉시 성공 처리\n- Circuit Breaker 미구현\n- Retry 로직 미구현\n\nSprint 3 이후 구현 예정\n- 실제 외부 채널 API 연동\n- Circuit Breaker 패턴\n- Retry 및 실패 처리
|
||||
|
||||
@enduml
|
||||
Reference in New Issue
Block a user