초기 프로젝트 설정 및 설계 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
cherry2250
2025-10-24 10:10:16 +09:00
commit 3f6e005026
76 changed files with 37842 additions and 0 deletions
+665
View File
@@ -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
+914
View File
@@ -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
+849
View File
@@ -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
+991
View File
@@ -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
+393
View File
@@ -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
+304
View File
@@ -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