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

🤖 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
+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