diff --git a/design/backend/sequence/inner/event-AI추천요청.puml b/design/backend/sequence/inner/event-AI추천요청.puml index 5d396b1..f3baef0 100644 --- a/design/backend/sequence/inner/event-AI추천요청.puml +++ b/design/backend/sequence/inner/event-AI추천요청.puml @@ -3,54 +3,124 @@ title Event Service - AI 추천 요청 (Kafka Job 발행) (UFR-EVENT-030) -participant "EventController" as Controller <> -participant "EventService" as Service <> -participant "JobService" as JobSvc <> -participant "EventRepository" as Repo <> +actor Client +participant "API Gateway" as Gateway +participant "EventController" as Controller <> +participant "EventService" as Service <> +participant "JobService" as JobSvc <> +participant "EventRepository" as Repo <> participant "Redis Cache" as Cache <> database "Event DB" as DB <> participant "Kafka Producer" as Kafka <> -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 diff --git a/design/backend/sequence/inner/event-이미지결과조회.puml b/design/backend/sequence/inner/event-이미지결과조회.puml index 1a47290..c9265e9 100644 --- a/design/backend/sequence/inner/event-이미지결과조회.puml +++ b/design/backend/sequence/inner/event-이미지결과조회.puml @@ -1,45 +1,140 @@ @startuml event-이미지결과조회 !theme mono -title Event Service - 이미지 생성 결과 폴링 조회 +title Content Service - 이미지 생성 결과 폴링 조회 -participant "EventController" as Controller <> -participant "JobService" as JobSvc <> +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "JobManager" as JobMgr <> participant "Redis Cache" as Cache <> -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 diff --git a/design/backend/sequence/inner/event-이미지생성요청.puml b/design/backend/sequence/inner/event-이미지생성요청.puml index cdd888b..1d38c6a 100644 --- a/design/backend/sequence/inner/event-이미지생성요청.puml +++ b/design/backend/sequence/inner/event-이미지생성요청.puml @@ -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 <> -participant "EventService" as Service <> -participant "JobService" as JobSvc <> -participant "EventRepository" as Repo <> +actor Client +participant "API Gateway" as Gateway +participant "ContentController" as Controller <> +participant "ContentService" as Service <> +participant "JobManager" as JobMgr <> participant "Redis Cache" as Cache <> -database "Event DB" as DB <> -participant "Kafka Producer" as Kafka <> -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