mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 06:46:25 +00:00
outer 및 inner sequence 동기화 및 한글화
변경 사항: - 이벤트생성플로우 outer: FE → Gateway → User Service 호출 패턴 추가 - user-로그인 inner: 전화번호 → 이메일 기반 인증으로 변경 - user-회원가입 inner: 국세청 API 제거, 이메일 중복검사 추가 - event-목적선택 inner: Gateway 경유, 요청/응답 한글화 - ai-트렌드분석및추천 inner: 과거 이벤트 데이터 제거, Timeout 5분으로 변경 - analytics-대시보드조회 inner: Redis TTL 5분 → 1시간으로 변경 모든 파일에 Repository CRUD 작업 한글 설명 적용 (SQL 제거) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
eea1fff98c
commit
edb7045008
@ -14,7 +14,6 @@ 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>>
|
||||
|
||||
@ -66,24 +65,17 @@ else 유효한 메시지
|
||||
end note
|
||||
|
||||
else 캐시 미스
|
||||
TrendEngine -> EventDB: 과거 이벤트 데이터 조회\n(업종과 지역으로 필터링,\n최근 3개월 이벤트,\nROI 내림차순 정렬)
|
||||
EventDB --> TrendEngine: 이벤트 통계 데이터\n{성공 이벤트 리스트, ROI 정보}
|
||||
note right of TrendEngine
|
||||
**트렌드 분석 입력 데이터**
|
||||
- 업종 정보
|
||||
- 지역 정보
|
||||
- 현재 시즌 (계절, 월)
|
||||
- 이벤트 목적
|
||||
|
||||
TrendEngine -> TrendEngine: 트렌드 패턴 분석
|
||||
note right
|
||||
분석 항목:
|
||||
1. 업종 트렌드
|
||||
- 최근 3개월 성공 이벤트 유형
|
||||
- 고객 선호 경품 Top 5
|
||||
- 효과적인 참여 방법
|
||||
|
||||
2. 지역 특성
|
||||
- 해당 지역 이벤트 성공률
|
||||
- 지역 고객 연령대/성별 분포
|
||||
|
||||
3. 시즌 특성
|
||||
- 계절별 추천 이벤트
|
||||
- 특별 시즌 (명절, 기념일)
|
||||
**외부 AI API 호출**
|
||||
- 과거 이벤트 데이터 사용 안 함
|
||||
- 실시간 시장 트렌드 분석
|
||||
- 업종별/지역별 일반적 특성
|
||||
end note
|
||||
|
||||
TrendEngine -> CB: executeWithCircuitBreaker(\nAI API 트렌드 분석 호출)
|
||||
@ -91,35 +83,44 @@ else 유효한 메시지
|
||||
|
||||
CB -> CB: Circuit Breaker 상태 확인
|
||||
note right
|
||||
Circuit Breaker 설정:
|
||||
**Circuit Breaker 설정**
|
||||
- Failure Rate Threshold: 50%
|
||||
- Timeout: 30초
|
||||
- Half-Open Wait Duration: 30초
|
||||
- Timeout: 5분 (300초)
|
||||
- Half-Open Wait Duration: 1분 (60초)
|
||||
- Permitted Calls in Half-Open: 3
|
||||
- Sliding Window Size: 10
|
||||
end note
|
||||
|
||||
alt Circuit CLOSED (정상)
|
||||
CB -> AIClient: callAIAPI(\nmethod: "trendAnalysis",\nprompt: 트렌드 분석 프롬프트,\ntimeout: 30초)
|
||||
CB -> AIClient: callAIAPI(\nmethod: "trendAnalysis",\nprompt: 트렌드 분석 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
note right
|
||||
프롬프트 예시:
|
||||
note right of AIClient
|
||||
**AI 프롬프트 구성**
|
||||
"당신은 마케팅 트렌드 분석 전문가입니다.
|
||||
업종: {업종}
|
||||
지역: {지역}
|
||||
과거 데이터: {이벤트 통계}
|
||||
|
||||
다음을 분석하세요:
|
||||
1. 업종 트렌드 (성공 이벤트 유형)
|
||||
2. 지역 특성 (고객 특성)
|
||||
3. 시즌 특성 (현재 시기 추천)"
|
||||
**입력 정보**
|
||||
- 업종: {업종}
|
||||
- 지역: {지역}
|
||||
- 현재 시즌: {계절/월}
|
||||
- 이벤트 목적: {목적}
|
||||
|
||||
**분석 요청사항**
|
||||
1. 업종별 일반적 트렌드
|
||||
(업종 특성 기반 효과적인 이벤트 유형)
|
||||
|
||||
2. 지역별 특성
|
||||
(지역 고객 특성, 선호도)
|
||||
|
||||
3. 시즌별 추천
|
||||
(현재 시기에 적합한 이벤트)"
|
||||
end note
|
||||
|
||||
AIClient -> ExternalAPI: POST /api/v1/analyze\nAuthorization: Bearer {API_KEY}\nTimeout: 30초
|
||||
AIClient -> ExternalAPI: AI API 호출\nPOST /api/v1/analyze\nAuthorization: Bearer {API_KEY}\nTimeout: 5분\nPayload: {업종, 지역, 시즌, 목적}
|
||||
activate ExternalAPI
|
||||
|
||||
ExternalAPI --> AIClient: 200 OK\n{트렌드 분석 결과}
|
||||
ExternalAPI --> AIClient: 200 OK\n{"industryTrend": "...",\n"regionalCharacteristics": "...",\n"seasonalRecommendation": "..."}
|
||||
deactivate ExternalAPI
|
||||
|
||||
AIClient -> AIClient: 응답 검증 및 파싱
|
||||
@ -153,9 +154,15 @@ else 유효한 메시지
|
||||
CB -> CB: 연속 성공 시 CLOSED로 전환
|
||||
CB --> TrendEngine: 트렌드 분석 결과
|
||||
|
||||
else Timeout (30초 초과)
|
||||
else Timeout (5분 초과)
|
||||
CB --> TrendEngine: TimeoutException
|
||||
TrendEngine -> TrendEngine: Fallback 실행
|
||||
note right of TrendEngine
|
||||
**Timeout 처리**
|
||||
- 5분 초과 시 즉시 실패
|
||||
- Fallback: 기본 트렌드 사용
|
||||
- 사용자에게 안내 메시지 제공
|
||||
end note
|
||||
TrendEngine -> TrendEngine: Fallback 실행\n(기본 트렌드 템플릿 사용)
|
||||
end
|
||||
end
|
||||
|
||||
@ -178,7 +185,7 @@ else 유효한 메시지
|
||||
group parallel
|
||||
RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 1: 저비용)
|
||||
activate CB
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 저비용 추천 프롬프트,\ntimeout: 10초)
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 저비용 추천 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
@ -207,7 +214,7 @@ else 유효한 메시지
|
||||
|
||||
RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 2: 중비용)
|
||||
activate CB
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 중비용 추천 프롬프트,\ntimeout: 10초)
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 중비용 추천 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
@ -236,7 +243,7 @@ else 유효한 메시지
|
||||
|
||||
RecommendEngine -> CB: executeWithCircuitBreaker(\nAI API 추천 생성 - 옵션 3: 고비용)
|
||||
activate CB
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 고비용 추천 프롬프트,\ntimeout: 10초)
|
||||
CB -> AIClient: callAIAPI(\nmethod: "generateRecommendation",\nprompt: 고비용 추천 프롬프트,\ntimeout: 5분)
|
||||
activate AIClient
|
||||
|
||||
AIClient -> AIClient: 프롬프트 구성
|
||||
@ -299,23 +306,38 @@ end
|
||||
|
||||
== 예외 처리 ==
|
||||
note over Handler, Producer
|
||||
1. AI API 장애 시:
|
||||
- Circuit Breaker Open
|
||||
- Fallback: 기본 트렌드 데이터 사용
|
||||
- Job 상태: COMPLETED (안내 메시지 포함)
|
||||
**AI API 장애 시**
|
||||
- Circuit Breaker Open
|
||||
- Fallback: 기본 트렌드 템플릿 사용
|
||||
- Job 상태: COMPLETED (안내 메시지 포함)
|
||||
- 사용자에게 "AI 분석이 제한적으로 제공됩니다" 안내
|
||||
|
||||
2. Timeout (30초 초과):
|
||||
- Circuit Breaker로 즉시 실패
|
||||
- Retry 없음 (비동기 Job)
|
||||
- Job 상태: FAILED
|
||||
**Timeout (5분 초과)**
|
||||
- Circuit Breaker로 즉시 실패
|
||||
- Retry 없음 (비동기 Job)
|
||||
- Job 상태: FAILED
|
||||
- 사용자에게 재시도 요청 안내
|
||||
|
||||
3. Kafka 메시지 처리 실패:
|
||||
- DLQ로 이동
|
||||
- 수동 검토 및 재처리
|
||||
**Kafka 메시지 처리 실패**
|
||||
- DLQ(Dead Letter Queue)로 이동
|
||||
- 수동 검토 및 재처리
|
||||
- 에러 로그 기록
|
||||
|
||||
4. Redis 장애:
|
||||
- 캐싱 스킵, DB만 사용
|
||||
- Job 상태는 메모리에 임시 저장
|
||||
**Redis 장애**
|
||||
- 캐싱 스킵
|
||||
- Job 상태는 메모리에 임시 저장
|
||||
- 성능 저하 가능 (매 요청마다 AI API 호출)
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 2분 이내
|
||||
- P95 응답 시간: 4분 이내
|
||||
- Circuit Breaker Timeout: 5분
|
||||
- Redis 캐시 TTL: 24시간
|
||||
|
||||
**데이터 처리 원칙**
|
||||
- 과거 이벤트 데이터 사용 안 함
|
||||
- 외부 AI API로 실시간 트렌드 분석
|
||||
- 업종/지역 기반 일반적 마케팅 트렌드 활용
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
||||
@ -34,7 +34,7 @@ note right of Cache
|
||||
- Redis GET 호출
|
||||
- Cache Key 구조:
|
||||
analytics:dashboard:{eventId}
|
||||
- TTL: 300초 (5분)
|
||||
- TTL: 3600초 (1시간)
|
||||
end note
|
||||
|
||||
Cache -> Redis: GET analytics:dashboard:{eventId}
|
||||
@ -265,23 +265,24 @@ else Cache MISS
|
||||
|||
|
||||
== 4. Redis 캐싱 및 응답 ==
|
||||
|
||||
Service -> Cache: set(\n "analytics:dashboard:{eventId}",\n dashboardData,\n TTL=300\n)
|
||||
Service -> Cache: set(\n "analytics:dashboard:{eventId}",\n dashboardData,\n TTL=3600\n)
|
||||
activate Cache
|
||||
|
||||
Cache -> Redis: SET analytics:dashboard:{eventId}\nvalue={통합 데이터}\nEX 300
|
||||
Cache -> Redis: 캐시 저장\nSET analytics:dashboard:{eventId}\nvalue={통합 데이터}\nEX 3600 (1시간)
|
||||
activate Redis
|
||||
|
||||
Redis --> Cache: OK
|
||||
Redis --> Cache: OK (저장 완료)
|
||||
deactivate Redis
|
||||
|
||||
Cache --> Service: OK
|
||||
Cache --> Service: OK (캐싱 완료)
|
||||
deactivate Cache
|
||||
|
||||
note right of Service
|
||||
**캐싱 완료**
|
||||
- TTL: 300초 (5분)
|
||||
- TTL: 3600초 (1시간)
|
||||
- 다음 조회 시 Cache HIT
|
||||
- 예상 크기: 5KB
|
||||
- 갱신 주기: 1시간마다 새 데이터 조회
|
||||
end note
|
||||
|
||||
Service --> Controller: DashboardResponse\n(200 OK)
|
||||
|
||||
@ -3,49 +3,108 @@
|
||||
|
||||
title Event Service - 이벤트 목적 선택 및 저장 (UFR-EVENT-020)
|
||||
|
||||
participant "EventController" as Controller <<C>>
|
||||
participant "EventService" as Service <<S>>
|
||||
participant "EventRepository" as Repo <<R>>
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
participant "Kafka Producer" as Kafka <<E>>
|
||||
actor Client
|
||||
|
||||
note over Controller, DB
|
||||
**UFR-EVENT-020: 이벤트 목적 선택 및 저장**
|
||||
- 목적 선택: 신규 고객 유치, 재방문 유도, 매출 증대, 인지도 향상
|
||||
- Redis 캐시 사용 (TTL: 30분)
|
||||
- Kafka 이벤트 발행 (EventDraftCreated)
|
||||
- 사용자 및 매장 정보는 User Service에서 조회 후 전달됨
|
||||
end note
|
||||
|
||||
Client -> Controller: POST /api/events/purposes\n{"userId": 123,\n"objective": "신규 고객 유치",\n"storeName": "맛있는집",\n"industry": "음식점",\n"address": "서울시 강남구"}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 입력값 검증\n(필수 필드, 목적 유효성 확인)
|
||||
|
||||
note over Controller: POST /api/events/purposes
|
||||
Controller -> Service: createEventDraft(userId, objective, storeInfo)
|
||||
activate Service
|
||||
|
||||
Service -> Cache: get("purpose:" + userId)
|
||||
== 1단계: Redis 캐시 확인 ==
|
||||
|
||||
Service -> Cache: 캐시 조회\nKey: draft:event:{userId}\n(기존 작성 중인 이벤트 확인)
|
||||
activate Cache
|
||||
Cache --> Service: null (캐시 미스)
|
||||
deactivate Cache
|
||||
|
||||
Service -> Service: validate(objective, storeInfo)
|
||||
note right: 목적 유효성 검증\n- 신규 고객 유치\n- 재방문 유도\n- 매출 증대\n- 인지도 향상
|
||||
== 2단계: 목적 유효성 검증 ==
|
||||
|
||||
Service -> Service: 목적 유효성 검증\n- 신규 고객 유치\n- 재방문 유도\n- 매출 증대\n- 인지도 향상
|
||||
|
||||
Service -> Service: 매장 정보 유효성 검증\n(매장명, 업종, 주소)
|
||||
|
||||
== 3단계: 이벤트 초안 저장 ==
|
||||
|
||||
Service -> Repo: save(eventDraft)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 저장\n(사용자ID, 목적, 매장정보,\n상태를 저장)
|
||||
Repo -> DB: 이벤트 초안 저장\n(사용자ID, 목적, 매장명,\n업종, 주소, 상태=DRAFT,\n생성일시)\n저장 후 이벤트초안ID 반환
|
||||
activate DB
|
||||
DB --> Repo: eventDraftId
|
||||
DB --> Repo: 생성된 이벤트초안ID
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
Repo --> Service: EventDraft 엔티티\n(eventDraftId 포함)
|
||||
deactivate Repo
|
||||
|
||||
Service -> Cache: set("purpose:" + userId, eventDraft, TTL=30분)
|
||||
== 4단계: Redis 캐시 저장 ==
|
||||
|
||||
Service -> Cache: 캐시 저장\nKey: draft:event:{eventDraftId}\nValue: {목적, 매장정보, 상태}\nTTL: 24시간
|
||||
activate Cache
|
||||
Cache --> Service: OK
|
||||
Cache --> Service: 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
Service -> Kafka: publish(EventDraftCreated,\n{eventDraftId, userId, objective, createdAt})
|
||||
== 5단계: Kafka 이벤트 발행 ==
|
||||
|
||||
Service -> Kafka: 이벤트 발행\nTopic: event-topic\nEvent: EventDraftCreated\nPayload: {eventDraftId,\nuserId, objective,\ncreatedAt}
|
||||
activate Kafka
|
||||
note right: Kafka Event Topic:\nevent-topic\n\nEvent: EventDraftCreated\n(목적 선택 시 발행)\n\n※ EventCreated는\n최종 승인 시 발행
|
||||
Kafka --> Service: ACK
|
||||
note right of Kafka
|
||||
**Kafka Event Topic**
|
||||
- Topic: event-topic
|
||||
- Event: EventDraftCreated
|
||||
- 목적 선택 시 발행
|
||||
|
||||
**구독자**
|
||||
- Analytics Service (선택적)
|
||||
|
||||
**참고**
|
||||
- EventCreated는
|
||||
최종 승인 시 발행
|
||||
end note
|
||||
Kafka --> Service: ACK (발행 확인)
|
||||
deactivate Kafka
|
||||
|
||||
Service --> Controller: EventDraftResponse\n{eventDraftId, objective, status}
|
||||
deactivate Service
|
||||
Controller --> Client: 200 OK\n{eventDraftId}
|
||||
== 6단계: 응답 반환 ==
|
||||
|
||||
note over Controller, Kafka: 캐시 히트 시:\n1. Redis에서 조회 → 즉시 반환\n2. DB 조회 생략\n\n※ EventDraftCreated 이벤트는\nAnalytics Service가 선택적으로 구독\n(통계 초기화는 EventCreated 시)
|
||||
Service -> Service: 응답 DTO 생성
|
||||
Service --> Controller: EventDraftResponse\n{eventDraftId, objective,\nstoreName, status=DRAFT}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 200 OK\n{"eventDraftId": "draft-123",\n"objective": "신규 고객 유치",\n"storeName": "맛있는집",\n"status": "DRAFT"}
|
||||
deactivate Controller
|
||||
|
||||
note over Controller, Kafka
|
||||
**캐시 전략**
|
||||
- Key: draft:event:{eventDraftId}
|
||||
- TTL: 24시간
|
||||
- 캐시 히트 시: DB 조회 생략, 즉시 반환
|
||||
|
||||
**이벤트 발행 전략**
|
||||
- EventDraftCreated: 목적 선택 시 발행 (Analytics Service 선택적 구독)
|
||||
- EventCreated: 최종 승인 시 발행 (통계 초기화 시작)
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 0.3초 이내
|
||||
- P95 응답 시간: 0.5초 이내
|
||||
- Redis 캐시 조회: 0.05초 이내
|
||||
|
||||
**에러 코드**
|
||||
- EVENT_001: 유효하지 않은 목적
|
||||
- EVENT_002: 매장 정보 누락
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
||||
@ -15,30 +15,30 @@ 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)
|
||||
Client -> Controller: POST /api/users/login\n{"email": "user@example.com",\n"password": "password123"}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: @Valid 어노테이션 검증\n(필수 필드 확인)
|
||||
Controller -> Controller: 입력값 검증\n(필수 필드, 이메일 형식 확인)
|
||||
|
||||
Controller -> AuthService: authenticate(phoneNumber, password)
|
||||
Controller -> AuthService: authenticate(email, password)
|
||||
activate AuthService
|
||||
|
||||
== 1단계: 사용자 조회 ==
|
||||
|
||||
AuthService -> Service: findByPhoneNumber(phoneNumber)
|
||||
AuthService -> Service: findByEmail(email)
|
||||
activate Service
|
||||
Service -> UserRepo: findByPhoneNumber(phoneNumber)
|
||||
Service -> UserRepo: findByEmail(email)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 전화번호로 사용자 조회\n(사용자ID, 비밀번호해시, 역할,\n이름, 이메일 조회)
|
||||
UserRepo -> UserDB: 이메일로 사용자 조회\n(사용자ID, 비밀번호해시, 역할,\n이름, 전화번호 조회)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 사용자 정보 또는 NULL
|
||||
UserDB --> UserRepo: 사용자 정보 반환 또는 없음
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: Optional<User>
|
||||
deactivate UserRepo
|
||||
@ -46,8 +46,8 @@ Service --> AuthService: Optional<User>
|
||||
deactivate Service
|
||||
|
||||
alt 사용자 없음
|
||||
AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"error": "전화번호 또는 비밀번호를\n확인해주세요"}
|
||||
AuthService --> Controller: throw AuthenticationFailedException\n("이메일 또는 비밀번호를 확인해주세요")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "이메일 또는 비밀번호를\n확인해주세요"}
|
||||
deactivate AuthService
|
||||
deactivate Controller
|
||||
|
||||
@ -62,8 +62,8 @@ else 사용자 존재
|
||||
deactivate PwdEncoder
|
||||
|
||||
alt 비밀번호 불일치
|
||||
AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"error": "전화번호 또는 비밀번호를\n확인해주세요"}
|
||||
AuthService --> Controller: throw AuthenticationFailedException\n("이메일 또는 비밀번호를 확인해주세요")
|
||||
Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"message": "이메일 또는 비밀번호를\n확인해주세요"}
|
||||
deactivate AuthService
|
||||
deactivate Controller
|
||||
|
||||
@ -79,9 +79,9 @@ else 사용자 존재
|
||||
|
||||
== 4단계: 세션 저장 ==
|
||||
|
||||
AuthService -> Redis: SET user:session:{token}\n(userId, role, TTL 7일)
|
||||
AuthService -> Redis: 세션 정보 저장\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7일
|
||||
activate Redis
|
||||
Redis --> AuthService: 세션 저장 완료
|
||||
Redis --> AuthService: 저장 완료
|
||||
deactivate Redis
|
||||
|
||||
== 5단계: 최종 로그인 시각 업데이트 (비동기) ==
|
||||
@ -96,7 +96,7 @@ else 사용자 존재
|
||||
end note
|
||||
Service ->> UserRepo: updateLastLoginAt(userId)
|
||||
activate UserRepo
|
||||
UserRepo ->> UserDB: 최종 로그인 시각 업데이트\n(현재 시각으로 갱신)
|
||||
UserRepo ->> UserDB: 사용자 최종 로그인 시각 갱신\n(현재 시각으로 업데이트)
|
||||
activate UserDB
|
||||
UserDB -->> UserRepo: 업데이트 완료
|
||||
deactivate UserDB
|
||||
@ -113,11 +113,11 @@ else 사용자 존재
|
||||
|
||||
== 6단계: 응답 반환 ==
|
||||
|
||||
AuthService -> AuthService: 응답 DTO 생성\n(LoginResponse)
|
||||
AuthService --> Controller: LoginResponse\n(token, userId, userName,\nrole, email)
|
||||
AuthService -> AuthService: 로그인 응답 DTO 생성
|
||||
AuthService --> Controller: LoginResponse\n{token, userId, userName, role, email}
|
||||
deactivate AuthService
|
||||
|
||||
Controller --> Client: 200 OK\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"}
|
||||
Controller --> Client: 200 OK\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "홍길동",\n"role": "OWNER",\n"email": "hong@example.com"}
|
||||
deactivate Controller
|
||||
end
|
||||
end
|
||||
@ -125,7 +125,7 @@ end
|
||||
note over Controller, UserDB
|
||||
**보안 처리**
|
||||
- 비밀번호: bcrypt compare (원본 노출 안 됨)
|
||||
- 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (Timing Attack 방어)
|
||||
- 에러 메시지: 이메일/비밀번호 구분 없이 동일 메시지 반환 (Timing Attack 방어)
|
||||
- JWT 토큰: 7일 만료, 서버 세션과 동기화
|
||||
|
||||
**보안 강화 (향후 구현)**
|
||||
@ -141,7 +141,7 @@ note over Controller, UserDB
|
||||
- Redis 세션 조회: 0.1초 이내
|
||||
|
||||
**에러 코드**
|
||||
- AUTH_001: 인증 실패 (전화번호 또는 비밀번호 불일치)
|
||||
- AUTH_001: 인증 실패 (이메일 또는 비밀번호 불일치)
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
||||
@ -5,112 +5,68 @@ 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
|
||||
note over Controller, UserDB
|
||||
**UFR-USER-010: 회원가입**
|
||||
- 기본 정보: 이름, 전화번호, 이메일, 비밀번호
|
||||
- 매장 정보: 매장명, 업종, 주소, 영업시간, 사업자번호
|
||||
- 사업자번호 검증 (국세청 API)
|
||||
- 이메일/전화번호 중복 검사
|
||||
- 트랜잭션 처리
|
||||
- JWT 토큰 발급
|
||||
end note
|
||||
|
||||
Client -> Controller: POST /api/users/register\n(RegisterRequest DTO)
|
||||
Client -> Controller: POST /api/users/register\n{"name": "홍길동",\n"phoneNumber": "01012345678",\n"email": "hong@example.com",\n"password": "password123"}
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: @Valid 어노테이션 검증\n(이메일 형식, 비밀번호 8자 이상 등)
|
||||
Controller -> Controller: 입력값 검증\n(이메일 형식, 비밀번호 8자 이상 등)
|
||||
|
||||
Controller -> Service: register(RegisterRequest)
|
||||
activate Service
|
||||
|
||||
== 1단계: 중복 사용자 확인 ==
|
||||
== 1단계: 이메일 중복 확인 ==
|
||||
|
||||
Service -> UserRepo: findByPhoneNumber(phoneNumber)
|
||||
Service -> UserRepo: findByEmail(email)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인)
|
||||
UserRepo -> UserDB: 이메일로 사용자 조회\n(중복 가입 확인)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 조회 결과
|
||||
UserDB --> UserRepo: 조회 결과 반환 또는 없음
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: Optional<User>
|
||||
deactivate UserRepo
|
||||
|
||||
alt 중복 사용자 존재
|
||||
Service --> Controller: throw DuplicateUserException\n("이미 가입된 전화번호입니다")
|
||||
Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"error": "이미 가입된 전화번호입니다"}
|
||||
alt 이메일 중복 존재
|
||||
Service --> Controller: throw DuplicateEmailException\n("이미 가입된 이메일입니다")
|
||||
Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"message": "이미 가입된 이메일입니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
else 신규 사용자
|
||||
|
||||
== 2단계: 사업자번호 검증 ==
|
||||
else 이메일 신규
|
||||
|
||||
Service -> Validator: validateBusinessNumber(businessNumber)
|
||||
activate Validator
|
||||
== 2단계: 전화번호 중복 확인 ==
|
||||
|
||||
Validator -> Redis: GET user:business:{사업자번호}
|
||||
activate Redis
|
||||
Redis --> Validator: 캐시 확인 결과
|
||||
deactivate Redis
|
||||
Service -> UserRepo: findByPhoneNumber(phoneNumber)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인)
|
||||
activate UserDB
|
||||
UserDB --> UserRepo: 조회 결과 반환 또는 없음
|
||||
deactivate UserDB
|
||||
UserRepo --> Service: Optional<User>
|
||||
deactivate UserRepo
|
||||
|
||||
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{"code": "USER_002",\n"error": "유효하지 않은 사업자번호입니다.\n휴폐업 여부를 확인해주세요."}
|
||||
alt 전화번호 중복 존재
|
||||
Service --> Controller: throw DuplicatePhoneException\n("이미 가입된 전화번호입니다")
|
||||
Controller --> Client: 400 Bad Request\n{"code": "USER_002",\n"message": "이미 가입된 전화번호입니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
|
||||
else 사업자번호 검증 성공
|
||||
Validator --> Service: ValidationResult\n(valid: true, needsManualCheck: false)
|
||||
deactivate Validator
|
||||
else 신규 사용자
|
||||
|
||||
== 3단계: 비밀번호 해싱 ==
|
||||
|
||||
@ -120,40 +76,30 @@ else 신규 사용자
|
||||
PwdEncoder --> Service: passwordHash
|
||||
deactivate PwdEncoder
|
||||
|
||||
== 4단계: 사업자번호 암호화 ==
|
||||
|
||||
Service -> Service: EncryptionUtil 호출 준비
|
||||
note right of Service
|
||||
**암호화 처리**
|
||||
- AES-256-GCM 모드 사용
|
||||
- 환경변수에서 암호화 키 로드
|
||||
end note
|
||||
Service -> Service: encryptedBusinessNumber =\nEncryptionUtil.encrypt(businessNumber)
|
||||
|
||||
== 5단계: 데이터베이스 트랜잭션 ==
|
||||
== 4단계: 데이터베이스 트랜잭션 ==
|
||||
|
||||
Service -> UserDB: 트랜잭션 시작
|
||||
activate UserDB
|
||||
|
||||
Service -> UserRepo: save(User)\n(name, phoneNumber, email,\npasswordHash, createdAt)
|
||||
Service -> UserRepo: save(User)
|
||||
activate UserRepo
|
||||
UserRepo -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시)\n사용자ID 반환
|
||||
UserDB --> UserRepo: user_id
|
||||
UserRepo -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시)\n저장 후 사용자ID 반환
|
||||
UserDB --> UserRepo: 생성된 사용자ID
|
||||
UserRepo --> Service: User 엔티티\n(userId 포함)
|
||||
deactivate UserRepo
|
||||
|
||||
Service -> StoreRepo: save(Store)\n(userId, storeName, industry,\naddress, businessNumberEncrypted,\nbusinessHours)
|
||||
Service -> StoreRepo: save(Store)
|
||||
activate StoreRepo
|
||||
StoreRepo -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 암호화된사업자번호,\n영업시간)\n매장ID 반환
|
||||
UserDB --> StoreRepo: store_id
|
||||
StoreRepo -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 사업자번호, 영업시간)\n저장 후 매장ID 반환
|
||||
UserDB --> StoreRepo: 생성된 매장ID
|
||||
StoreRepo --> Service: Store 엔티티\n(storeId 포함)
|
||||
deactivate StoreRepo
|
||||
|
||||
Service -> UserDB: 트랜잭션 커밋
|
||||
UserDB --> Service: 트랜잭션 커밋 완료
|
||||
UserDB --> Service: 커밋 완료
|
||||
deactivate UserDB
|
||||
|
||||
== 6단계: JWT 토큰 생성 ==
|
||||
== 5단계: JWT 토큰 생성 ==
|
||||
|
||||
Service -> JwtProvider: generateToken(userId, role)
|
||||
activate JwtProvider
|
||||
@ -161,48 +107,43 @@ else 신규 사용자
|
||||
JwtProvider --> Service: JWT 토큰
|
||||
deactivate JwtProvider
|
||||
|
||||
== 7단계: 세션 저장 ==
|
||||
== 6단계: 세션 저장 ==
|
||||
|
||||
Service -> Redis: SET user:session:{token}\n(userId, role, TTL 7일)
|
||||
Service -> Redis: 세션 정보 저장\nKey: user:session:{token}\nValue: {userId, role}\nTTL: 7일
|
||||
activate Redis
|
||||
Redis --> Service: 세션 저장 완료
|
||||
Redis --> Service: 저장 완료
|
||||
deactivate Redis
|
||||
|
||||
== 8단계: 응답 반환 ==
|
||||
== 7단계: 응답 반환 ==
|
||||
|
||||
Service -> Service: 응답 DTO 생성\n(RegisterResponse)
|
||||
Service --> Controller: RegisterResponse\n(token, userId, userName,\nstoreId, storeName)
|
||||
Service -> Service: 회원가입 응답 DTO 생성
|
||||
Service --> Controller: RegisterResponse\n{token, userId, userName, storeId, storeName}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Client: 201 Created\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"storeId": 456,\n"storeName": "맛있는집"}
|
||||
Controller --> Client: 201 Created\n{"token": "eyJhbGc...",\n"userId": 123,\n"userName": "홍길동",\n"storeId": 456,\n"storeName": "맛있는집"}
|
||||
deactivate Controller
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
note over Controller, NTSApi
|
||||
note over Controller, UserDB
|
||||
**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-GCM 암호화 (EncryptionUtil)
|
||||
- JWT 토큰: 7일 만료, 서버 세션과 동기화
|
||||
|
||||
**성능 목표**
|
||||
- 평균 응답 시간: 2.0초 이내 (국세청 API 포함)
|
||||
- P95 응답 시간: 3.0초 이내
|
||||
- 캐시 HIT 시: 0.8초 이내
|
||||
- 평균 응답 시간: 1.0초 이내
|
||||
- P95 응답 시간: 1.5초 이내
|
||||
- 트랜잭션 처리: 0.5초 이내
|
||||
|
||||
**에러 코드**
|
||||
- USER_001: 중복 사용자
|
||||
- USER_002: 사업자번호 검증 실패
|
||||
- USER_001: 이메일 중복
|
||||
- USER_002: 전화번호 중복
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
||||
@ -21,14 +21,18 @@ participant "배포 채널 APIs" as ChannelApis
|
||||
|
||||
== 1. 이벤트 목적 선택 (UFR-EVENT-020) ==
|
||||
User -> FE: 이벤트 목적 선택
|
||||
FE -> UserSvc: GET /api/users/{userId}/store\n회원 및 매장정보 조회
|
||||
FE -> Gateway: GET /api/users/{userId}/store\n회원 및 매장정보 조회
|
||||
activate Gateway
|
||||
Gateway -> UserSvc: GET /api/users/{userId}/store\n회원 및 매장정보 조회
|
||||
activate UserSvc
|
||||
UserSvc -> UserDB: 사용자 및 매장 정보 조회
|
||||
activate UserDB
|
||||
UserDB --> UserSvc: 사용자, 매장 정보 반환
|
||||
deactivate UserDB
|
||||
UserSvc --> FE: 200 OK\n{userId, storeName, industry, address}
|
||||
UserSvc --> Gateway: 200 OK\n{userId, storeName, industry, address}
|
||||
deactivate UserSvc
|
||||
Gateway --> FE: 200 OK\n{userId, storeName, industry, address}
|
||||
deactivate Gateway
|
||||
FE -> Gateway: POST /events/purposes\n{목적, userId, storeName, industry, address}
|
||||
Gateway -> Event: 이벤트 목적 저장 요청
|
||||
Event -> Redis: 이벤트 목적 정보 저장\nKey: draft:event:{eventDraftId}\n(목적, 매장정보 저장)\nTTL: 24시간
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user