add outer/inner sequence

This commit is contained in:
cherry2250
2025-10-22 14:13:57 +09:00
parent 44011cd73a
commit 9fd060b275
127 changed files with 7527 additions and 9 deletions
@@ -0,0 +1,386 @@
# User Service 내부 시퀀스 설계서
## 문서 정보
- **작성일**: 2025-10-22
- **작성자**: System Architect
- **버전**: 1.0
- **관련 문서**:
- [유저스토리](../../../userstory.md)
- [외부 시퀀스](../outer/사용자인증플로우.puml)
- [논리 아키텍처](../../logical/logical-architecture.md)
---
## 개요
User Service의 4가지 주요 시나리오에 대한 내부 처리 흐름을 상세히 정의합니다.
### 시나리오 목록
| 번호 | 파일명 | 유저스토리 | 주요 처리 내용 |
|------|--------|-----------|---------------|
| 1 | user-회원가입.puml | UFR-USER-010 | 기본 정보 검증, 사업자번호 검증(국세청 API), 트랜잭션 처리, JWT 발급 |
| 2 | user-로그인.puml | UFR-USER-020 | 비밀번호 검증(bcrypt), JWT 발급, 세션 저장, 최종 로그인 시각 업데이트 |
| 3 | user-프로필수정.puml | UFR-USER-030 | 기본 정보 수정, 매장 정보 수정, 비밀번호 변경, 트랜잭션 처리 |
| 4 | user-로그아웃.puml | UFR-USER-040 | JWT 검증, 세션 삭제, Blacklist 추가 |
---
## 아키텍처 구조
### Layered Architecture
```
┌─────────────────────────────────────┐
│ API Layer (Controller) │ - HTTP 요청/응답 처리
│ │ - DTO 변환 및 검증
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Business Layer (Service) │ - 비즈니스 로직 처리
│ - UserService │ - 트랜잭션 관리
│ - AuthenticationService │ - 외부 API 연동
│ - BusinessValidator │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Data Layer (Repository) │ - 데이터베이스 접근
│ - UserRepository │ - JPA/MyBatis
│ - StoreRepository │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ External Systems │ - 국세청 API
│ │ - Redis Cache
│ │ - PostgreSQL DB
└─────────────────────────────────────┘
```
### 주요 컴포넌트
#### API Layer
- **UserController**: 사용자 관련 REST API 엔드포인트
- `POST /api/users/register`: 회원가입
- `POST /api/users/login`: 로그인
- `PUT /api/users/profile`: 프로필 수정
- `POST /api/users/logout`: 로그아웃
#### Business Layer
- **UserService**: 사용자 정보 관리 비즈니스 로직
- **AuthenticationService**: 인증 및 세션 관리 로직
- **BusinessValidator**: 사업자번호 검증 로직 (Circuit Breaker 적용)
#### Data Layer
- **UserRepository**: users 테이블 CRUD
- **StoreRepository**: stores 테이블 CRUD
#### Utility
- **PasswordEncoder**: bcrypt 해싱 (Cost Factor 10)
- **JwtTokenProvider**: JWT 토큰 생성/검증 (만료 7일)
---
## 시나리오별 상세 설명
### 1. 회원가입 (user-회원가입.puml)
#### 처리 단계
1. **입력 검증**: `@Valid` 어노테이션으로 DTO 검증
2. **중복 사용자 확인**: 전화번호 기반 중복 체크
3. **사업자번호 검증**:
- Redis 캐시 확인 (TTL 7일)
- 캐시 MISS: 국세청 API 호출 (Circuit Breaker 적용)
- 캐시 HIT: 0.1초, MISS: 5초
4. **비밀번호 해싱**: bcrypt (Cost Factor 10)
5. **사업자번호 암호화**: AES-256
6. **데이터베이스 트랜잭션**:
- User INSERT
- Store INSERT
- COMMIT (실패 시 자동 Rollback)
7. **JWT 토큰 생성**: Claims(userId, role=OWNER, exp=7일)
8. **세션 저장**: Redis (TTL 7일)
#### Resilience 패턴
- **Circuit Breaker**: 국세청 API (실패율 50% 초과 시 Open)
- **Retry**: 최대 3회 (지수 백오프: 1초, 2초, 4초)
- **Timeout**: 5초
- **Fallback**: 사업자번호 검증 스킵 (수동 확인 안내)
#### 응답 시간
- 캐시 HIT: 1초 이내
- 캐시 MISS: 5초 이내
---
### 2. 로그인 (user-로그인.puml)
#### 처리 단계
1. **입력 검증**: 필수 필드 확인
2. **사용자 조회**: 전화번호로 사용자 검색
3. **비밀번호 검증**: bcrypt compare
4. **JWT 토큰 생성**: Claims(userId, role=OWNER, exp=7일)
5. **세션 저장**: Redis (TTL 7일)
6. **최종 로그인 시각 업데이트**: 비동기 처리 (`@Async`)
#### 보안 처리
- 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환
- 비밀번호: bcrypt compare (원본 노출 안 됨)
#### 성능 최적화
- 최종 로그인 시각 업데이트: 비동기 처리로 응답 시간 단축
- 응답 시간: 0.5초 목표
---
### 3. 프로필 수정 (user-프로필수정.puml)
#### 처리 단계
1. **JWT 인증**: `@AuthenticationPrincipal`로 userId 추출
2. **사용자 조회**: userId로 기존 정보 조회
3. **비밀번호 변경 처리** (선택적):
- 현재 비밀번호 검증 (bcrypt compare)
- 새 비밀번호 해싱 (bcrypt)
4. **기본 정보 업데이트**: 이름, 전화번호, 이메일
5. **매장 정보 업데이트**: 매장명, 업종, 주소, 영업시간
6. **데이터베이스 트랜잭션**:
- User UPDATE
- Store UPDATE
- COMMIT (실패 시 자동 Rollback)
7. **캐시 무효화**: 프로필 캐시 삭제 (선택적)
#### 보안 처리
- 비밀번호 변경: 현재 비밀번호 확인 필수
- 권한 검증: 본인만 수정 가능
#### 향후 개선사항
- 전화번호 변경: SMS/이메일 재인증 구현
- 이메일 변경: 이메일 인증 구현
---
### 4. 로그아웃 (user-로그아웃.puml)
#### 처리 단계
1. **JWT 인증**: `@AuthenticationPrincipal`로 userId 추출
2. **JWT 토큰 검증**: 서명 및 만료 시간 확인
3. **Redis 세션 삭제**: `DEL user:session:{token}`
4. **JWT Blacklist 추가** (선택적):
- 만료되지 않은 토큰 강제 무효화
- Redis에 Blacklist 추가 (TTL: 남은 만료 시간)
5. **로그아웃 로그 기록**: userId, timestamp
#### 보안 처리
- JWT Blacklist: 만료 전 토큰 강제 무효화
- 멱등성 보장: 중복 로그아웃 요청에 안전
#### 클라이언트 측 처리
- LocalStorage 또는 Cookie에서 JWT 토큰 삭제
- 로그인 화면으로 리다이렉트
#### 성능 최적화
- Redis 삭제 연산: O(1) 시간 복잡도
- 응답 시간: 0.1초 이내
---
## 데이터 모델
### User Entity
```java
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long userId;
@Column(nullable = false, length = 100)
private String name;
@Column(nullable = false, unique = true, length = 20)
private String phoneNumber;
@Column(nullable = false, unique = true, length = 255)
private String email;
@Column(nullable = false, length = 60)
private String passwordHash; // bcrypt hash
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private UserRole role; // OWNER, CUSTOMER
@Column(name = "last_login_at")
private LocalDateTime lastLoginAt;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
```
### Store Entity
```java
@Entity
@Table(name = "stores")
public class Store {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long storeId;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(nullable = false, length = 200)
private String storeName;
@Column(length = 100)
private String industry;
@Column(columnDefinition = "TEXT")
private String address;
@Column(name = "business_number_encrypted", length = 255)
private String businessNumberEncrypted; // AES-256 encrypted
@Column(name = "business_hours", length = 500)
private String businessHours;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
```
---
## 캐싱 전략
### Redis 캐시 키 구조
| 캐시 키 패턴 | 데이터 타입 | TTL | 용도 |
|------------|-----------|-----|------|
| `user:session:{token}` | String (JSON) | 7일 | JWT 세션 정보 (userId, role) |
| `user:business:{사업자번호}` | String (JSON) | 7일 | 사업자번호 검증 결과 (valid, status) |
| `jwt:blacklist:{token}` | String | 남은 만료 시간 | 로그아웃된 JWT 토큰 Blacklist |
| `user:profile:{userId}` | String (JSON) | 1시간 | 사용자 프로필 정보 (선택적) |
### Cache-Aside 패턴
1. Application → Redis 확인 (Cache HIT/MISS)
2. Cache MISS → Database/External API 조회
3. Database/External API → Redis 캐싱 (TTL 설정)
4. Redis → Application 반환
---
## 에러 처리
### 주요 예외 클래스
| 예외 클래스 | HTTP 상태 | 발생 시점 |
|-----------|---------|----------|
| `DuplicateUserException` | 400 | 이미 가입된 전화번호 |
| `BusinessNumberInvalidException` | 400 | 사업자번호 검증 실패 |
| `AuthenticationFailedException` | 401 | 로그인 실패 (전화번호/비밀번호 불일치) |
| `InvalidTokenException` | 401 | JWT 토큰 무효 |
| `UserNotFoundException` | 404 | 사용자 없음 |
| `InvalidPasswordException` | 400 | 현재 비밀번호 불일치 (프로필 수정) |
### 에러 응답 형식
```json
{
"error": "에러 메시지",
"code": "ERROR_CODE",
"timestamp": "2025-10-22T10:30:00Z"
}
```
---
## 보안 고려사항
### 1. 비밀번호 보안
- **해싱 알고리즘**: bcrypt (Cost Factor 10)
- **원본 노출 방지**: 비밀번호는 해시로만 저장, 평문 로깅 금지
- **에러 메시지**: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (보안 강화)
### 2. JWT 보안
- **만료 시간**: 7일 (Refresh Token 별도 구현 가능)
- **서명 알고리즘**: HS256 또는 RS256
- **Blacklist 관리**: 로그아웃 시 Redis Blacklist에 추가
### 3. 민감 정보 암호화
- **사업자번호**: AES-256 암호화 저장
- **전송 보안**: HTTPS 강제 적용
### 4. 세션 관리
- **세션 저장**: Redis (서버 재시작에도 유지)
- **세션 만료**: 7일 후 자동 삭제 (TTL)
- **동시 세션**: 동일 사용자 다중 세션 허용 (필요 시 제한 가능)
---
## 성능 최적화
### 1. 캐싱 효과
- **사업자번호 검증**: 5초 → 0.1초 (98% 개선)
- **세션 조회**: Redis 사용으로 DB 부하 감소
### 2. 비동기 처리
- **최종 로그인 시각 업데이트**: `@Async`로 비동기 처리
- **응답 시간 개선**: 0.5초 목표 달성
### 3. 데이터베이스 최적화
- **인덱스**: `phone_number`, `email` 컬럼에 Unique Index
- **Connection Pool**: HikariCP 사용 (최소 10개, 최대 50개)
---
## 테스트 전략
### Unit Test
- UserService, AuthenticationService 단위 테스트
- Mock 객체: UserRepository, Redis, 국세청 API
### Integration Test
- Controller → Service → Repository 통합 테스트
- 실제 Redis 및 PostgreSQL 사용 (Testcontainers)
### E2E Test
- Postman 또는 REST Assured로 전체 플로우 테스트
- 회원가입 → 로그인 → 프로필 수정 → 로그아웃
---
## 향후 개선사항
### Phase 2
1. **Refresh Token 구현**: Access Token 만료 시 갱신 메커니즘
2. **소셜 로그인**: 카카오, 네이버, 구글 OAuth 2.0 연동
3. **2FA (Two-Factor Authentication)**: SMS 또는 TOTP 기반 2단계 인증
4. **비밀번호 재설정**: 이메일/SMS를 통한 비밀번호 재설정 기능
### Phase 3
1. **계정 잠금 정책**: 로그인 5회 실패 시 계정 잠금
2. **세션 관리 고도화**: 동시 세션 수 제한, 세션 활성 기록
3. **감사 로그**: 민감 작업(로그인, 비밀번호 변경) 감사 로그 저장
4. **Rate Limiting**: 로그인 API에 Rate Limiting 적용 (사용자당 5회/분)
---
## 참고 문서
- [유저스토리](../../../userstory.md)
- [외부 시퀀스](../outer/사용자인증플로우.puml)
- [논리 아키텍처](../../logical/logical-architecture.md)
- [공통설계원칙](../../../common-principles.md)
- [내부시퀀스설계가이드](../../../../claude/sequence-inner-design.md)
---
**문서 버전**: 1.0
**최종 수정일**: 2025-10-22
**작성자**: System Architect
**변경 사항**: User Service 내부 시퀀스 4개 시나리오 초안 작성 완료
@@ -0,0 +1,263 @@
# Event Service - 내부 시퀀스 설계 완료
## 문서 정보
- **작성일**: 2025-10-22
- **작성자**: System Architect
- **관련 문서**:
- [유저스토리](../../../userstory.md)
- [외부 시퀀스 - 이벤트생성플로우](../outer/이벤트생성플로우.puml)
- [논리 아키텍처](../../logical/logical-architecture.md)
---
## 작성 완료 시나리오 (10개)
### 1. event-목적선택.puml
- **유저스토리**: UFR-EVENT-020
- **기능**: 이벤트 목적 선택 및 저장
- **주요 흐름**:
- POST /api/events/purposes
- EventService → EventRepository → Event DB 저장
- Redis 캐시 저장 (TTL 30분)
- Kafka EventCreated 이벤트 발행
- **특징**: 캐시 히트 시 DB 조회 생략
### 2. event-AI추천요청.puml
- **유저스토리**: UFR-EVENT-030
- **기능**: AI 추천 요청 (Kafka Job 발행)
- **주요 흐름**:
- POST /api/events/{id}/ai-recommendations
- EventService → JobService
- Kafka ai-job 토픽 발행
- Job ID 즉시 반환 (202 Accepted)
- **특징**: 비동기 처리, AI Service는 백그라운드에서 Kafka 구독
### 3. event-추천결과조회.puml
- **유저스토리**: UFR-EVENT-030 (결과 조회)
- **기능**: AI 추천 결과 폴링 조회
- **주요 흐름**:
- GET /api/jobs/{jobId}/status
- JobService → Redis 캐시 조회
- Job 상태에 따라 응답 (COMPLETED/PROCESSING/FAILED)
- **특징**: 최대 30초 동안 폴링 (2초 간격)
### 4. event-이미지생성요청.puml
- **유저스토리**: UFR-CONT-010
- **기능**: 이미지 생성 요청 (Kafka Job 발행)
- **주요 흐름**:
- POST /api/events/{id}/content-generation
- EventService → JobService
- Kafka image-job 토픽 발행
- Job ID 즉시 반환 (202 Accepted)
- **특징**: Content Service는 백그라운드에서 3가지 스타일 생성
### 5. event-이미지결과조회.puml
- **유저스토리**: UFR-CONT-010 (결과 조회)
- **기능**: 이미지 생성 결과 폴링 조회
- **주요 흐름**:
- GET /api/jobs/{jobId}/status
- JobService → Redis 캐시 조회
- 완료 시 3가지 스타일 이미지 URL 반환
- **특징**: 최대 30초 동안 폴링 (3초 간격)
### 6. event-콘텐츠선택.puml
- **유저스토리**: UFR-CONT-020
- **기능**: 선택한 콘텐츠 저장
- **주요 흐름**:
- PUT /api/events/drafts/{id}/content
- EventService → EventRepository
- 선택한 이미지 URL 및 편집 내용 저장
- 캐시 무효화
- **특징**: 텍스트, 색상 편집 내용 적용
### 7. event-최종승인및배포.puml
- **유저스토리**: UFR-EVENT-050
- **기능**: 최종 승인 및 Distribution Service 동기 호출
- **주요 흐름**:
- POST /api/events/{id}/publish
- 이벤트 상태 변경 (DRAFT → APPROVED)
- Kafka EventCreated 이벤트 발행
- Distribution Service 동기 호출 (POST /api/distribution/distribute)
- 배포 완료 후 상태 변경 (APPROVED → ACTIVE)
- **특징**: Circuit Breaker 적용, Timeout 70초
### 8. event-상세조회.puml
- **유저스토리**: UFR-EVENT-060
- **기능**: 이벤트 상세 정보 조회
- **주요 흐름**:
- GET /api/events/{id}
- Redis 캐시 확인 (TTL 5분)
- 캐시 미스 시 DB 조회 (JOIN으로 경품, 배포 이력 포함)
- 사용자 권한 검증
- **특징**: JOIN 쿼리로 관련 데이터 한 번에 조회
### 9. event-목록조회.puml
- **유저스토리**: UFR-EVENT-070
- **기능**: 이벤트 목록 조회 (필터/검색)
- **주요 흐름**:
- GET /api/events?status={status}&keyword={keyword}
- Redis 캐시 확인 (TTL 1분)
- 캐시 미스 시 DB 조회 (필터/검색 조건 적용)
- 페이지네이션 (20개/페이지)
- **특징**: 인덱스 활용 (user_id, status, created_at)
### 10. event-대시보드조회.puml
- **유저스토리**: UFR-EVENT-010
- **기능**: 대시보드 이벤트 목록
- **주요 흐름**:
- GET /api/events/dashboard
- Redis 캐시 확인 (TTL 1분)
- 캐시 미스 시 병렬 조회 (진행중/예정/종료)
- 각 섹션 최대 5개 표시
- **특징**: 병렬 쿼리로 성능 최적화
---
## 설계 원칙 준수 사항
### 1. 공통설계원칙 준수
- ✅ 모든 레이어 표시 (Controller → Service → Repository)
- ✅ 외부 시스템/인프라 `<<E>>` 표시
- ✅ 캐시 접근 명시 (Redis)
- ✅ DB 접근 명시 (PostgreSQL)
- ✅ Kafka 이벤트/Job 발행 표시
### 2. 내부시퀀스설계 가이드 준수
- ✅ 각 시나리오별 독립 파일 생성
- ✅ PlantUML `!theme mono` 적용
- ✅ 명확한 타이틀 (서비스명 + 시나리오 + 유저스토리)
- ✅ 참여자 타입 표시 (<<C>>, <<S>>, <<R>>, <<E>>)
- ✅ 데이터베이스 쿼리 표시
- ✅ 캐싱 전략 표시 (Cache-Aside)
- ✅ 비동기 처리 흐름 표시 (Kafka)
### 3. Event-Driven 아키텍처 반영
- ✅ Kafka Event Topics 발행 (EventCreated)
- ✅ Kafka Job Topics 발행 (ai-job, image-job)
- ✅ 비동기 작업 Job ID 즉시 반환 (202 Accepted)
- ✅ 폴링 방식 결과 조회 (GET /api/jobs/{jobId}/status)
### 4. Resilience 패턴 명시
- ✅ Circuit Breaker 적용 표시 (Distribution Service 호출)
- ✅ Timeout 설정 표시 (70초)
- ✅ 캐싱 전략 표시 (TTL 설정)
---
## 검증 체크리스트
### 유저스토리 매칭
- [x] UFR-EVENT-010: 대시보드 이벤트 목록 → event-대시보드조회.puml
- [x] UFR-EVENT-020: 이벤트 목적 선택 → event-목적선택.puml
- [x] UFR-EVENT-030: AI 이벤트 추천 → event-AI추천요청.puml, event-추천결과조회.puml
- [x] UFR-EVENT-040: 배포 채널 선택 → (최종승인에 포함)
- [x] UFR-EVENT-050: 최종 승인 및 배포 → event-최종승인및배포.puml
- [x] UFR-EVENT-060: 이벤트 상세 조회 → event-상세조회.puml
- [x] UFR-EVENT-070: 이벤트 목록 관리 → event-목록조회.puml
- [x] UFR-CONT-010: SNS 이미지 생성 → event-이미지생성요청.puml, event-이미지결과조회.puml
- [x] UFR-CONT-020: 콘텐츠 편집 → event-콘텐츠선택.puml
### 외부 시퀀스 일치성
- [x] Kafka Job 발행 (AI 추천) - ai-job 토픽
- [x] Kafka Job 발행 (이미지 생성) - image-job 토픽
- [x] Kafka Event 발행 (EventCreated) - event-topic
- [x] Distribution Service 동기 호출 (REST API)
- [x] Redis 캐싱 전략 (Cache-Aside)
- [x] Job 폴링 방식 (5초 간격 AI, 3초 간격 이미지)
### 논리 아키텍처 일치성
- [x] Event Service 책임 범위
- [x] Kafka 통합 메시징 플랫폼
- [x] Redis 캐시 키 패턴
- [x] Database-per-Service 원칙
- [x] Resilience 패턴 적용
---
## 파일 위치
```
design/backend/sequence/inner/
├── event-목적선택.puml
├── event-AI추천요청.puml
├── event-추천결과조회.puml
├── event-이미지생성요청.puml
├── event-이미지결과조회.puml
├── event-콘텐츠선택.puml
├── event-최종승인및배포.puml
├── event-상세조회.puml
├── event-목록조회.puml
└── event-대시보드조회.puml
```
---
## 다이어그램 확인 방법
### PlantUML 렌더링
1. https://www.plantuml.com/plantuml/uml/ 접속
2.`.puml` 파일 내용 붙여넣기
3. 다이어그램 시각적 확인
### 로컬 렌더링 (IntelliJ/VS Code)
- IntelliJ: PlantUML Integration 플러그인 설치
- VS Code: PlantUML 확장 설치
---
## 주요 설계 결정사항
### 1. 비동기 처리 전략
- **AI 추천**: Kafka ai-job 토픽 발행 → 비동기 처리 → Job 폴링
- **이미지 생성**: Kafka image-job 토픽 발행 → 비동기 처리 → Job 폴링
- **이유**: 장시간 작업 (10초, 5초)을 동기로 처리 시 사용자 경험 저하
### 2. 배포 동기 처리
- **Distribution Service**: REST API 동기 호출 (POST /api/distribution/distribute)
- **이유**: 배포 완료 여부를 즉시 확인하고 사용자에게 피드백 제공
- **Resilience**: Circuit Breaker + Timeout 70초
### 3. 캐싱 전략
- **목적 선택**: TTL 30분 (임시 저장 성격)
- **상세 조회**: TTL 5분 (자주 조회, 실시간성 중요)
- **목록/대시보드**: TTL 1분 (실시간 업데이트)
- **이유**: 조회 빈도와 실시간성 요구사항에 따라 차등 적용
### 4. Job 상태 관리
- **Redis 캐시**: Job 상태 및 결과 저장 (TTL 1시간)
- **폴링 방식**: 클라이언트가 주기적으로 Job 상태 확인
- **이유**: 간단한 구현, WebSocket 대비 낮은 복잡도
---
## 성능 최적화 포인트
### 1. 캐시 히트율
- 목적 선택: 90% 예상 (재방문 시 캐시 활용)
- 상세 조회: 95% 예상 (자주 조회)
- 목록/대시보드: 90% 예상 (1분 TTL로 대부분 캐시 활용)
### 2. 데이터베이스 최적화
- 인덱스: user_id, status, created_at
- JOIN 최적화: 상세 조회 시 관련 데이터 한 번에 조회
- 페이지네이션: 20개/페이지로 쿼리 부하 감소
### 3. 병렬 처리
- 대시보드 조회: 진행중/예정/종료 병렬 쿼리
- 이미지 생성: 3가지 스타일 병렬 생성 (Content Service에서)
---
## 향후 개선 방안
### Phase 2 이후
1. **WebSocket 실시간 푸시**: Job 폴링을 WebSocket으로 전환
2. **Event Sourcing**: 모든 상태 변경을 이벤트로 저장
3. **GraphQL**: 클라이언트 맞춤형 데이터 조회
4. **Database Read Replica**: 읽기 부하 분산
---
**문서 작성 완료일**: 2025-10-22
**작성자**: System Architect
**상태**: ✅ 완료 (10개 시나리오 모두 작성)
+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,335 @@
@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 "Event DB" as EventDB <<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 캐시 미스
TrendEngine -> EventDB: 과거 이벤트 데이터 조회\nSELECT * FROM events\nWHERE industry = ?\nAND region LIKE ?\nAND created_at > NOW() - INTERVAL 3 MONTH\nORDER BY roi DESC
EventDB --> TrendEngine: 이벤트 통계 데이터\n{성공 이벤트 리스트, ROI 정보}
TrendEngine -> TrendEngine: 트렌드 패턴 분석
note right
분석 항목:
1. 업종 트렌드
- 최근 3개월 성공 이벤트 유형
- 고객 선호 경품 Top 5
- 효과적인 참여 방법
2. 지역 특성
- 해당 지역 이벤트 성공률
- 지역 고객 연령대/성별 분포
3. 시즌 특성
- 계절별 추천 이벤트
- 특별 시즌 (명절, 기념일)
end note
TrendEngine -> CB: executeWithCircuitBreaker(\nAI API 트렌드 분석 호출)
activate CB
CB -> CB: Circuit Breaker 상태 확인
note right
Circuit Breaker 설정:
- Failure Rate Threshold: 50%
- Timeout: 30초
- Half-Open Wait Duration: 30초
- Permitted Calls in Half-Open: 3
end note
alt Circuit CLOSED (정상)
CB -> AIClient: callAIAPI(\nmethod: "trendAnalysis",\nprompt: 트렌드 분석 프롬프트,\ntimeout: 30초)
activate AIClient
AIClient -> AIClient: 프롬프트 구성
note right
프롬프트 예시:
"당신은 마케팅 트렌드 분석 전문가입니다.
업종: {업종}
지역: {지역}
과거 데이터: {이벤트 통계}
다음을 분석하세요:
1. 업종 트렌드 (성공 이벤트 유형)
2. 지역 특성 (고객 특성)
3. 시즌 특성 (현재 시기 추천)"
end note
AIClient -> ExternalAPI: POST /api/v1/analyze\nAuthorization: Bearer {API_KEY}\nTimeout: 30초
activate ExternalAPI
ExternalAPI --> AIClient: 200 OK\n{트렌드 분석 결과}
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 (30초 초과)
CB --> TrendEngine: TimeoutException
TrendEngine -> TrendEngine: Fallback 실행
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: 10초)
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: 10초)
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: 10초)
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
== 5. Kafka Event 발행 (선택적) ==
Handler -> Producer: publishEventRecommended(\neventId: eventDraftId,\nrecommendations: 3가지추천안)
Producer -> Producer: Kafka 메시지 구성
note right
Event Topic: event-topic
Message: {
eventType: "EventRecommended",
eventId: eventDraftId,
recommendations: [...]
timestamp: ISO8601
}
end note
Producer --> Handler: 이벤트 발행 완료
Handler --> Consumer: ACK (메시지 처리 완료)
deactivate Handler
note over Consumer: Job 처리 완료\n클라이언트는 폴링으로 결과 조회
end
== 예외 처리 ==
note over Handler, Producer
1. AI API 장애 시:
- Circuit Breaker Open
- Fallback: 기본 트렌드 데이터 사용
- Job 상태: COMPLETED (안내 메시지 포함)
2. Timeout (30초 초과):
- Circuit Breaker로 즉시 실패
- Retry 없음 (비동기 Job)
- Job 상태: FAILED
3. Kafka 메시지 처리 실패:
- DLQ로 이동
- 수동 검토 및 재처리
4. Redis 장애:
- 캐싱 스킵, DB만 사용
- Job 상태는 메모리에 임시 저장
end note
@enduml
@@ -0,0 +1,238 @@
@startuml analytics-대시보드조회-캐시미스
!theme mono
title Analytics Service - 대시보드 조회 (Cache MISS + 외부 API 병렬 호출) 내부 시퀀스\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" as Redis
database "Analytics DB" as DB
-> Controller: GET /api/events/{id}/analytics
activate Controller
Controller -> Service: getDashboardData(eventId, userId)
activate Service
Service -> Cache: get("analytics:dashboard:{eventId}")
activate Cache
Cache -> Redis: GET analytics:dashboard:{eventId}
activate Redis
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: SELECT event_stats\nWHERE event_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 3개 생성
- 우리동네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: 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)
CB --> ChannelService: SNSStats\n- Instagram: likes 300, comments 50\n- Naver: views 2000\n- Kakao: shares 100
deactivate CB
ChannelService --> Service: SNSStats
deactivate ChannelService
end
|||
== 3. 데이터 통합 및 ROI 계산 ==
Service -> Service: mergeChannelStats(\n wooriTV, genieTV, 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=300\n)
activate Cache
Cache -> Redis: SET analytics:dashboard:{eventId}\nvalue={통합 데이터}\nEX 300
activate Redis
Redis --> Cache: OK
deactivate Redis
Cache --> Service: OK
deactivate Cache
note right of Service
**캐싱 완료**
- TTL: 300초 (5분)
- 다음 조회 시 Cache HIT
- 예상 크기: 5KB
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
@enduml
@@ -0,0 +1,72 @@
@startuml analytics-대시보드조회-캐시히트
!theme mono
title Analytics Service - 대시보드 조회 (Cache HIT) 내부 시퀀스\n(UFR-ANAL-010: 실시간 성과분석 대시보드 조회)
participant "AnalyticsController" as Controller
participant "AnalyticsService" as Service
participant "CacheService" as Cache
participant "Redis" as Redis
-> 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: 300초 (5분)
end note
Cache -> Redis: GET analytics:dashboard:{eventId}
activate Redis
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
@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: INSERT INTO channel_stats (\n event_id,\n channel_name,\n status,\n expected_views,\n distributed_at\n) VALUES (?, ?, ?, ?, ?)\nON CONFLICT (event_id, channel_name)\nDO UPDATE SET\n status = EXCLUDED.status,\n expected_views = EXCLUDED.expected_views,\n distributed_at = EXCLUDED.distributed_at
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: UPDATE event_stats\nSET total_views = ?,\n updated_at = NOW()\nWHERE event_id = ?
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: INSERT INTO event_stats (\n event_id,\n store_id,\n title,\n objective,\n participant_count,\n total_views,\n estimated_roi,\n sales_growth_rate,\n created_at\n) VALUES (?, ?, ?, ?, 0, 0, 0, 0, ?)
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: UPDATE event_stats\nSET participant_count = participant_count + 1,\n updated_at = NOW()\nWHERE event_id = ?
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,232 @@
@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 "CDNUploader" as CDN
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: 심플 이미지 URL
deactivate SDClient
Generator -> CDN: CDN 업로드 요청\n{imageUrl, eventId, style: SIMPLE}
activate CDN
CDN --> Generator: CDN URL (심플)
deactivate CDN
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: 심플 이미지 URL
deactivate DALLEClient
Generator -> CDN: CDN 업로드 요청\n{imageUrl, eventId, style: SIMPLE}
activate CDN
CDN --> Generator: CDN URL (심플)
deactivate CDN
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: 화려한 이미지 URL
deactivate SDClient
Generator -> CDN: CDN 업로드 요청\n{imageUrl, eventId, style: FANCY}
activate CDN
CDN --> Generator: CDN URL (화려한)
deactivate CDN
else API 실패
SDClient --> Generator: 실패 응답
deactivate SDClient
Generator -> DALLEClient: Fallback - DALL-E API 호출
activate DALLEClient
alt Fallback 성공
DALLEClient --> Generator: 화려한 이미지 URL
deactivate DALLEClient
Generator -> CDN: CDN 업로드
activate CDN
CDN --> Generator: CDN URL (화려한)
deactivate CDN
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: 트렌디 이미지 URL
deactivate SDClient
Generator -> CDN: CDN 업로드 요청\n{imageUrl, eventId, style: TRENDY}
activate CDN
CDN --> Generator: CDN URL (트렌디)
deactivate CDN
else API 실패
SDClient --> Generator: 실패 응답
deactivate SDClient
Generator -> DALLEClient: Fallback - DALL-E API 호출
activate DALLEClient
alt Fallback 성공
DALLEClient --> Generator: 트렌디 이미지 URL
deactivate DALLEClient
Generator -> CDN: CDN 업로드
activate CDN
CDN --> Generator: CDN URL (트렌디)
deactivate CDN
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: 이미지 URL 캐싱\nkey: content:image:{eventDraftId}\nTTL: 7일
activate Cache
Cache -> Redis: SET content:image:{eventDraftId}\n{simple, fancy, trendy}\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
note over Handler: Kafka Event 발행\nContentCreated\n{jobId, eventDraftId, imageUrls}
Handler --> Consumer: 처리 완료
end
deactivate Handler
note over Consumer, Redis
**Resilience 패턴 적용**
- Circuit Breaker: 실패율 50% 초과 시 Open
- Timeout: 20초
- Fallback: Stable Diffusion 실패 시 DALL-E, 모두 실패 시 기본 템플릿
- Cache-Aside: Redis 캐싱 (TTL 7일)
**처리 시간**
- 캐시 HIT: 0.1초
- 캐시 MISS: 5초 이내 (병렬 처리)
**병렬 처리**
- 3가지 스타일 동시 생성 (par 블록)
- 독립적인 스레드 풀 사용
**CDN 업로드**
- 이미지 생성 후 CDN 업로드
- CDN URL 반환 및 캐싱
end note
@enduml
@@ -0,0 +1,276 @@
@startuml distribution-다중채널배포
!theme mono
title Distribution Service - 다중 채널 배포 (UFR-DIST-010)
actor Client
participant "Event Service" as EventSvc
participant "Distribution\nREST API" as API
participant "Distribution\nController" as Controller
participant "Distribution\nService" as Service
participant "Circuit Breaker\nManager" as CB
participant "Channel\nDistributor" as Distributor
participant "Retry Handler" as Retry
database "Event DB" as DB
queue "Kafka" as Kafka
participant "우리동네TV API" as WooridongneTV
participant "링고비즈 API" as RingoBiz
participant "지니TV API" as GenieTV
participant "SNS APIs" as SNS
participant "Redis Cache" as Cache
== 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: 배포 이력 초기화\nINSERT distribution_logs\n{eventId, status: PENDING}
DB --> Service: 배포 이력 ID
note over Service: 배포 시작 상태로 변경
Service -> DB: UPDATE distribution_logs\nSET status = 'IN_PROGRESS'
== Circuit Breaker 및 Bulkhead 초기화 ==
Service -> CB: 채널별 Circuit Breaker 상태 확인
CB --> Service: 모든 Circuit Breaker 상태\n(CLOSED/OPEN/HALF_OPEN)
note over Service: Bulkhead 패턴 적용\n채널별 독립 스레드 풀
== 다중 채널 병렬 배포 (Parallel) ==
par 우리동네TV 배포
alt 채널 선택됨
Service -> Distributor: distributeToWooridongneTV\n(eventId, contentUrls)
activate Distributor
Distributor -> CB: checkCircuitBreaker("WooridongneTV")
alt Circuit Breaker OPEN
CB --> Distributor: 서킷 오픈 상태\n(즉시 실패)
Distributor -> DB: 배포 채널 로그 저장\n{channel: WooridongneTV, status: FAILED, reason: Circuit Open}
Distributor --> Service: 실패 (Circuit Open)
else Circuit Breaker CLOSED 또는 HALF_OPEN
CB --> Distributor: 요청 허용
Distributor -> Retry: executeWithRetry(() -> callWooridongneTV())
activate Retry
loop Retry 최대 3회 (지수 백오프: 1초, 2초, 4초)
Retry -> WooridongneTV: POST /api/upload-video\n{eventId, videoUrl, region, schedule}
activate WooridongneTV
alt 성공
WooridongneTV --> Retry: 200 OK\n{distributionId, estimatedViews}
deactivate WooridongneTV
Retry --> Distributor: 배포 성공
Distributor -> CB: recordSuccess("WooridongneTV")
Distributor -> DB: 배포 채널 로그 저장\n{channel: WooridongneTV, status: SUCCESS, distributionId}
Distributor --> Service: 성공\n{channel, distributionId, estimatedViews}
else 실패 (일시적 오류)
WooridongneTV --> Retry: 500 Internal Server Error
deactivate WooridongneTV
note over Retry: 지수 백오프 대기\n(1초 → 2초 → 4초)
end
end
alt 3회 모두 실패
Retry --> Distributor: 배포 실패 (Retry 소진)
deactivate Retry
Distributor -> CB: recordFailure("WooridongneTV")
note over CB: 실패율 50% 초과 시\nCircuit Open (30초)
Distributor -> DB: 배포 채널 로그 저장\n{channel: WooridongneTV, status: FAILED, retries: 3}
Distributor --> Service: 실패 (Retry 소진)
end
end
deactivate Distributor
end
alt 채널 선택됨
Service -> Distributor: distributeToRingoBiz\n(eventId, phoneNumber, audioUrl)
activate Distributor
Distributor -> CB: checkCircuitBreaker("RingoBiz")
alt Circuit Breaker CLOSED 또는 HALF_OPEN
CB --> Distributor: 요청 허용
Distributor -> Retry: executeWithRetry(() -> callRingoBiz())
activate Retry
loop Retry 최대 3회
Retry -> RingoBiz: POST /api/update-ringtone\n{phoneNumber, audioUrl}
activate RingoBiz
alt 성공
RingoBiz --> Retry: 200 OK\n{updateTimestamp}
deactivate RingoBiz
Retry --> Distributor: 배포 성공
deactivate Retry
Distributor -> CB: recordSuccess("RingoBiz")
Distributor -> DB: 배포 채널 로그 저장\n{channel: RingoBiz, status: SUCCESS}
Distributor --> Service: 성공\n{channel, updateTimestamp}
else 실패
RingoBiz --> Retry: 500 Error
deactivate RingoBiz
end
end
alt 3회 모두 실패
Retry --> Distributor: 배포 실패
deactivate Retry
Distributor -> CB: recordFailure("RingoBiz")
Distributor -> DB: 배포 채널 로그 저장\n{channel: RingoBiz, status: FAILED}
Distributor --> Service: 실패
end
else Circuit Breaker OPEN
CB --> Distributor: 서킷 오픈 상태
Distributor -> DB: 배포 채널 로그 저장\n{channel: RingoBiz, status: FAILED, reason: Circuit Open}
Distributor --> Service: 실패 (Circuit Open)
end
deactivate Distributor
end
alt 채널 선택됨
Service -> Distributor: distributeToGenieTV\n(eventId, region, schedule, budget)
activate Distributor
Distributor -> CB: checkCircuitBreaker("GenieTV")
alt Circuit Breaker CLOSED 또는 HALF_OPEN
CB --> Distributor: 요청 허용
Distributor -> Retry: executeWithRetry(() -> callGenieTV())
activate Retry
loop Retry 최대 3회
Retry -> GenieTV: POST /api/register-ad\n{eventId, contentUrl, region, schedule, budget}
activate GenieTV
alt 성공
GenieTV --> Retry: 200 OK\n{adId, impressionSchedule}
deactivate GenieTV
Retry --> Distributor: 배포 성공
deactivate Retry
Distributor -> CB: recordSuccess("GenieTV")
Distributor -> DB: 배포 채널 로그 저장\n{channel: GenieTV, status: SUCCESS, adId}
Distributor --> Service: 성공\n{channel, adId, schedule}
else 실패
GenieTV --> Retry: 500 Error
deactivate GenieTV
end
end
alt 3회 모두 실패
Retry --> Distributor: 배포 실패
deactivate Retry
Distributor -> CB: recordFailure("GenieTV")
Distributor -> DB: 배포 채널 로그 저장\n{channel: GenieTV, status: FAILED}
Distributor --> Service: 실패
end
else Circuit Breaker OPEN
CB --> Distributor: 서킷 오픈 상태
Distributor -> DB: 배포 채널 로그 저장\n{channel: GenieTV, status: FAILED, reason: Circuit Open}
Distributor --> Service: 실패 (Circuit Open)
end
deactivate Distributor
end
alt Instagram 선택됨
Service -> Distributor: distributeToInstagram\n(eventId, imageUrl, caption, hashtags)
activate Distributor
Distributor -> CB: checkCircuitBreaker("Instagram")
alt Circuit Breaker CLOSED 또는 HALF_OPEN
CB --> Distributor: 요청 허용
Distributor -> Retry: executeWithRetry(() -> callInstagram())
activate Retry
loop Retry 최대 3회
Retry -> SNS: POST /instagram/api/posts\n{imageUrl, caption, hashtags}
activate SNS
alt 성공
SNS --> Retry: 200 OK\n{postUrl, postId}
deactivate SNS
Retry --> Distributor: 배포 성공
deactivate Retry
Distributor -> CB: recordSuccess("Instagram")
Distributor -> DB: 배포 채널 로그 저장\n{channel: Instagram, status: SUCCESS, postUrl}
Distributor --> Service: 성공\n{channel, postUrl}
else 실패
SNS --> Retry: 500 Error
deactivate SNS
end
end
alt 3회 모두 실패
Retry --> Distributor: 배포 실패
deactivate Retry
Distributor -> CB: recordFailure("Instagram")
Distributor -> DB: 배포 채널 로그 저장\n{channel: Instagram, status: FAILED}
Distributor --> Service: 실패
end
else Circuit Breaker OPEN
CB --> Distributor: 서킷 오픈 상태
Distributor -> DB: 배포 채널 로그 저장\n{channel: Instagram, status: FAILED, reason: Circuit Open}
Distributor --> Service: 실패 (Circuit Open)
end
deactivate Distributor
end
alt Naver Blog 선택됨
Service -> Distributor: distributeToNaverBlog\n(eventId, imageUrl, content)
activate Distributor
note over Distributor: Naver Blog 배포 로직\n(Instagram과 동일 패턴)
Distributor --> Service: 성공 또는 실패
deactivate Distributor
end
alt Kakao Channel 선택됨
Service -> Distributor: distributeToKakaoChannel\n(eventId, imageUrl, message)
activate Distributor
note over Distributor: Kakao Channel 배포 로직\n(Instagram과 동일 패턴)
Distributor --> Service: 성공 또는 실패
deactivate Distributor
end
end
note over Service: 모든 채널 배포 완료\n(1분 이내)
== 배포 결과 집계 및 저장 ==
Service -> Service: 채널별 배포 결과 집계\n성공: [list], 실패: [list]
alt 모든 채널 성공
Service -> DB: UPDATE distribution_logs\nSET status = 'COMPLETED', completed_at = NOW()
else 일부 채널 실패
Service -> DB: UPDATE distribution_logs\nSET status = 'PARTIAL_FAILURE', completed_at = NOW()
note over Service: 실패한 채널 정보 저장
else 모든 채널 실패
Service -> DB: UPDATE distribution_logs\nSET status = 'FAILED', completed_at = NOW()
end
== Kafka 이벤트 발행 ==
Service -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, channels[], results[], completedAt}
note over Kafka: Analytics Service 구독\n실시간 통계 업데이트
Service -> Cache: 배포 상태 캐싱\nkey: distribution:{eventId}\nvalue: {status, results[]}\nTTL: 1시간
== REST API 동기 응답 ==
Service --> Controller: 배포 완료 응답\n{status, successChannels[], failedChannels[]}
deactivate Service
Controller --> API: DistributionResponse\n{eventId, status, results[]}
deactivate Controller
API --> EventSvc: 200 OK\n{distributionId, status, results[]}
deactivate API
note over EventSvc: 배포 완료 응답 수신\n이벤트 상태 업데이트\nAPPROVED → ACTIVE
== 배포 실패 처리 (비동기) ==
note over Service: 실패한 채널은\n- 수동 재시도 가능\n- 알림 발송 (추후 구현)\n- Circuit Open 시 30초 후\n 자동 Half-Open 전환
@enduml
@@ -0,0 +1,169 @@
@startuml distribution-배포상태조회
!theme mono
title Distribution Service - 배포 상태 조회 (UFR-DIST-020)
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
participant "Distribution\nREST API" as API
participant "Distribution\nController" as Controller
participant "Distribution\nService" as Service
participant "Redis Cache" as Cache
database "Event DB" as DB
participant "Circuit Breaker\nManager" as CB
== 배포 상태 조회 요청 ==
User -> FE: 이벤트 상세 화면\n배포 상태 섹션 확인
activate FE
FE -> Gateway: GET /api/distribution/{eventId}/status
activate Gateway
Gateway -> Gateway: JWT 토큰 검증
Gateway -> API: GET /distribution/{eventId}/status
activate API
API -> Controller: getDistributionStatus(eventId)
activate Controller
Controller -> Service: fetchDistributionStatus(eventId)
activate Service
== Cache-Aside 패턴 적용 ==
Service -> Cache: 캐시 조회\nkey: distribution:{eventId}
activate Cache
alt 캐시 HIT
Cache --> Service: 캐시된 배포 상태\n{status, results[], cachedAt}
deactivate Cache
note over Service: 캐시 TTL: 1시간\n빠른 응답 (0.1초)
Service --> Controller: DistributionStatus\n{eventId, status, channelResults[]}
deactivate Service
else 캐시 MISS
Cache --> Service: null (캐시 없음)
deactivate Cache
== 데이터베이스에서 배포 이력 조회 ==
Service -> DB: SELECT * FROM distribution_logs\nWHERE event_id = :eventId\nORDER BY created_at DESC\nLIMIT 1
activate DB
alt 배포 이력 존재
DB --> Service: DistributionLog\n{id, eventId, status, createdAt, completedAt}
deactivate DB
Service -> DB: SELECT * FROM distribution_channel_logs\nWHERE distribution_log_id = :logId
activate DB
DB --> Service: List<ChannelLog>\n{channel, status, distributionId, postUrl, retries, error}
deactivate DB
== 실시간 채널 상태 확인 (선택적) ==
note over Service: 진행중(IN_PROGRESS) 상태일 때만\n외부 API로 실시간 상태 확인
alt 배포 진행중 (IN_PROGRESS)
loop 각 채널별 상태 확인
Service -> CB: checkCircuitBreaker(channel)
alt Circuit Breaker CLOSED
CB --> Service: 요청 허용
alt 우리동네TV
Service -> DB: SELECT distribution_id FROM distribution_channel_logs\nWHERE channel = 'WooridongneTV'
DB --> Service: distributionId
note over Service: Timeout: 5초\nCircuit Breaker 적용
Service -> "외부 APIs": GET /api/status/{distributionId}
activate "외부 APIs"
alt 성공
"외부 APIs" --> Service: 200 OK\n{status: COMPLETED, views: 1500}
deactivate "외부 APIs"
Service -> DB: UPDATE distribution_channel_logs\nSET status = 'COMPLETED', views = 1500
else 실패 (Timeout/Error)
"외부 APIs" --> Service: 500 Error or Timeout
deactivate "외부 APIs"
Service -> CB: recordFailure(channel)
note over Service: Circuit Breaker 실패율 증가\n50% 초과 시 Circuit Open
end
end
note over Service: 다른 채널(링고비즈, 지니TV, SNS)도\n동일한 패턴으로 상태 확인
else Circuit Breaker OPEN
CB --> Service: 서킷 오픈 상태\n(외부 API 호출 스킵)
note over Service: Fallback: DB 저장 상태 사용\n30초 후 Half-Open 전환
end
end
== 배포 완료 여부 판단 ==
Service -> Service: 모든 채널 상태 확인\n완료/실패 여부 판단
alt 모든 채널 완료
Service -> DB: UPDATE distribution_logs\nSET status = 'COMPLETED', completed_at = NOW()
else 일부 실패
Service -> DB: UPDATE distribution_logs\nSET status = 'PARTIAL_FAILURE'
end
end
== 배포 상태 응답 준비 ==
Service -> Service: 채널별 상태 집계\n{channel, status, distributionId, postUrl, views, error}
Service -> Cache: 배포 상태 캐싱\nkey: distribution:{eventId}\nvalue: {status, results[]}\nTTL: 1시간
Service --> Controller: DistributionStatus\n{eventId, status, channelResults[]}
deactivate Service
else 배포 이력 없음
DB --> Service: null
deactivate DB
Service --> Controller: DistributionStatus\n{eventId, status: NOT_FOUND}
deactivate Service
end
end
== 응답 반환 ==
Controller --> API: DistributionStatusResponse\n{eventId, status, channelResults[]}
deactivate Controller
API --> Gateway: 200 OK\n{eventId, overallStatus, channels[]}
deactivate API
Gateway --> FE: 배포 상태 응답\n{overallStatus, channels[]}
deactivate Gateway
== 프론트엔드 화면 표시 ==
FE -> FE: 채널별 상태 표시\n- 대기중: 회색\n- 진행중: 파란색\n- 완료: 초록색\n- 실패: 빨간색
FE --> User: 배포 상태 시각화\n채널별 세부 정보 표시
deactivate FE
note over User: 배포 상태 정보\n- 우리동네TV: 완료 (배포ID, 조회수)\n- 링고비즈: 완료 (업데이트 시각)\n- 지니TV: 완료 (광고ID, 스케줄)\n- SNS: 완료 (포스팅 URL)
== 재시도 기능 (실패한 채널) ==
alt 실패한 채널 존재
User -> FE: "재시도" 버튼 클릭
FE -> Gateway: POST /api/distribution/{eventId}/retry\n{channels: [failed_channel_list]}
Gateway -> API: 재시도 요청
API -> Controller: retryDistribution(eventId, channels)
Controller -> Service: retryFailedChannels(eventId, channels)
activate Service
note over Service: 실패한 채널만\n다시 배포 시도\n(동일한 Circuit Breaker/Retry 적용)
Service -> DB: 새로운 배포 시도 로그 생성
Service -> Cache: 캐시 무효화
Service --> Controller: 재시도 완료\n{retryStatus}
deactivate Service
Controller --> API: RetryResponse
API --> Gateway: 200 OK
Gateway --> FE: 재시도 결과
FE --> User: "재시도가 완료되었습니다"
end
== 실시간 업데이트 (선택적) ==
note over FE: Frontend는 5초마다\nPolling 또는 WebSocket으로\n배포 상태 자동 갱신\n(Phase 2에서 WebSocket 적용 가능)
@enduml
@@ -0,0 +1,56 @@
@startuml event-AI추천요청
!theme mono
title Event Service - AI 추천 요청 (Kafka Job 발행) (UFR-EVENT-030)
participant "EventController" as Controller <<C>>
participant "EventService" as Service <<S>>
participant "JobService" as JobSvc <<S>>
participant "EventRepository" as Repo <<R>>
participant "Redis Cache" as Cache <<E>>
database "Event DB" as DB <<E>>
participant "Kafka Producer" as Kafka <<E>>
note over Controller: POST /api/events/{id}/ai-recommendations
Controller -> Service: requestAIRecommendation(eventDraftId, userId)
activate Service
Service -> Repo: findById(eventDraftId)
activate Repo
Repo -> DB: SELECT * FROM event_drafts\nWHERE id = ? AND user_id = ?
activate DB
DB --> Repo: EventDraft
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Service: validateOwnership(userId, eventDraft)
note right: 사용자 권한 검증
Service -> JobSvc: createAIJob(eventDraft)
activate JobSvc
JobSvc -> JobSvc: generateJobId()
note right: UUID 생성
JobSvc -> Cache: set("job:" + jobId,\n{status: PENDING, createdAt}, TTL=1시간)
activate Cache
Cache --> JobSvc: OK
deactivate Cache
JobSvc -> Kafka: publish(ai-job,\n{jobId, eventDraftId, objective,\nindustry, region, storeInfo})
activate Kafka
note right: Kafka Job Topic:\nai-job-topic
Kafka --> JobSvc: ACK
deactivate Kafka
JobSvc --> Service: JobResponse\n{jobId, status: PENDING}
deactivate JobSvc
Service --> Controller: JobResponse\n{jobId, status: PENDING}
deactivate Service
Controller --> Client: 202 Accepted\n{jobId, status: PENDING}
note over Controller, Kafka: AI Service는 백그라운드에서\nKafka ai-job 토픽을 구독하여\n비동기로 처리
@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: SELECT e.*, COUNT(p.id) as participant_count\nFROM events e\nLEFT JOIN participants p ON e.id = p.event_id\nWHERE e.user_id = ?\nAND e.status = 'ACTIVE'\nGROUP BY e.id\nORDER BY e.created_at DESC\nLIMIT 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: SELECT e.*\nFROM events e\nWHERE e.user_id = ?\nAND e.status = 'APPROVED'\nORDER BY e.approved_at DESC\nLIMIT 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: SELECT e.*, COUNT(p.id) as participant_count\nFROM events e\nLEFT JOIN participants p ON e.id = p.event_id\nWHERE e.user_id = ?\nAND e.status = 'COMPLETED'\nGROUP BY e.id\nORDER BY e.completed_at DESC\nLIMIT 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={status}&keyword={keyword}\n&page={page}&size={size}
Controller -> Service: getEventList(userId, filters, pagination)
activate Service
Service -> Cache: get("events:" + userId + ":" + filters + ":" + page)
activate Cache
alt 캐시 히트
Cache --> Service: Event list data
Service --> Controller: EventListResponse
else 캐시 미스
Cache --> Service: null
deactivate Cache
Service -> Repo: findByUserIdWithFilters(userId, filters, pagination)
activate Repo
alt 필터 있음 (상태별)
Repo -> DB: SELECT e.*, COUNT(p.id) as participant_count\nFROM events e\nLEFT JOIN participants p ON e.id = p.event_id\nWHERE e.user_id = ?\nAND e.status = ?\nGROUP BY e.id\nORDER BY e.created_at DESC\nLIMIT ? OFFSET ?
else 검색 있음 (키워드)
Repo -> DB: SELECT e.*, COUNT(p.id) as participant_count\nFROM events e\nLEFT JOIN participants p ON e.id = p.event_id\nWHERE e.user_id = ?\nAND (e.title LIKE ? OR e.description LIKE ?)\nGROUP BY e.id\nORDER BY e.created_at DESC\nLIMIT ? OFFSET ?
else 필터 없음 (전체)
Repo -> DB: SELECT e.*, COUNT(p.id) as participant_count\nFROM events e\nLEFT JOIN participants p ON e.id = p.event_id\nWHERE e.user_id = ?\nGROUP BY e.id\nORDER BY e.created_at DESC\nLIMIT ? OFFSET ?
end
activate DB
note right: 인덱스 활용:\n- user_id\n- status\n- created_at
DB --> Repo: Event list with participant count
deactivate DB
Repo -> DB: SELECT COUNT(*) FROM events\nWHERE user_id = ? [AND filters]
activate DB
DB --> Repo: totalCount
deactivate DB
Repo --> Service: PagedResult<Event>
deactivate Repo
Service -> Cache: set("events:" + userId + ":" + filters + ":" + page,\npagedResult, TTL=1분)
activate Cache
Cache --> Service: OK
deactivate Cache
end
Service --> Controller: EventListResponse\n{events: [...], totalCount,\ntotalPages, currentPage}
deactivate Service
Controller --> Client: 200 OK\n{events: [\n {eventId, title, period, status,\n participantCount, roi, createdAt},\n ...\n],\ntotalCount, totalPages, currentPage}
note over Controller, DB: 필터 옵션:\n- status: DRAFT, ACTIVE, COMPLETED\n- 기간: 최근 1개월/3개월/6개월/1년\n- 정렬: 최신순, 참여자 많은 순,\n ROI 높은 순\n\n페이지네이션:\n- 기본 20개/페이지\n- 페이지 번호 기반
@enduml
@@ -0,0 +1,51 @@
@startuml event-목적선택
!theme mono
title Event Service - 이벤트 목적 선택 및 저장 (UFR-EVENT-020)
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>>
participant "Kafka Producer" as Kafka <<E>>
note over Controller: POST /api/events/purposes
Controller -> Service: createEventDraft(userId, objective, storeInfo)
activate Service
Service -> Cache: get("purpose:" + userId)
activate Cache
Cache --> Service: null (캐시 미스)
deactivate Cache
Service -> Service: validate(objective, storeInfo)
note right: 목적 유효성 검증\n- 신규 고객 유치\n- 재방문 유도\n- 매출 증대\n- 인지도 향상
Service -> Repo: save(eventDraft)
activate Repo
Repo -> DB: INSERT INTO event_drafts\n(user_id, objective, store_info, status)
activate DB
DB --> Repo: eventDraftId
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Cache: set("purpose:" + userId, eventDraft, TTL=30분)
activate Cache
Cache --> Service: OK
deactivate Cache
Service -> Kafka: publish(EventCreated,\n{eventDraftId, userId, objective, createdAt})
activate Kafka
note right: Kafka Event Topic:\nevent-topic
Kafka --> Service: ACK
deactivate Kafka
Service --> Controller: EventDraftResponse\n{eventDraftId, objective, status}
deactivate Service
Controller --> Client: 200 OK\n{eventDraftId}
note over Controller, Kafka: 캐시 히트 시:\n1. Redis에서 조회 → 즉시 반환\n2. DB 조회 생략
@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: SELECT e.*, p.*, d.*\nFROM events e\nLEFT JOIN event_prizes p ON e.id = p.event_id\nLEFT JOIN distribution_logs d ON e.id = d.event_id\nWHERE e.id = ?
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,45 @@
@startuml event-이미지결과조회
!theme mono
title Event Service - 이미지 생성 결과 폴링 조회
participant "EventController" as Controller <<C>>
participant "JobService" as JobSvc <<S>>
participant "Redis Cache" as Cache <<E>>
note over Controller: GET /api/jobs/{jobId}/status
Controller -> JobSvc: getJobStatus(jobId)
activate JobSvc
JobSvc -> Cache: get("job:" + jobId)
activate Cache
alt 캐시 히트
Cache --> JobSvc: Job data\n{status, result, createdAt}
alt Job 완료 (status: COMPLETED)
JobSvc --> Controller: JobStatusResponse\n{jobId, status: COMPLETED,\nimageUrls: {...}}
Controller --> Client: 200 OK\n{status: COMPLETED,\nimageUrls: {\n simple: "https://cdn.../simple.png",\n fancy: "https://cdn.../fancy.png",\n trendy: "https://cdn.../trendy.png"\n}}
else Job 진행중 (status: PROCESSING)
JobSvc --> Controller: JobStatusResponse\n{jobId, status: PROCESSING,\nprogress: 33%}
Controller --> Client: 200 OK\n{status: PROCESSING,\nprogress: 33%}
note right: 클라이언트는 3초 후\n재요청
else Job 실패 (status: FAILED)
JobSvc --> Controller: JobStatusResponse\n{jobId, status: FAILED, error}
Controller --> Client: 200 OK\n{status: FAILED, error}
end
else 캐시 미스
Cache --> JobSvc: null
JobSvc --> Controller: NotFoundError
Controller --> Client: 404 Not Found\n{error: "Job not found"}
end
deactivate Cache
deactivate JobSvc
note over Controller, Cache: 최대 30초 동안 폴링\n(3초 간격, 최대 10회)\n\n타임아웃 시 클라이언트는\n에러 메시지 표시 및\n"다시 생성" 옵션 제공
@enduml
@@ -0,0 +1,57 @@
@startuml event-이미지생성요청
!theme mono
title Event Service - 이미지 생성 요청 (Kafka Job 발행) (UFR-CONT-010)
participant "EventController" as Controller <<C>>
participant "EventService" as Service <<S>>
participant "JobService" as JobSvc <<S>>
participant "EventRepository" as Repo <<R>>
participant "Redis Cache" as Cache <<E>>
database "Event DB" as DB <<E>>
participant "Kafka Producer" as Kafka <<E>>
note over Controller: POST /api/events/{id}/content-generation
Controller -> Service: requestImageGeneration(eventDraftId, userId)
activate Service
Service -> Repo: findById(eventDraftId)
activate Repo
Repo -> DB: SELECT * FROM event_drafts\nWHERE id = ? AND user_id = ?
activate DB
DB --> Repo: EventDraft
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Service: validateOwnership(userId, eventDraft)
Service -> Service: validateRecommendationSelected()
note right: AI 추천 선택 여부 확인
Service -> JobSvc: createImageJob(eventDraft)
activate JobSvc
JobSvc -> JobSvc: generateJobId()
note right: UUID 생성
JobSvc -> Cache: set("job:" + jobId,\n{status: PENDING, createdAt}, TTL=1시간)
activate Cache
Cache --> JobSvc: OK
deactivate Cache
JobSvc -> Kafka: publish(image-job,\n{jobId, eventDraftId, title, prize,\nbrandColor, logoUrl, storeInfo})
activate Kafka
note right: Kafka Job Topic:\nimage-job-topic
Kafka --> JobSvc: ACK
deactivate Kafka
JobSvc --> Service: JobResponse\n{jobId, status: PENDING}
deactivate JobSvc
Service --> Controller: JobResponse\n{jobId, status: PENDING}
deactivate Service
Controller --> Client: 202 Accepted\n{jobId, status: PENDING}
note over Controller, Kafka: Content Service는 백그라운드에서\nKafka image-job 토픽을 구독하여\n3가지 스타일 이미지 생성\n(심플, 화려한, 트렌디)
@enduml
@@ -0,0 +1,75 @@
@startuml event-최종승인및배포
!theme mono
title Event Service - 최종 승인 및 Distribution Service 동기 호출 (UFR-EVENT-050)
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>>
participant "Distribution Service" as DistSvc <<E>>
participant "Kafka Producer" as Kafka <<E>>
note over Controller: POST /api/events/{id}/publish
Controller -> Service: publishEvent(eventDraftId, userId, selectedChannels)
activate Service
Service -> Repo: findById(eventDraftId)
activate Repo
Repo -> DB: SELECT * FROM event_drafts\nWHERE id = ? AND user_id = ?
activate DB
DB --> Repo: EventDraft
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Service: validateOwnership(userId, eventDraft)
Service -> Service: validatePublishReady()
note right: 발행 준비 검증:\n- 목적 선택 완료\n- AI 추천 선택 완료\n- 콘텐츠 선택 완료\n- 배포 채널 최소 1개
Service -> Repo: updateStatus(eventDraftId, APPROVED)
activate Repo
Repo -> DB: UPDATE event_drafts SET\nstatus = 'APPROVED',\napproved_at = NOW()\nWHERE id = ?
activate DB
DB --> Repo: OK
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Kafka: publish(EventCreated,\n{eventId, userId, title,\nobjective, createdAt})
activate Kafka
note right: Kafka Event Topic:\nevent-topic
Kafka --> Service: ACK
deactivate Kafka
Service -> DistSvc: POST /api/distribution/distribute\n{eventId, channels, content}
activate DistSvc
note right: 동기 호출 (Circuit Breaker 적용)\nTimeout: 70초
DistSvc -> DistSvc: distributeToChannels(eventId, channels)
note right: 다중 채널 병렬 배포:\n- 우리동네TV\n- 링고비즈\n- 지니TV\n- Instagram\n- Naver Blog\n- Kakao Channel
DistSvc --> Service: DistributionResponse\n{distributionId, channelResults}
deactivate DistSvc
Service -> Repo: updateStatus(eventDraftId, ACTIVE)
activate Repo
Repo -> DB: UPDATE event_drafts SET\nstatus = 'ACTIVE',\npublished_at = NOW()\nWHERE id = ?
activate DB
DB --> Repo: OK
deactivate DB
Repo --> Service: Event entity
deactivate Repo
Service -> Cache: delete("purpose:" + userId)
activate Cache
note right: 캐시 무효화
Cache --> Service: OK
deactivate Cache
Service --> Controller: PublishResponse\n{eventId, status: ACTIVE,\ndistributionResults}
deactivate Service
Controller --> Client: 200 OK\n{eventId, distributionResults}
note over Controller, Kafka: Distribution Service는\n배포 완료 후 Kafka에\nDistributionCompleted\n이벤트 발행
@enduml
@@ -0,0 +1,45 @@
@startuml event-추천결과조회
!theme mono
title Event Service - AI 추천 결과 폴링 조회
participant "EventController" as Controller <<C>>
participant "JobService" as JobSvc <<S>>
participant "Redis Cache" as Cache <<E>>
note over Controller: GET /api/jobs/{jobId}/status
Controller -> JobSvc: getJobStatus(jobId)
activate JobSvc
JobSvc -> Cache: get("job:" + jobId)
activate Cache
alt 캐시 히트
Cache --> JobSvc: Job data\n{status, result, createdAt}
alt Job 완료 (status: COMPLETED)
JobSvc --> Controller: JobStatusResponse\n{jobId, status: COMPLETED,\nrecommendations: [...]}
Controller --> Client: 200 OK\n{status: COMPLETED,\nrecommendations: [\n {title, prize, method, cost, roi},\n {title, prize, method, cost, roi},\n {title, prize, method, cost, roi}\n]}
else Job 진행중 (status: PROCESSING)
JobSvc --> Controller: JobStatusResponse\n{jobId, status: PROCESSING}
Controller --> Client: 200 OK\n{status: PROCESSING}
note right: 클라이언트는 2초 후\n재요청
else Job 실패 (status: FAILED)
JobSvc --> Controller: JobStatusResponse\n{jobId, status: FAILED, error}
Controller --> Client: 200 OK\n{status: FAILED, error}
end
else 캐시 미스
Cache --> JobSvc: null
JobSvc --> Controller: NotFoundError
Controller --> Client: 404 Not Found\n{error: "Job not found"}
end
deactivate Cache
deactivate JobSvc
note over Controller, Cache: 최대 30초 동안 폴링\n(2초 간격, 최대 15회)\n\n타임아웃 시 클라이언트는\n에러 메시지 표시
@enduml
@@ -0,0 +1,53 @@
@startuml event-콘텐츠선택
!theme mono
title Event Service - 선택한 콘텐츠 저장 (UFR-CONT-020)
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: PUT /api/events/drafts/{id}/content
Controller -> Service: updateEventContent(eventDraftId, userId,\nselectedImageUrl, editedContent)
activate Service
Service -> Repo: findById(eventDraftId)
activate Repo
Repo -> DB: SELECT * FROM event_drafts\nWHERE id = ? AND user_id = ?
activate DB
DB --> Repo: EventDraft
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Service: validateOwnership(userId, eventDraft)
Service -> Service: validateImageUrl(selectedImageUrl)
note right: 선택한 이미지 URL\n유효성 검증
Service -> Service: applyContentEdits(eventDraft, editedContent)
note right: 편집 내용 적용:\n- 텍스트 수정\n- 색상 변경
Service -> Repo: update(eventDraft)
activate Repo
Repo -> DB: UPDATE event_drafts SET\nselected_image_url = ?,\nedited_title = ?,\nedited_text = ?,\nbackground_color = ?,\ntext_color = ?,\nupdated_at = NOW()\nWHERE id = ?
activate DB
DB --> Repo: OK
deactivate DB
Repo --> Service: EventDraft entity
deactivate Repo
Service -> Cache: delete("purpose:" + userId)
activate Cache
note right: 캐시 무효화
Cache --> Service: OK
deactivate Cache
Service --> Controller: EventContentResponse\n{eventDraftId, selectedImageUrl,\neditedContent}
deactivate Service
Controller --> Client: 200 OK\n{eventDraftId}
note over Controller, Cache: 콘텐츠 편집 내용:\n- 제목 텍스트\n- 경품 정보 텍스트\n- 참여 안내 텍스트\n- 배경색\n- 텍스트 색상\n- 강조 색상
@enduml
@@ -0,0 +1,163 @@
@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
participant "KafkaProducer" as Kafka
== 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 -> Repo: findAllByEventIdAndIsWinner(eventId, false)
activate Repo
Repo -> DB: SELECT * FROM participants\nWHERE event_id = ?\nAND is_winner = false\nORDER BY participated_at ASC
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 -> Service: 이벤트 상태 확인\n- 이벤트 종료 여부\n- 이미 추첨 완료 여부
alt 이벤트가 아직 진행 중
Service --> Controller: EventNotEndedException
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
추첨 알고리즘:
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 트랜잭션 시작
Service -> Repo: updateWinners(winnerIds)
activate Repo
Repo -> DB: UPDATE participants\nSET is_winner = true,\nwon_at = NOW()\nWHERE participant_id IN (?)
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: INSERT INTO draw_logs\n(draw_log_id, event_id, draw_method,\nalgorithm, visit_bonus_applied,\nwinner_count, drawn_at)
activate DB
note right of DB
추첨 로그 저장:
- 추첨 일시 기록
- 알고리즘 버전 기록
- 가산점 적용 여부
- 감사 추적 목적
end note
DB --> LogRepo: 로그 저장 완료
deactivate DB
LogRepo --> Service: DrawLog 엔티티
deactivate LogRepo
Service -> Service: DB 트랜잭션 커밋
Service -> Kafka: Publish Event\n"WinnerSelected"\nTopic: participant-events
activate Kafka
note right of Kafka
Event Payload:
{
"eventId": "UUID",
"winners": [
{
"participantId": "UUID",
"name": "홍길동",
"phone": "010-1234-5678"
}
],
"timestamp": "2025-10-22T15:00:00Z"
}
end note
Kafka --> Service: 이벤트 발행 완료
deactivate Kafka
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,126 @@
@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
Gateway -> Gateway: JWT 토큰 검증\n(선택사항, 비회원 참여 가능)
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: SELECT * FROM participants\nWHERE event_id = ? AND phone_number = ?
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: INSERT INTO participants\n(participant_id, event_id, name, phone_number,\nentry_path, application_number, participated_at,\nconsent_marketing)
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,115 @@
@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: 5분
- 실시간 정확도 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: SELECT p.participant_id, p.name,\np.phone_number, p.entry_path,\np.application_number, p.participated_at,\np.is_winner\nFROM participants p\nWHERE p.event_id = ?\n[AND p.entry_path = ?]\n[AND p.is_winner = ?]\n[AND (p.name LIKE ? OR p.phone_number LIKE ?)]\nORDER BY p.participated_at DESC\nLIMIT ? OFFSET ?
activate DB
note right of DB
동적 쿼리 조건:
- entryPath 필터 (선택)
- isWinner 필터 (선택)
- name/phone 검색 (선택)
- 페이지네이션 (필수)
end note
DB --> Repo: 참여자 목록 결과셋
deactivate DB
Repo -> DB: SELECT COUNT(*)\nFROM participants\nWHERE event_id = ?\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: 5분
activate Cache
note right of Cache
캐시 저장:
- 짧은 TTL (5분)
- 실시간 참여 반영을 위해
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,114 @@
@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{"error": "유효하지 않은 토큰입니다"}
deactivate AuthService
deactivate Controller
else JWT 토큰 유효
== 2단계: Redis 세션 삭제 ==
AuthService -> Redis: DEL 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 확인
end note
AuthService -> JwtProvider: getRemainingExpiration(token)
activate JwtProvider
JwtProvider -> JwtProvider: JWT Claims에서\nexp(만료 시간) 추출\n(현재 시간과 비교)
JwtProvider --> AuthService: remainingSeconds
deactivate JwtProvider
alt 남은 만료 시간 > 0
AuthService -> Redis: SET jwt:blacklist:{token}\n"revoked" (TTL: remainingSeconds)
activate Redis
Redis --> AuthService: Blacklist 추가 완료
deactivate Redis
end
== 4단계: 응답 반환 ==
AuthService -> AuthService: 로그아웃 성공 로그 기록\n(userId, timestamp)
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에서 세션 정보 제거
**클라이언트 측 처리**
- 프론트엔드: LocalStorage 또는 Cookie에서 JWT 토큰 삭제
- 로그인 화면으로 리다이렉트
**성능 최적화**
- Redis 삭제 연산: O(1) 시간 복잡도
- 응답 시간: 0.1초 이내
end note
@enduml
@@ -0,0 +1,129 @@
@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(LoginRequest DTO)
activate Controller
Controller -> Controller: @Valid 어노테이션 검증\n(필수 필드 확인)
Controller -> AuthService: authenticate(phoneNumber, password)
activate AuthService
== 1단계: 사용자 조회 ==
AuthService -> Service: findByPhoneNumber(phoneNumber)
activate Service
Service -> UserRepo: findByPhoneNumber(phoneNumber)
activate UserRepo
UserRepo -> UserDB: SELECT user_id, password_hash,\nrole, name, email\nFROM users\nWHERE phone_number = ?
activate UserDB
UserDB --> UserRepo: 사용자 정보 또는 NULL
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{"error": "전화번호 또는 비밀번호를\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{"error": "전화번호 또는 비밀번호를\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: SET user:session:{token}\n(userId, role, TTL 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: UPDATE users\nSET last_login_at = NOW()\nWHERE user_id = ?
activate UserDB
UserDB -->> UserRepo: 업데이트 완료
deactivate UserDB
UserRepo -->> Service: void
deactivate UserRepo
Service -->> AuthService: void (비동기 완료)
deactivate Service
== 6단계: 응답 반환 ==
AuthService -> AuthService: 응답 DTO 생성\n(LoginResponse)
AuthService --> Controller: LoginResponse\n(token, userId, userName,\nrole, email)
deactivate AuthService
Controller --> Client: 200 OK\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"}
deactivate Controller
end
end
note over Controller, UserDB
**보안 처리**
- 비밀번호: bcrypt compare (원본 노출 안 됨)
- 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (보안 강화)
- JWT 토큰: 7일 만료, 서버 세션과 동기화
**성능 최적화**
- 최종 로그인 시각 업데이트: 비동기 처리 (@Async)
- 응답 시간: 0.5초 목표
end note
@enduml
@@ -0,0 +1,191 @@
@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: SELECT * FROM users\nWHERE user_id = ?
activate UserDB
UserDB --> UserRepo: 사용자 정보
deactivate UserDB
UserRepo --> Service: User 엔티티
deactivate UserRepo
alt 사용자 없음
Service --> Controller: throw UserNotFoundException\n("사용자를 찾을 수 없습니다")
Controller --> Client: 404 Not Found\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{"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단계: 기본 정보 업데이트 ==
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: SELECT * FROM stores\nWHERE user_id = ?
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: BEGIN TRANSACTION
activate UserDB
Service -> UserRepo: save(user)
activate UserRepo
UserRepo -> UserDB: UPDATE users\nSET name = ?, phone_number = ?,\nemail = ?, password_hash = ?,\nupdated_at = NOW()\nWHERE user_id = ?
UserDB --> UserRepo: 업데이트 완료
UserRepo --> Service: User 엔티티
deactivate UserRepo
Service -> StoreRepo: save(store)
activate StoreRepo
StoreRepo -> UserDB: UPDATE stores\nSET store_name = ?, industry = ?,\naddress = ?, business_hours = ?,\nupdated_at = NOW()\nWHERE store_id = ?
UserDB --> StoreRepo: 업데이트 완료
StoreRepo --> Service: Store 엔티티
deactivate StoreRepo
Service -> UserDB: COMMIT TRANSACTION
UserDB --> Service: 트랜잭션 커밋 완료
deactivate UserDB
== 6단계: 캐시 무효화 (선택적) ==
note right of Service
**캐시 무효화 전략**
- 세션 정보는 변경 없음 (JWT 유지)
- 프로필 캐시가 있다면 무효화
end note
alt 프로필 캐시 사용 중
Service -> Redis: DEL 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
note over Controller, UserDB
**Transaction Rollback 처리**
- 트랜잭션 실패 시 자동 Rollback
- User/Store UPDATE 중 하나라도 실패 시 전체 롤백
**보안 처리**
- 비밀번호 변경: 현재 비밀번호 확인 필수
- JWT 인증: Controller에서 @AuthenticationPrincipal로 userId 추출
- 권한 검증: 본인만 수정 가능
**향후 개선사항**
- 전화번호 변경: SMS/이메일 재인증 구현
- 이메일 변경: 이메일 인증 구현
end note
@enduml
@@ -0,0 +1,193 @@
@startuml user-회원가입
!theme mono
title User Service - 회원가입 내부 시퀀스 (UFR-USER-010)
participant "UserController" as Controller <<API Layer>>
participant "UserService" as Service <<Business Layer>>
participant "BusinessValidator" as Validator <<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>>
participant "국세청 API" as NTSApi <<E>>
actor Client
note over Controller, NTSApi
**UFR-USER-010: 회원가입**
- 기본 정보: 이름, 전화번호, 이메일, 비밀번호
- 매장 정보: 매장명, 업종, 주소, 영업시간, 사업자번호
- 사업자번호 검증 (국세청 API)
- 트랜잭션 처리
- JWT 토큰 발급
end note
Client -> Controller: POST /api/users/register\n(RegisterRequest DTO)
activate Controller
Controller -> Controller: @Valid 어노테이션 검증\n(이메일 형식, 비밀번호 8자 이상 등)
Controller -> Service: register(RegisterRequest)
activate Service
== 1단계: 중복 사용자 확인 ==
Service -> UserRepo: findByPhoneNumber(phoneNumber)
activate UserRepo
UserRepo -> UserDB: SELECT * FROM users\nWHERE phone_number = ?
activate UserDB
UserDB --> UserRepo: 조회 결과
deactivate UserDB
UserRepo --> Service: Optional<User>
deactivate UserRepo
alt 중복 사용자 존재
Service --> Controller: throw DuplicateUserException\n("이미 가입된 전화번호입니다")
Controller --> Client: 400 Bad Request\n{"error": "이미 가입된 전화번호입니다"}
deactivate Service
deactivate Controller
else 신규 사용자
== 2단계: 사업자번호 검증 ==
Service -> Validator: validateBusinessNumber(businessNumber)
activate Validator
Validator -> Redis: GET user:business:{사업자번호}
activate Redis
Redis --> Validator: 캐시 확인 결과
deactivate Redis
alt 캐시 HIT (검증 결과 있음)
Validator -> Validator: 캐시된 검증 결과 사용\n(응답 시간: 0.1초)
else 캐시 MISS (검증 필요)
note right of Validator
**Circuit Breaker 설정**
- 실패율: 50% 초과 시 Open
- 타임아웃: 5초
- Half-Open: 30초 후 전환
end note
Validator -> NTSApi: POST /사업자번호_검증\n(사업자번호)\n[Circuit Breaker, Timeout 5초]
activate NTSApi
alt 국세청 API 정상 응답
NTSApi --> Validator: 200 OK\n{"valid": true, "status": "영업중"}
deactivate NTSApi
Validator -> Redis: SET user:business:{사업자번호}\n검증 결과 (TTL 7일)
activate Redis
Redis --> Validator: 캐싱 완료
deactivate Redis
else 국세청 API 장애 (Circuit Breaker Open)
NTSApi --> Validator: 500 Internal Server Error\n또는 Timeout
deactivate NTSApi
note right of Validator
**Resilience 패턴 적용**
- Circuit Breaker: Open
- Retry: 최대 3회 (1초, 2초, 4초)
- Fallback: 검증 스킵 (수동 확인 안내)
end note
Validator -> Validator: Fallback 실행:\n사업자번호 검증 스킵\n(수동 확인 안내 플래그 설정)
end
end
alt 사업자번호 검증 실패 (휴폐업 등)
Validator --> Service: throw BusinessNumberInvalidException\n("유효하지 않은 사업자번호입니다")
deactivate Validator
Service --> Controller: BusinessNumberInvalidException
Controller --> Client: 400 Bad Request\n{"error": "유효하지 않은 사업자번호입니다.\n휴폐업 여부를 확인해주세요."}
deactivate Service
deactivate Controller
else 사업자번호 검증 성공
Validator --> Service: ValidationResult\n(valid: true, needsManualCheck: false)
deactivate Validator
== 3단계: 비밀번호 해싱 ==
Service -> PwdEncoder: encode(rawPassword)
activate PwdEncoder
PwdEncoder -> PwdEncoder: bcrypt 해싱\n(Cost Factor 10)
PwdEncoder --> Service: passwordHash
deactivate PwdEncoder
== 4단계: 사업자번호 암호화 ==
Service -> Service: encryptBusinessNumber(businessNumber)\n(AES-256 암호화)
== 5단계: 데이터베이스 트랜잭션 ==
Service -> UserDB: BEGIN TRANSACTION
activate UserDB
Service -> UserRepo: save(User)\n(name, phoneNumber, email,\npasswordHash, createdAt)
activate UserRepo
UserRepo -> UserDB: INSERT INTO users\n(name, phone_number, email,\npassword_hash, created_at)\nRETURNING user_id
UserDB --> UserRepo: user_id
UserRepo --> Service: User 엔티티\n(userId 포함)
deactivate UserRepo
Service -> StoreRepo: save(Store)\n(userId, storeName, industry,\naddress, businessNumberEncrypted,\nbusinessHours)
activate StoreRepo
StoreRepo -> UserDB: INSERT INTO stores\n(user_id, store_name, industry,\naddress, business_number_encrypted,\nbusiness_hours)\nRETURNING store_id
UserDB --> StoreRepo: store_id
StoreRepo --> Service: Store 엔티티\n(storeId 포함)
deactivate StoreRepo
Service -> UserDB: COMMIT TRANSACTION
UserDB --> Service: 트랜잭션 커밋 완료
deactivate UserDB
== 6단계: JWT 토큰 생성 ==
Service -> JwtProvider: generateToken(userId, role)
activate JwtProvider
JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일)
JwtProvider --> Service: JWT 토큰
deactivate JwtProvider
== 7단계: 세션 저장 ==
Service -> Redis: SET user:session:{token}\n(userId, role, TTL 7일)
activate Redis
Redis --> Service: 세션 저장 완료
deactivate Redis
== 8단계: 응답 반환 ==
Service -> Service: 응답 DTO 생성\n(RegisterResponse)
Service --> Controller: RegisterResponse\n(token, userId, userName,\nstoreId, storeName)
deactivate Service
Controller --> Client: 201 Created\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"storeId": 456,\n"storeName": "맛있는집"}
deactivate Controller
end
end
note over Controller, NTSApi
**Transaction Rollback 처리**
- 트랜잭션 실패 시 자동 Rollback
- User/Store INSERT 중 하나라도 실패 시 전체 롤백
- 예외: DataAccessException, ConstraintViolationException
**Resilience 패턴 요약**
- Circuit Breaker: 국세청 API (실패율 50% 초과 시 Open)
- Retry: 최대 3회 (지수 백오프: 1초, 2초, 4초)
- Timeout: 5초
- Fallback: 사업자번호 검증 스킵 (수동 확인 안내)
**보안 처리**
- 비밀번호: bcrypt 해싱 (Cost Factor 10)
- 사업자번호: AES-256 암호화
end note
@enduml
@@ -3,6 +3,7 @@
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
actor Client
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
@@ -103,13 +104,11 @@ FE --> User: "이미지 생성 중..." (로딩)
note over Content: Kafka Consumer\nimage-job-topic 구독
Kafka --> Content: Consume Job Message\n{jobId, eventDraftId, ...}
par 3가지 스타일 병렬 생성
group parallel
Content -> ImageApi: 심플 스타일 생성 요청
ImageApi --> Content: 심플 이미지 URL
and
Content -> ImageApi: 화려한 스타일 생성 요청
ImageApi --> Content: 화려한 이미지 URL
and
Content -> ImageApi: 트렌디 스타일 생성 요청
ImageApi --> Content: 트렌디 이미지 URL
end
@@ -160,32 +159,27 @@ Event -> Dist: REST API - 배포 요청\nPOST /distributions\n{eventId, channels
note over Dist: Circuit Breaker 적용
par 다중 채널 병렬 배포
group parallel
alt 우리동네TV 선택
Dist -> ChannelApis: 우리동네TV API\n15초 영상 업로드
ChannelApis --> Dist: 배포 완료\n{배포ID, 예상노출수}
end
and
alt 링고비즈 선택
Dist -> ChannelApis: 링고비즈 API\n연결음 업데이트
ChannelApis --> Dist: 업데이트 완료\n{완료시각}
end
and
alt 지니TV 선택
Dist -> ChannelApis: 지니TV API\n광고 등록
ChannelApis --> Dist: 광고 등록 완료\n{광고ID, 스케줄}
end
and
alt Instagram 선택
Dist -> ChannelApis: Instagram API\n포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Naver Blog 선택
Dist -> ChannelApis: Naver API\n블로그 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Kakao Channel 선택
Dist -> ChannelApis: Kakao API\n채널 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
@@ -0,0 +1,210 @@
@startuml 이벤트생성플로우
!theme mono
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
participant "Event Service" as Event
participant "AI Service" as AI
participant "Content Service" as Content
participant "Distribution Service" as Dist
participant "Kafka" as Kafka
participant "Redis Cache" as Cache
database "Event DB" as EventDB
participant "외부 AI API" as AIApi
participant "이미지 생성 API" as ImageApi
participant "배포 채널 APIs" as ChannelApis
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
User -> FE: 이벤트 목적 선택
FE -> Gateway: POST /events/purposes\n{목적, 매장정보}
Gateway -> Event: 이벤트 목적 저장 요청
Event -> Cache: 캐시 조회\nkey: purpose:{userId}
alt 캐시 히트
Cache --> Event: 캐시된 데이터
else 캐시 미스
Event -> EventDB: 이벤트 목적 저장
EventDB --> Event: 저장 완료
Event -> Cache: 캐시 저장\nTTL: 30분
end
Event --> Gateway: 저장 완료\n{eventDraftId}
Gateway --> FE: 200 OK
FE --> User: AI 추천 화면으로 이동
== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) ==
User -> FE: AI 추천 요청
FE -> Gateway: POST /events/recommendations\n{eventDraftId, 목적, 업종, 지역}
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-job-topic 구독
Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...}
AI -> Cache: 트렌드 분석 캐시 조회\nkey: trend:{업종}:{지역}
alt 캐시 히트
Cache --> AI: 캐시된 트렌드 데이터
else 캐시 미스
AI -> EventDB: 과거 이벤트 데이터 조회
EventDB --> AI: 이벤트 통계 데이터
AI -> AIApi: 트렌드 분석 요청\n{업종, 지역, 과거데이터}
AIApi --> AI: 트렌드 분석 결과
AI -> Cache: 트렌드 캐시 저장\nTTL: 1시간
end
AI -> AIApi: 이벤트 추천 요청\n{목적, 트렌드, 매장정보}
AIApi --> AI: 3가지 추천안 생성
AI -> EventDB: 추천 결과 저장
EventDB --> AI: 저장 완료
AI -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
AI -> Kafka: Publish to event-topic\nEventRecommended\n{jobId, eventDraftId, recommendations}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, result}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations}
Gateway --> FE: 추천 결과 반환
FE --> User: 3가지 추천안 표시\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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 콘텐츠 생성 화면으로 이동
== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) ==
User -> FE: 이미지 생성 요청
FE -> Gateway: POST /contents/images\n{eventDraftId, 이벤트정보}
Gateway -> Event: 이미지 생성 요청
Event -> Kafka: Publish to image-job-topic\n{jobId, eventDraftId, 이벤트정보}
Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
Gateway --> FE: 202 Accepted\n{jobId}
FE --> User: "이미지 생성 중..." (로딩)
note over Content: Kafka Consumer\nimage-job-topic 구독
Kafka --> Content: Consume Job Message\n{jobId, eventDraftId, ...}
par 3가지 스타일 병렬 생성
Content -> ImageApi: 심플 스타일 생성 요청
ImageApi --> Content: 심플 이미지 URL
and
Content -> ImageApi: 화려한 스타일 생성 요청
ImageApi --> Content: 화려한 이미지 URL
and
Content -> ImageApi: 트렌디 스타일 생성 요청
ImageApi --> Content: 트렌디 이미지 URL
end
Content -> EventDB: 이미지 URL 저장
EventDB --> Content: 저장 완료
Content -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
Content -> Kafka: Publish to event-topic\nContentCreated\n{jobId, eventDraftId, imageUrls}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, imageUrls}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, imageUrls}
Gateway --> FE: 이미지 URL 반환
FE --> User: 3가지 스타일 카드 표시
else Job 진행중
Event --> 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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 배포 채널 선택 화면으로 이동
== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) ==
User -> FE: 배포 채널 선택\n최종 승인 요청
FE -> Gateway: POST /events/{eventDraftId}/approve\n{선택 채널 목록, 승인}
Gateway -> Event: 최종 승인 처리
Event -> EventDB: 이벤트 상태 변경\nDRAFT → APPROVED
EventDB --> Event: 상태 변경 완료
Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보}
note over Event: 동기 호출로 배포 진행
Event -> Dist: REST API - 배포 요청\nPOST /distributions\n{eventId, channels, content}
note over Dist: Circuit Breaker 적용
par 다중 채널 병렬 배포
alt 우리동네TV 선택
Dist -> ChannelApis: 우리동네TV API\n15초 영상 업로드
ChannelApis --> Dist: 배포 완료\n{배포ID, 예상노출수}
end
and
alt 링고비즈 선택
Dist -> ChannelApis: 링고비즈 API\n연결음 업데이트
ChannelApis --> Dist: 업데이트 완료\n{완료시각}
end
and
alt 지니TV 선택
Dist -> ChannelApis: 지니TV API\n광고 등록
ChannelApis --> Dist: 광고 등록 완료\n{광고ID, 스케줄}
end
and
alt Instagram 선택
Dist -> ChannelApis: Instagram API\n포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Naver Blog 선택
Dist -> ChannelApis: Naver API\n블로그 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Kakao Channel 선택
Dist -> ChannelApis: Kakao API\n채널 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
end
Dist -> EventDB: 배포 이력 저장
EventDB --> Dist: 저장 완료
Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, 배포결과}
Dist --> Event: REST API 응답\n200 OK\n{배포결과, 채널별 상태}
Event -> EventDB: 이벤트 상태 업데이트\nAPPROVED → ACTIVE
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK\n{eventId, 배포결과}
Gateway --> FE: 배포 완료
FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동
note over Event, Dist: 배포 실패 시\n- 채널별 독립 처리\n- 자동 재시도 (최대 3회)\n- Circuit Breaker로 장애 전파 방지\n- 실패한 채널만 재시도 가능
@enduml
@@ -0,0 +1,211 @@
@startuml 이벤트생성플로우
!theme mono
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
actor Client
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
participant "Event Service" as Event
participant "AI Service" as AI
participant "Content Service" as Content
participant "Distribution Service" as Dist
participant "Kafka" as Kafka
participant "Redis Cache" as Cache
database "Event DB" as EventDB
participant "외부 AI API" as AIApi
participant "이미지 생성 API" as ImageApi
participant "배포 채널 APIs" as ChannelApis
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
User -> FE: 이벤트 목적 선택
FE -> Gateway: POST /events/purposes\n{목적, 매장정보}
Gateway -> Event: 이벤트 목적 저장 요청
Event -> Cache: 캐시 조회\nkey: purpose:{userId}
alt 캐시 히트
Cache --> Event: 캐시된 데이터
else 캐시 미스
Event -> EventDB: 이벤트 목적 저장
EventDB --> Event: 저장 완료
Event -> Cache: 캐시 저장\nTTL: 30분
end
Event --> Gateway: 저장 완료\n{eventDraftId}
Gateway --> FE: 200 OK
FE --> User: AI 추천 화면으로 이동
== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) ==
User -> FE: AI 추천 요청
FE -> Gateway: POST /events/recommendations\n{eventDraftId, 목적, 업종, 지역}
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-job-topic 구독
Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...}
AI -> Cache: 트렌드 분석 캐시 조회\nkey: trend:{업종}:{지역}
alt 캐시 히트
Cache --> AI: 캐시된 트렌드 데이터
else 캐시 미스
AI -> EventDB: 과거 이벤트 데이터 조회
EventDB --> AI: 이벤트 통계 데이터
AI -> AIApi: 트렌드 분석 요청\n{업종, 지역, 과거데이터}
AIApi --> AI: 트렌드 분석 결과
AI -> Cache: 트렌드 캐시 저장\nTTL: 1시간
end
AI -> AIApi: 이벤트 추천 요청\n{목적, 트렌드, 매장정보}
AIApi --> AI: 3가지 추천안 생성
AI -> EventDB: 추천 결과 저장
EventDB --> AI: 저장 완료
AI -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
AI -> Kafka: Publish to event-topic\nEventRecommended\n{jobId, eventDraftId, recommendations}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, result}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations}
Gateway --> FE: 추천 결과 반환
FE --> User: 3가지 추천안 표시\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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 콘텐츠 생성 화면으로 이동
== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) ==
User -> FE: 이미지 생성 요청
FE -> Gateway: POST /contents/images\n{eventDraftId, 이벤트정보}
Gateway -> Event: 이미지 생성 요청
Event -> Kafka: Publish to image-job-topic\n{jobId, eventDraftId, 이벤트정보}
Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
Gateway --> FE: 202 Accepted\n{jobId}
FE --> User: "이미지 생성 중..." (로딩)
note over Content: Kafka Consumer\nimage-job-topic 구독
Kafka --> Content: Consume Job Message\n{jobId, eventDraftId, ...}
par 3가지 스타일 병렬 생성
Content -> ImageApi: 심플 스타일 생성 요청
ImageApi --> Content: 심플 이미지 URL
and
Content -> ImageApi: 화려한 스타일 생성 요청
ImageApi --> Content: 화려한 이미지 URL
and
Content -> ImageApi: 트렌디 스타일 생성 요청
ImageApi --> Content: 트렌디 이미지 URL
end
Content -> EventDB: 이미지 URL 저장
EventDB --> Content: 저장 완료
Content -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
Content -> Kafka: Publish to event-topic\nContentCreated\n{jobId, eventDraftId, imageUrls}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, imageUrls}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, imageUrls}
Gateway --> FE: 이미지 URL 반환
FE --> User: 3가지 스타일 카드 표시
else Job 진행중
Event --> 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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 배포 채널 선택 화면으로 이동
== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) ==
User -> FE: 배포 채널 선택\n최종 승인 요청
FE -> Gateway: POST /events/{eventDraftId}/approve\n{선택 채널 목록, 승인}
Gateway -> Event: 최종 승인 처리
Event -> EventDB: 이벤트 상태 변경\nDRAFT → APPROVED
EventDB --> Event: 상태 변경 완료
Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보}
note over Event: 동기 호출로 배포 진행
Event -> Dist: REST API - 배포 요청\nPOST /distributions\n{eventId, channels, content}
note over Dist: Circuit Breaker 적용
par 다중 채널 병렬 배포
alt 우리동네TV 선택
Dist -> ChannelApis: 우리동네TV API\n15초 영상 업로드
ChannelApis --> Dist: 배포 완료\n{배포ID, 예상노출수}
end
and
alt 링고비즈 선택
Dist -> ChannelApis: 링고비즈 API\n연결음 업데이트
ChannelApis --> Dist: 업데이트 완료\n{완료시각}
end
and
alt 지니TV 선택
Dist -> ChannelApis: 지니TV API\n광고 등록
ChannelApis --> Dist: 광고 등록 완료\n{광고ID, 스케줄}
end
and
alt Instagram 선택
Dist -> ChannelApis: Instagram API\n포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Naver Blog 선택
Dist -> ChannelApis: Naver API\n블로그 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Kakao Channel 선택
Dist -> ChannelApis: Kakao API\n채널 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
end
Dist -> EventDB: 배포 이력 저장
EventDB --> Dist: 저장 완료
Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, 배포결과}
Dist --> Event: REST API 응답\n200 OK\n{배포결과, 채널별 상태}
Event -> EventDB: 이벤트 상태 업데이트\nAPPROVED → ACTIVE
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK\n{eventId, 배포결과}
Gateway --> FE: 배포 완료
FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동
note over Event, Dist: 배포 실패 시\n- 채널별 독립 처리\n- 자동 재시도 (최대 3회)\n- Circuit Breaker로 장애 전파 방지\n- 실패한 채널만 재시도 가능
@enduml
@@ -0,0 +1,213 @@
@startuml 이벤트생성플로우
!theme mono
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
actor Client
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
participant "Event Service" as Event
participant "AI Service" as AI
participant "Content Service" as Content
participant "Distribution Service" as Dist
participant "Kafka" as Kafka
participant "Redis Cache" as Cache
database "Event DB" as EventDB
participant "외부 AI API" as AIApi
participant "이미지 생성 API" as ImageApi
participant "배포 채널 APIs" as ChannelApis
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
User -> FE: 이벤트 목적 선택
FE -> Gateway: POST /events/purposes\n{목적, 매장정보}
Gateway -> Event: 이벤트 목적 저장 요청
Event -> Cache: 캐시 조회\nkey: purpose:{userId}
alt 캐시 히트
Cache --> Event: 캐시된 데이터
else 캐시 미스
Event -> EventDB: 이벤트 목적 저장
EventDB --> Event: 저장 완료
Event -> Cache: 캐시 저장\nTTL: 30분
end
Event --> Gateway: 저장 완료\n{eventDraftId}
Gateway --> FE: 200 OK
FE --> User: AI 추천 화면으로 이동
== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) ==
User -> FE: AI 추천 요청
FE -> Gateway: POST /events/recommendations\n{eventDraftId, 목적, 업종, 지역}
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-job-topic 구독
Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...}
AI -> Cache: 트렌드 분석 캐시 조회\nkey: trend:{업종}:{지역}
alt 캐시 히트
Cache --> AI: 캐시된 트렌드 데이터
else 캐시 미스
AI -> EventDB: 과거 이벤트 데이터 조회
EventDB --> AI: 이벤트 통계 데이터
AI -> AIApi: 트렌드 분석 요청\n{업종, 지역, 과거데이터}
AIApi --> AI: 트렌드 분석 결과
AI -> Cache: 트렌드 캐시 저장\nTTL: 1시간
end
AI -> AIApi: 이벤트 추천 요청\n{목적, 트렌드, 매장정보}
AIApi --> AI: 3가지 추천안 생성
AI -> EventDB: 추천 결과 저장
EventDB --> AI: 저장 완료
AI -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
AI -> Kafka: Publish to event-topic\nEventRecommended\n{jobId, eventDraftId, recommendations}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, result}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations}
Gateway --> FE: 추천 결과 반환
FE --> User: 3가지 추천안 표시\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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 콘텐츠 생성 화면으로 이동
== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) ==
User -> FE: 이미지 생성 요청
FE -> Gateway: POST /contents/images\n{eventDraftId, 이벤트정보}
Gateway -> Event: 이미지 생성 요청
Event -> Kafka: Publish to image-job-topic\n{jobId, eventDraftId, 이벤트정보}
Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
Gateway --> FE: 202 Accepted\n{jobId}
FE --> User: "이미지 생성 중..." (로딩)
note over Content: Kafka Consumer\nimage-job-topic 구독
Kafka --> Content: Consume Job Message\n{jobId, eventDraftId, ...}
par
note over : 3가지 스타일 병렬 생성
Content -> ImageApi: 심플 스타일 생성 요청
ImageApi --> Content: 심플 이미지 URL
and
Content -> ImageApi: 화려한 스타일 생성 요청
ImageApi --> Content: 화려한 이미지 URL
and
Content -> ImageApi: 트렌디 스타일 생성 요청
ImageApi --> Content: 트렌디 이미지 URL
end
Content -> EventDB: 이미지 URL 저장
EventDB --> Content: 저장 완료
Content -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
Content -> Kafka: Publish to event-topic\nContentCreated\n{jobId, eventDraftId, imageUrls}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, imageUrls}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, imageUrls}
Gateway --> FE: 이미지 URL 반환
FE --> User: 3가지 스타일 카드 표시
else Job 진행중
Event --> 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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 배포 채널 선택 화면으로 이동
== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) ==
User -> FE: 배포 채널 선택\n최종 승인 요청
FE -> Gateway: POST /events/{eventDraftId}/approve\n{선택 채널 목록, 승인}
Gateway -> Event: 최종 승인 처리
Event -> EventDB: 이벤트 상태 변경\nDRAFT → APPROVED
EventDB --> Event: 상태 변경 완료
Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보}
note over Event: 동기 호출로 배포 진행
Event -> Dist: REST API - 배포 요청\nPOST /distributions\n{eventId, channels, content}
note over Dist: Circuit Breaker 적용
par
note over : 다중 채널 병렬 배포
alt 우리동네TV 선택
Dist -> ChannelApis: 우리동네TV API\n15초 영상 업로드
ChannelApis --> Dist: 배포 완료\n{배포ID, 예상노출수}
end
and
alt 링고비즈 선택
Dist -> ChannelApis: 링고비즈 API\n연결음 업데이트
ChannelApis --> Dist: 업데이트 완료\n{완료시각}
end
and
alt 지니TV 선택
Dist -> ChannelApis: 지니TV API\n광고 등록
ChannelApis --> Dist: 광고 등록 완료\n{광고ID, 스케줄}
end
and
alt Instagram 선택
Dist -> ChannelApis: Instagram API\n포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Naver Blog 선택
Dist -> ChannelApis: Naver API\n블로그 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Kakao Channel 선택
Dist -> ChannelApis: Kakao API\n채널 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
end
Dist -> EventDB: 배포 이력 저장
EventDB --> Dist: 저장 완료
Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, 배포결과}
Dist --> Event: REST API 응답\n200 OK\n{배포결과, 채널별 상태}
Event -> EventDB: 이벤트 상태 업데이트\nAPPROVED → ACTIVE
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK\n{eventId, 배포결과}
Gateway --> FE: 배포 완료
FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동
note over Event, Dist: 배포 실패 시\n- 채널별 독립 처리\n- 자동 재시도 (최대 3회)\n- Circuit Breaker로 장애 전파 방지\n- 실패한 채널만 재시도 가능
@enduml
@@ -0,0 +1,211 @@
@startuml 이벤트생성플로우
!theme mono
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
actor Client
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
participant "Event Service" as Event
participant "AI Service" as AI
participant "Content Service" as Content
participant "Distribution Service" as Dist
participant "Kafka" as Kafka
participant "Redis Cache" as Cache
database "Event DB" as EventDB
participant "외부 AI API" as AIApi
participant "이미지 생성 API" as ImageApi
participant "배포 채널 APIs" as ChannelApis
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
User -> FE: 이벤트 목적 선택
FE -> Gateway: POST /events/purposes\n{목적, 매장정보}
Gateway -> Event: 이벤트 목적 저장 요청
Event -> Cache: 캐시 조회\nkey: purpose:{userId}
alt 캐시 히트
Cache --> Event: 캐시된 데이터
else 캐시 미스
Event -> EventDB: 이벤트 목적 저장
EventDB --> Event: 저장 완료
Event -> Cache: 캐시 저장\nTTL: 30분
end
Event --> Gateway: 저장 완료\n{eventDraftId}
Gateway --> FE: 200 OK
FE --> User: AI 추천 화면으로 이동
== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) ==
User -> FE: AI 추천 요청
FE -> Gateway: POST /events/recommendations\n{eventDraftId, 목적, 업종, 지역}
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-job-topic 구독
Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...}
AI -> Cache: 트렌드 분석 캐시 조회\nkey: trend:{업종}:{지역}
alt 캐시 히트
Cache --> AI: 캐시된 트렌드 데이터
else 캐시 미스
AI -> EventDB: 과거 이벤트 데이터 조회
EventDB --> AI: 이벤트 통계 데이터
AI -> AIApi: 트렌드 분석 요청\n{업종, 지역, 과거데이터}
AIApi --> AI: 트렌드 분석 결과
AI -> Cache: 트렌드 캐시 저장\nTTL: 1시간
end
AI -> AIApi: 이벤트 추천 요청\n{목적, 트렌드, 매장정보}
AIApi --> AI: 3가지 추천안 생성
AI -> EventDB: 추천 결과 저장
EventDB --> AI: 저장 완료
AI -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
AI -> Kafka: Publish to event-topic\nEventRecommended\n{jobId, eventDraftId, recommendations}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, result}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations}
Gateway --> FE: 추천 결과 반환
FE --> User: 3가지 추천안 표시\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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 콘텐츠 생성 화면으로 이동
== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) ==
User -> FE: 이미지 생성 요청
FE -> Gateway: POST /contents/images\n{eventDraftId, 이벤트정보}
Gateway -> Event: 이미지 생성 요청
Event -> Kafka: Publish to image-job-topic\n{jobId, eventDraftId, 이벤트정보}
Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
Gateway --> FE: 202 Accepted\n{jobId}
FE --> User: "이미지 생성 중..." (로딩)
note over Content: Kafka Consumer\nimage-job-topic 구독
Kafka --> Content: Consume Job Message\n{jobId, eventDraftId, ...}
par
Content -> ImageApi: 심플 스타일 생성 요청
ImageApi --> Content: 심플 이미지 URL
and
Content -> ImageApi: 화려한 스타일 생성 요청
ImageApi --> Content: 화려한 이미지 URL
and
Content -> ImageApi: 트렌디 스타일 생성 요청
ImageApi --> Content: 트렌디 이미지 URL
end
Content -> EventDB: 이미지 URL 저장
EventDB --> Content: 저장 완료
Content -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
Content -> Kafka: Publish to event-topic\nContentCreated\n{jobId, eventDraftId, imageUrls}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, imageUrls}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, imageUrls}
Gateway --> FE: 이미지 URL 반환
FE --> User: 3가지 스타일 카드 표시
else Job 진행중
Event --> 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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 배포 채널 선택 화면으로 이동
== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) ==
User -> FE: 배포 채널 선택\n최종 승인 요청
FE -> Gateway: POST /events/{eventDraftId}/approve\n{선택 채널 목록, 승인}
Gateway -> Event: 최종 승인 처리
Event -> EventDB: 이벤트 상태 변경\nDRAFT → APPROVED
EventDB --> Event: 상태 변경 완료
Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보}
note over Event: 동기 호출로 배포 진행
Event -> Dist: REST API - 배포 요청\nPOST /distributions\n{eventId, channels, content}
note over Dist: Circuit Breaker 적용
par
alt 우리동네TV 선택
Dist -> ChannelApis: 우리동네TV API\n15초 영상 업로드
ChannelApis --> Dist: 배포 완료\n{배포ID, 예상노출수}
end
and
alt 링고비즈 선택
Dist -> ChannelApis: 링고비즈 API\n연결음 업데이트
ChannelApis --> Dist: 업데이트 완료\n{완료시각}
end
and
alt 지니TV 선택
Dist -> ChannelApis: 지니TV API\n광고 등록
ChannelApis --> Dist: 광고 등록 완료\n{광고ID, 스케줄}
end
and
alt Instagram 선택
Dist -> ChannelApis: Instagram API\n포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Naver Blog 선택
Dist -> ChannelApis: Naver API\n블로그 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Kakao Channel 선택
Dist -> ChannelApis: Kakao API\n채널 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
end
Dist -> EventDB: 배포 이력 저장
EventDB --> Dist: 저장 완료
Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, 배포결과}
Dist --> Event: REST API 응답\n200 OK\n{배포결과, 채널별 상태}
Event -> EventDB: 이벤트 상태 업데이트\nAPPROVED → ACTIVE
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK\n{eventId, 배포결과}
Gateway --> FE: 배포 완료
FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동
note over Event, Dist: 배포 실패 시\n- 채널별 독립 처리\n- 자동 재시도 (최대 3회)\n- Circuit Breaker로 장애 전파 방지\n- 실패한 채널만 재시도 가능
@enduml
@@ -0,0 +1,211 @@
@startuml 이벤트생성플로우
!theme mono
title 이벤트 생성 플로우 - 외부 시퀀스 다이어그램
actor Client
actor "소상공인" as User
participant "Frontend" as FE
participant "API Gateway" as Gateway
participant "Event Service" as Event
participant "AI Service" as AI
participant "Content Service" as Content
participant "Distribution Service" as Dist
participant "Kafka" as Kafka
participant "Redis Cache" as Cache
database "Event DB" as EventDB
participant "외부 AI API" as AIApi
participant "이미지 생성 API" as ImageApi
participant "배포 채널 APIs" as ChannelApis
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
User -> FE: 이벤트 목적 선택
FE -> Gateway: POST /events/purposes\n{목적, 매장정보}
Gateway -> Event: 이벤트 목적 저장 요청
Event -> Cache: 캐시 조회\nkey: purpose:{userId}
alt 캐시 히트
Cache --> Event: 캐시된 데이터
else 캐시 미스
Event -> EventDB: 이벤트 목적 저장
EventDB --> Event: 저장 완료
Event -> Cache: 캐시 저장\nTTL: 30분
end
Event --> Gateway: 저장 완료\n{eventDraftId}
Gateway --> FE: 200 OK
FE --> User: AI 추천 화면으로 이동
== 2. AI 이벤트 추천 - 비동기 처리 (UFR-EVENT-030) ==
User -> FE: AI 추천 요청
FE -> Gateway: POST /events/recommendations\n{eventDraftId, 목적, 업종, 지역}
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-job-topic 구독
Kafka --> AI: Consume Job Message\n{jobId, eventDraftId, ...}
AI -> Cache: 트렌드 분석 캐시 조회\nkey: trend:{업종}:{지역}
alt 캐시 히트
Cache --> AI: 캐시된 트렌드 데이터
else 캐시 미스
AI -> EventDB: 과거 이벤트 데이터 조회
EventDB --> AI: 이벤트 통계 데이터
AI -> AIApi: 트렌드 분석 요청\n{업종, 지역, 과거데이터}
AIApi --> AI: 트렌드 분석 결과
AI -> Cache: 트렌드 캐시 저장\nTTL: 1시간
end
AI -> AIApi: 이벤트 추천 요청\n{목적, 트렌드, 매장정보}
AIApi --> AI: 3가지 추천안 생성
AI -> EventDB: 추천 결과 저장
EventDB --> AI: 저장 완료
AI -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
AI -> Kafka: Publish to event-topic\nEventRecommended\n{jobId, eventDraftId, recommendations}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, result}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, recommendations}
Gateway --> FE: 추천 결과 반환
FE --> User: 3가지 추천안 표시\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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 콘텐츠 생성 화면으로 이동
== 3. SNS 이미지 생성 - 비동기 처리 (UFR-CONT-010) ==
User -> FE: 이미지 생성 요청
FE -> Gateway: POST /contents/images\n{eventDraftId, 이벤트정보}
Gateway -> Event: 이미지 생성 요청
Event -> Kafka: Publish to image-job-topic\n{jobId, eventDraftId, 이벤트정보}
Event --> Gateway: Job 생성 완료\n{jobId, status: PENDING}
Gateway --> FE: 202 Accepted\n{jobId}
FE --> User: "이미지 생성 중..." (로딩)
note over Content: Kafka Consumer\nimage-job-topic 구독
Kafka --> Content: Consume Job Message\n{jobId, eventDraftId, ...}
group parallel
Content -> ImageApi: 심플 스타일 생성 요청
ImageApi --> Content: 심플 이미지 URL
and
Content -> ImageApi: 화려한 스타일 생성 요청
ImageApi --> Content: 화려한 이미지 URL
and
Content -> ImageApi: 트렌디 스타일 생성 요청
ImageApi --> Content: 트렌디 이미지 URL
end
Content -> EventDB: 이미지 URL 저장
EventDB --> Content: 저장 완료
Content -> Cache: Job 상태 업데이트\nkey: job:{jobId}\nstatus: COMPLETED
Content -> Kafka: Publish to event-topic\nContentCreated\n{jobId, eventDraftId, imageUrls}
group Polling으로 상태 확인
loop 상태 확인 (최대 30초)
FE -> Gateway: GET /jobs/{jobId}/status
Gateway -> Event: Job 상태 조회
Event -> Cache: 캐시에서 Job 상태 확인
Cache --> Event: {status, imageUrls}
alt Job 완료
Event --> Gateway: 200 OK\n{status: COMPLETED, imageUrls}
Gateway --> FE: 이미지 URL 반환
FE --> User: 3가지 스타일 카드 표시
else Job 진행중
Event --> 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 -> EventDB: 이벤트 초안 업데이트
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK
Gateway --> FE: 저장 완료
FE --> User: 배포 채널 선택 화면으로 이동
== 4. 최종 승인 및 다중 채널 배포 - 동기 처리 (UFR-EVENT-050) ==
User -> FE: 배포 채널 선택\n최종 승인 요청
FE -> Gateway: POST /events/{eventDraftId}/approve\n{선택 채널 목록, 승인}
Gateway -> Event: 최종 승인 처리
Event -> EventDB: 이벤트 상태 변경\nDRAFT → APPROVED
EventDB --> Event: 상태 변경 완료
Event -> Kafka: Publish to event-topic\nEventCreated\n{eventId, 이벤트정보}
note over Event: 동기 호출로 배포 진행
Event -> Dist: REST API - 배포 요청\nPOST /distributions\n{eventId, channels, content}
note over Dist: Circuit Breaker 적용
group parallel
alt 우리동네TV 선택
Dist -> ChannelApis: 우리동네TV API\n15초 영상 업로드
ChannelApis --> Dist: 배포 완료\n{배포ID, 예상노출수}
end
and
alt 링고비즈 선택
Dist -> ChannelApis: 링고비즈 API\n연결음 업데이트
ChannelApis --> Dist: 업데이트 완료\n{완료시각}
end
and
alt 지니TV 선택
Dist -> ChannelApis: 지니TV API\n광고 등록
ChannelApis --> Dist: 광고 등록 완료\n{광고ID, 스케줄}
end
and
alt Instagram 선택
Dist -> ChannelApis: Instagram API\n포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Naver Blog 선택
Dist -> ChannelApis: Naver API\n블로그 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
and
alt Kakao Channel 선택
Dist -> ChannelApis: Kakao API\n채널 포스팅
ChannelApis --> Dist: 포스팅 완료\n{postUrl}
end
end
Dist -> EventDB: 배포 이력 저장
EventDB --> Dist: 저장 완료
Dist -> Kafka: Publish to event-topic\nDistributionCompleted\n{eventId, 배포결과}
Dist --> Event: REST API 응답\n200 OK\n{배포결과, 채널별 상태}
Event -> EventDB: 이벤트 상태 업데이트\nAPPROVED → ACTIVE
EventDB --> Event: 업데이트 완료
Event --> Gateway: 200 OK\n{eventId, 배포결과}
Gateway --> FE: 배포 완료
FE --> User: "이벤트가 배포되었습니다"\n대시보드로 이동
note over Event, Dist: 배포 실패 시\n- 채널별 독립 처리\n- 자동 재시도 (최대 3회)\n- Circuit Breaker로 장애 전파 방지\n- 실패한 채널만 재시도 가능
@enduml