mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 06:46:25 +00:00
이미지 생성 및 AI 추천 inner sequence 수정
주요 변경사항: - event-이미지생성요청.puml: Kafka 제거, ContentService 내부 Job 관리로 변경 - event-이미지결과조회.puml: ContentService 패턴으로 업데이트 - event-AI추천요청.puml: Gateway 패턴 추가, 한글화 아키텍처 구분: - AI 추천: Kafka 사용 (ai-job-topic) - 이미지 생성: 내부 Job 관리 (Kafka 사용 안 함) 모든 파일: - Gateway 패턴 적용 - 레이어 아키텍처 (<<API Layer>>, <<Business Layer>>) - 한글 요청/응답 - Redis 키 패턴 표준화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d7d4b0a2da
commit
e15b7b42e5
@ -3,54 +3,124 @@
|
||||
|
||||
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>>
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "EventController" as Controller <<API Layer>>
|
||||
participant "EventService" as Service <<Business Layer>>
|
||||
participant "JobService" as JobSvc <<Business Layer>>
|
||||
participant "EventRepository" as Repo <<Data Layer>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
database "Event DB" as DB <<E>>
|
||||
participant "Kafka Producer" as Kafka <<E>>
|
||||
|
||||
note over Controller: POST /api/events/{id}/ai-recommendations
|
||||
note over Controller, Kafka
|
||||
**UFR-EVENT-030: AI 이벤트 추천 요청**
|
||||
- Kafka 비동기 Job 발행
|
||||
- AI Service가 Kafka 구독하여 처리
|
||||
- 트렌드 분석 + 3가지 추천안 생성
|
||||
- 처리 시간: 평균 2분 이내
|
||||
end note
|
||||
|
||||
Client -> Gateway: POST /api/events/{eventDraftId}/ai-recommendations\n{"objective": "신규 고객 유치",\n"industry": "음식점",\n"region": "서울 강남구"}
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: POST /api/events/{eventDraftId}/ai-recommendations
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(필수 필드, 목적 유효성)
|
||||
|
||||
Controller -> Service: requestAIRecommendation(eventDraftId, userId)
|
||||
activate Service
|
||||
|
||||
== 1단계: 이벤트 초안 조회 및 검증 ==
|
||||
|
||||
Service -> Repo: findById(eventDraftId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID와 사용자ID로 조회)
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID로 이벤트 목적,\n매장 정보 조회)
|
||||
activate DB
|
||||
DB --> Repo: EventDraft
|
||||
DB --> Repo: EventDraft 엔티티\n{목적, 매장명, 업종, 주소}
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
|
||||
Service -> Service: validateOwnership(userId, eventDraft)
|
||||
note right: 사용자 권한 검증
|
||||
Service -> Service: 소유권 검증\nvalidateOwnership(userId, eventDraft)
|
||||
|
||||
Service -> JobSvc: createAIJob(eventDraft)
|
||||
activate JobSvc
|
||||
alt 소유권 없음
|
||||
Service --> Controller: throw ForbiddenException\n("권한이 없습니다")
|
||||
Controller --> Gateway: 403 Forbidden\n{"code": "EVENT_003",\n"message": "권한이 없습니다"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
Gateway --> Client: 403 Forbidden
|
||||
deactivate Gateway
|
||||
|
||||
JobSvc -> JobSvc: generateJobId()
|
||||
note right: UUID 생성
|
||||
else 소유권 확인
|
||||
|
||||
JobSvc -> Cache: set("job:" + jobId,\n{status: PENDING, createdAt}, TTL=1시간)
|
||||
activate Cache
|
||||
Cache --> JobSvc: OK
|
||||
deactivate Cache
|
||||
== 2단계: Kafka Job 생성 ==
|
||||
|
||||
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
|
||||
Service -> JobSvc: createAIJob(eventDraft)
|
||||
activate JobSvc
|
||||
|
||||
JobSvc --> Service: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate JobSvc
|
||||
JobSvc -> JobSvc: Job ID 생성 (UUID)
|
||||
|
||||
Service --> Controller: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate Service
|
||||
Controller --> Client: 202 Accepted\n{jobId, status: PENDING}
|
||||
JobSvc -> Cache: Job 상태 저장\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: AI_RECOMMEND,\ncreatedAt}\nTTL: 1시간
|
||||
activate Cache
|
||||
Cache --> JobSvc: 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
note over Controller, Kafka: AI Service는 백그라운드에서\nKafka ai-job 토픽을 구독하여\n비동기로 처리
|
||||
== 3단계: Kafka 이벤트 발행 ==
|
||||
|
||||
JobSvc -> Kafka: 이벤트 발행\nTopic: ai-job-topic\nPayload: {jobId, eventDraftId,\nobjective, industry,\nregion, storeInfo}
|
||||
activate Kafka
|
||||
note right of Kafka
|
||||
**Kafka Topic**
|
||||
- Topic: ai-job-topic
|
||||
- Consumer: AI Service
|
||||
- Consumer Group: ai-service-group
|
||||
|
||||
**Payload**
|
||||
{
|
||||
"jobId": "UUID",
|
||||
"eventDraftId": "UUID",
|
||||
"objective": "신규 고객 유치",
|
||||
"industry": "음식점",
|
||||
"region": "서울 강남구",
|
||||
"storeInfo": {...}
|
||||
}
|
||||
end note
|
||||
Kafka --> JobSvc: ACK (발행 확인)
|
||||
deactivate Kafka
|
||||
|
||||
JobSvc --> Service: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate JobSvc
|
||||
|
||||
== 4단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "AI가 분석 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 202 Accepted\nAI 분석 시작
|
||||
deactivate Gateway
|
||||
|
||||
note over Service, Kafka
|
||||
**AI Service 비동기 처리**
|
||||
- Kafka 구독: ai-job-topic
|
||||
- 트렌드 분석 (업종, 지역 기반)
|
||||
- 3가지 추천안 생성 (저/중/고 비용)
|
||||
- 결과: Redis에 저장 (TTL: 24시간)
|
||||
- 상세: ai-트렌드분석및추천.puml 참조
|
||||
|
||||
**처리 시간**
|
||||
- 평균: 2분 이내
|
||||
- P95: 4분 이내
|
||||
- Timeout: 5분
|
||||
|
||||
**결과 조회**
|
||||
- 폴링 방식: GET /api/jobs/{jobId}/status
|
||||
- 간격: 2초, 최대 30초
|
||||
end note
|
||||
end
|
||||
|
||||
@enduml
|
||||
|
||||
@ -1,45 +1,140 @@
|
||||
@startuml event-이미지결과조회
|
||||
!theme mono
|
||||
|
||||
title Event Service - 이미지 생성 결과 폴링 조회
|
||||
title Content Service - 이미지 생성 결과 폴링 조회
|
||||
|
||||
participant "EventController" as Controller <<C>>
|
||||
participant "JobService" as JobSvc <<S>>
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ContentController" as Controller <<API Layer>>
|
||||
participant "ContentService" as Service <<Business Layer>>
|
||||
participant "JobManager" as JobMgr <<Component>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
|
||||
note over Controller: GET /api/jobs/{jobId}/images
|
||||
Controller -> JobSvc: getImageJobStatus(jobId)
|
||||
activate JobSvc
|
||||
note over Controller, Cache
|
||||
**폴링 방식 Job 상태 조회**
|
||||
- 최대 30초 동안 폴링 (2초 간격)
|
||||
- Job 상태: PENDING → PROCESSING → COMPLETED
|
||||
- 이미지 URL: Redis에 저장 (TTL: 7일)
|
||||
end note
|
||||
|
||||
JobSvc -> Cache: get("job:" + jobId)
|
||||
Client -> Gateway: GET /api/content/jobs/{jobId}/status
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: GET /api/content/jobs/{jobId}/status
|
||||
activate Controller
|
||||
|
||||
Controller -> Service: getJobStatus(jobId)
|
||||
activate Service
|
||||
|
||||
Service -> JobMgr: getJobStatus(jobId)
|
||||
activate JobMgr
|
||||
|
||||
JobMgr -> Cache: Job 상태 조회\nKey: job:{jobId}
|
||||
activate Cache
|
||||
|
||||
alt 캐시 히트
|
||||
Cache --> JobSvc: Job data\n{status, imageUrls, createdAt}
|
||||
alt Job 데이터 존재
|
||||
Cache --> JobMgr: Job 데이터\n{status, eventDraftId,\ntype, createdAt}
|
||||
deactivate Cache
|
||||
|
||||
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}}
|
||||
alt status = COMPLETED
|
||||
JobMgr -> Cache: 이미지 URL 조회\nKey: content:image:{eventDraftId}
|
||||
activate Cache
|
||||
Cache --> JobMgr: 이미지 URL\n{simple, fancy, trendy}
|
||||
deactivate Cache
|
||||
|
||||
else Job 진행중 (status: PROCESSING)
|
||||
JobSvc --> Controller: JobStatusResponse\n{jobId, status: PROCESSING}
|
||||
Controller --> Client: 200 OK\n{status: PROCESSING}
|
||||
note right: 클라이언트는 3초 후\n재요청
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: COMPLETED,\nimageUrls: {...}}
|
||||
deactivate JobMgr
|
||||
|
||||
else Job 실패 (status: FAILED)
|
||||
JobSvc --> Controller: JobStatusResponse\n{jobId, status: FAILED, error}
|
||||
Controller --> Client: 200 OK\n{status: FAILED, error}
|
||||
Service --> Controller: JobStatusResponse\n{status: COMPLETED, imageUrls}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "COMPLETED",\n"imageUrls": {\n "simple": "https://cdn.../simple.png",\n "fancy": "https://cdn.../fancy.png",\n "trendy": "https://cdn.../trendy.png"\n}}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n이미지 URL 반환
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**프론트엔드 처리**
|
||||
- 3가지 스타일 카드 표시
|
||||
- 사용자가 스타일 선택
|
||||
- 이미지 편집 가능
|
||||
end note
|
||||
|
||||
else status = PROCESSING 또는 PENDING
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: PROCESSING}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: PROCESSING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "PROCESSING",\n"message": "이미지 생성 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n진행 중 상태
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**폴링 재시도**
|
||||
- 2초 후 재요청
|
||||
- 최대 30초 (15회)
|
||||
end note
|
||||
|
||||
else status = FAILED
|
||||
JobMgr -> Cache: 에러 정보 조회\nKey: job:{jobId}:error
|
||||
activate Cache
|
||||
Cache --> JobMgr: 에러 메시지
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: JobStatusResponse\n{jobId, status: FAILED, error}
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: JobStatusResponse\n{status: FAILED, error}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 200 OK\n{"status": "FAILED",\n"error": "이미지 생성 실패",\n"message": "다시 시도해주세요"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 200 OK\n실패 상태
|
||||
deactivate Gateway
|
||||
|
||||
note right of Client
|
||||
**실패 처리**
|
||||
- 에러 메시지 표시
|
||||
- "다시 생성" 버튼 제공
|
||||
end note
|
||||
end
|
||||
|
||||
else 캐시 미스
|
||||
Cache --> JobSvc: null
|
||||
JobSvc --> Controller: NotFoundError
|
||||
Controller --> Client: 404 Not Found\n{error: "Job not found"}
|
||||
else Job 데이터 없음
|
||||
Cache --> JobMgr: null (캐시 미스)
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: throw NotFoundException\n("Job을 찾을 수 없습니다")
|
||||
deactivate JobMgr
|
||||
|
||||
Service --> Controller: NotFoundException
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 404 Not Found\n{"code": "JOB_001",\n"message": "Job을 찾을 수 없습니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 404 Not Found
|
||||
deactivate Gateway
|
||||
end
|
||||
|
||||
deactivate Cache
|
||||
deactivate JobSvc
|
||||
note over Controller, Cache
|
||||
**폴링 전략**
|
||||
- 간격: 2초
|
||||
- 최대 시간: 30초 (15회)
|
||||
- Timeout 시: 사용자에게 알림 + "다시 생성" 옵션
|
||||
|
||||
note over Controller, Cache: 최대 30초 동안 폴링\n(3초 간격, 최대 10회)\n\n이미지 URL은 Redis에서 조회\n(TTL: 7일)\n\n타임아웃 시 클라이언트는\n에러 메시지 표시 및\n"다시 생성" 옵션 제공
|
||||
**Redis 캐시**
|
||||
- Job 상태: TTL 1시간
|
||||
- 이미지 URL: TTL 7일
|
||||
|
||||
**성능 목표**
|
||||
- 평균 이미지 생성 시간: 20초 이내
|
||||
- P95 이미지 생성 시간: 40초 이내
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
||||
@ -1,57 +1,90 @@
|
||||
@startuml event-이미지생성요청
|
||||
!theme mono
|
||||
|
||||
title Event Service - 이미지 생성 요청 (Kafka Job 발행) (UFR-CONT-010)
|
||||
title Content Service - 이미지 생성 요청 (UFR-CONT-010)
|
||||
|
||||
participant "EventController" as Controller <<C>>
|
||||
participant "EventService" as Service <<S>>
|
||||
participant "JobService" as JobSvc <<S>>
|
||||
participant "EventRepository" as Repo <<R>>
|
||||
actor Client
|
||||
participant "API Gateway" as Gateway
|
||||
participant "ContentController" as Controller <<API Layer>>
|
||||
participant "ContentService" as Service <<Business Layer>>
|
||||
participant "JobManager" as JobMgr <<Component>>
|
||||
participant "Redis Cache" as Cache <<E>>
|
||||
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)
|
||||
note over Controller, Cache
|
||||
**UFR-CONT-010: SNS 이미지 생성 요청**
|
||||
- Kafka 사용 안 함 (내부 Job 관리)
|
||||
- 백그라운드 워커가 비동기 처리
|
||||
- Redis에서 AI 추천 데이터 읽기
|
||||
- 3가지 스타일 이미지 생성 (심플, 화려한, 트렌디)
|
||||
end note
|
||||
|
||||
Client -> Gateway: POST /api/content/images/{eventDraftId}/generate
|
||||
activate Gateway
|
||||
|
||||
Gateway -> Controller: POST /api/content/images/{eventDraftId}/generate
|
||||
activate Controller
|
||||
|
||||
Controller -> Controller: 요청 검증\n(eventDraftId 유효성)
|
||||
|
||||
Controller -> Service: generateImages(eventDraftId)
|
||||
activate Service
|
||||
|
||||
Service -> Repo: findById(eventDraftId)
|
||||
activate Repo
|
||||
Repo -> DB: 이벤트 초안 조회\n(초안ID와 사용자ID로 조회)
|
||||
activate DB
|
||||
DB --> Repo: EventDraft
|
||||
deactivate DB
|
||||
Repo --> Service: EventDraft entity
|
||||
deactivate Repo
|
||||
== 1단계: Redis에서 AI 추천 데이터 확인 ==
|
||||
|
||||
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시간)
|
||||
Service -> Cache: AI 추천 데이터 조회\nKey: ai:event:{eventDraftId}
|
||||
activate Cache
|
||||
Cache --> JobSvc: OK
|
||||
Cache --> Service: AI 추천 결과\n{선택된 추천안, 이벤트 정보}
|
||||
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
|
||||
alt AI 추천 데이터 없음
|
||||
Service --> Controller: throw NotFoundException\n("AI 추천을 먼저 선택해주세요")
|
||||
Controller --> Gateway: 404 Not Found\n{"code": "CONTENT_001",\n"message": "AI 추천을 먼저 선택해주세요"}
|
||||
deactivate Service
|
||||
deactivate Controller
|
||||
Gateway --> Client: 404 Not Found
|
||||
deactivate Gateway
|
||||
|
||||
JobSvc --> Service: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate JobSvc
|
||||
else AI 추천 데이터 존재
|
||||
|
||||
Service --> Controller: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate Service
|
||||
Controller --> Client: 202 Accepted\n{jobId, status: PENDING}
|
||||
== 2단계: Job 생성 ==
|
||||
|
||||
note over Controller, Kafka: Content Service는 백그라운드에서\nKafka image-job 토픽을 구독하여\n3가지 스타일 이미지 생성\n(심플, 화려한, 트렌디)
|
||||
Service -> JobMgr: createJob(eventDraftId, imageGeneration)
|
||||
activate JobMgr
|
||||
|
||||
JobMgr -> JobMgr: Job ID 생성 (UUID)
|
||||
|
||||
JobMgr -> Cache: Job 상태 저장\nKey: job:{jobId}\nValue: {status: PENDING,\neventDraftId, type: IMAGE_GEN,\ncreatedAt}\nTTL: 1시간
|
||||
activate Cache
|
||||
Cache --> JobMgr: 저장 완료
|
||||
deactivate Cache
|
||||
|
||||
JobMgr --> Service: Job 생성 완료\n{jobId, status: PENDING}
|
||||
deactivate JobMgr
|
||||
|
||||
== 3단계: 응답 반환 ==
|
||||
|
||||
Service --> Controller: JobResponse\n{jobId, status: PENDING}
|
||||
deactivate Service
|
||||
|
||||
Controller --> Gateway: 202 Accepted\n{"jobId": "job-uuid-123",\n"status": "PENDING",\n"message": "이미지 생성 중입니다"}
|
||||
deactivate Controller
|
||||
|
||||
Gateway --> Client: 202 Accepted\n이미지 생성 시작
|
||||
deactivate Gateway
|
||||
|
||||
note over Service, Cache
|
||||
**백그라운드 워커 처리**
|
||||
- Redis 폴링 또는 스케줄러가 Job 감지
|
||||
- content-이미지생성.puml 참조
|
||||
- 외부 이미지 생성 API 호출 (병렬)
|
||||
- Redis에 이미지 URL 저장
|
||||
|
||||
**상세 내용**
|
||||
- 3가지 스타일 병렬 생성 (심플, 화려한, 트렌디)
|
||||
- Circuit Breaker 적용 (Timeout: 5분)
|
||||
- 결과: Redis Key: content:image:{eventDraftId}
|
||||
- TTL: 7일
|
||||
end note
|
||||
end
|
||||
|
||||
@enduml
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user