diff --git a/claude/api-design.md b/claude/api-design.md new file mode 100644 index 0000000..d44c64b --- /dev/null +++ b/claude/api-design.md @@ -0,0 +1,111 @@ +# API설계가이드 + +[요청사항] +- <작성원칙>을 준용하여 설계 +- <작성순서>에 따라 설계 +- [결과파일] 안내에 따라 파일 작성 +- 최종 완료 후 API 확인 방법 안내 + - https://editor.swagger.io/ 접근 + - 생성된 swagger yaml파일을 붙여서 확인 및 테스트 + +[가이드] +<작성 원칙> +- 각 서비스 API는 독립적으로 완전한 명세를 포함 +- 공통 스키마는 각 서비스에서 필요에 따라 직접 정의 +- 서비스 간 의존성을 최소화하여 독립 배포 가능 +- 중복되는 스키마가 많아질 경우에만 공통 파일 도입 검토 +<작성순서> +- 준비: + - 유저스토리, 외부시퀀스설계서, 내부시퀀스설계서 분석 및 이해 +- 실행: + - <병렬처리> 안내에 따라 동시 수행 + - 에 따라 API 선정 + - <파일작성안내>에 따라 작성 + - <검증방법>에 따라 작성된 YAML의 문법 및 구조 검증 수행 +- 검토: + - <작성원칙> 준수 검토 + - 스쿼드 팀원 리뷰: 누락 및 개선 사항 검토 + - 수정 사항 선택 및 반영 + + +- 유저스토리와 매칭 되어야 함. 불필요한 추가 설계 금지 + (유저스토리 ID를 x-user-story 확장 필드에 명시) +- '외부시퀀스설계서'/'내부시퀀스설계서'와 일관성 있게 선정 + +<파일작성안내> +- OpenAPI 3.0 스펙 준용 +- **servers 섹션 필수화** + - 모든 OpenAPI 명세에 servers 섹션 포함 + - SwaggerHub Mock URL을 첫 번째 옵션으로 배치 +- **example 데이터 권장** + - 스키마에 example을 추가하여 Swagger UI에서 테스트 할 수 있게함 +- **테스트 시나리오 포함** + - 각 API 엔드포인트별 테스트 케이스 정의 + - 성공/실패 케이스 모두 포함 +- 작성 형식 + - YAML 형식의 OpenAPI 3.0 명세 + - 각 API별 필수 항목: + - summary: API 목적 설명 + - operationId: 고유 식별자 + - x-user-story: 유저스토리 ID + - x-controller: 담당 컨트롤러 + - tags: API 그룹 분류 + - requestBody/responses: 상세 스키마 + - 각 서비스 파일에 필요한 모든 스키마 포함: + - components/schemas: 요청/응답 모델 + - components/parameters: 공통 파라미터 + - components/responses: 공통 응답 + - components/securitySchemes: 인증 방식 + +<파일 구조> +``` +design/backend/api/ +├── {service-name}-api.yaml # 각 마이크로서비스별 API 명세 +└── ... # 추가 서비스들 + +예시: +├── profile-service-api.yaml # 프로파일 서비스 API +├── order-service-api.yaml # 주문 서비스 API +└── payment-service-api.yaml # 결제 서비스 API +``` + +- 파일명 규칙 + - 서비스명은 kebab-case로 작성 + - 파일명 형식: {service-name}-api.yaml + - 서비스명은 유저스토리의 '서비스' 항목을 영문으로 변환하여 사용 + +<병렬처리> +- **의존성 분석 선행**: 병렬 처리 전 반드시 의존성 파악 +- **순차 처리 필요시**: 무리한 병렬화보다는 안전한 순차 처리 +- **검증 단계 필수**: 병렬 처리 후 통합 검증 + +<검증방법> +- swagger-cli를 사용한 자동 검증 수행 +- 검증 명령어: `swagger-cli validate {파일명}` +- swagger-cli가 없을 경우 자동 설치: + ```bash + # swagger-cli 설치 확인 및 자동 설치 + command -v swagger-cli >/dev/null 2>&1 || npm install -g @apidevtools/swagger-cli + + # 검증 실행 + swagger-cli validate design/backend/api/*.yaml + ``` +- 검증 항목: + - OpenAPI 3.0 스펙 준수 + - YAML 구문 오류 + - 스키마 참조 유효성 + - 필수 필드 존재 여부 + +[참고자료] +- 유저스토리 +- 외부시퀀스설계서 +- 내부시퀀스설계서 +- OpenAPI 스펙: https://swagger.io/specification/ + +[예시] +- swagger api yaml: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-swagger-api.yaml +- API설계서: https://raw.githubusercontent.com/cna-bootcamp/clauding-guide/refs/heads/main/samples/sample-API%20설계서.md + +[결과파일] +- 각 서비스별로 별도의 YAML 파일 생성 +- design/backend/api/*.yaml (OpenAPI 형식) \ No newline at end of file diff --git a/design/backend/api/ai-service-api.yaml b/design/backend/api/ai-service-api.yaml new file mode 100644 index 0000000..af13b25 --- /dev/null +++ b/design/backend/api/ai-service-api.yaml @@ -0,0 +1,1036 @@ +openapi: 3.0.3 +info: + title: AI Service API + description: | + AI 기반 트렌드 분석 및 이벤트 추천 서비스 API + + ## 주요 기능 + - 업종/지역/시즌별 트렌드 분석 + - AI 기반 이벤트 기획안 추천 (3가지 옵션) + - 비동기 Job 처리 및 폴링 기반 결과 조회 + + ## 기술 스택 + - AI Engine: Claude API / GPT-4 API + - 캐싱: Redis (트렌드 1시간, 추천안 24시간) + - 메시지 큐: Kafka (비동기 Job 처리) + - 안정성: Circuit Breaker 패턴 + version: 1.0.0 + contact: + name: AI Service Team + email: ai-team@kt.com + +servers: + - url: https://api.kt-event.com/ai/v1 + description: 프로덕션 서버 + - url: https://dev-api.kt-event.com/ai/v1 + description: 개발 서버 + - url: http://localhost:8083/ai/v1 + description: 로컬 개발 서버 + +tags: + - name: AI Analysis + description: AI 기반 트렌드 분석 및 추천 엔드포인트 + - name: Job Status + description: 비동기 Job 상태 조회 엔드포인트 + +paths: + /analyze-trends: + post: + tags: + - AI Analysis + summary: 트렌드 분석 요청 + description: | + 업종, 지역, 시즌을 기반으로 트렌드 분석을 수행합니다. + + ## 처리 방식 + - **비동기 처리**: Kafka를 통한 비동기 Job 생성 + - **응답 시간**: 즉시 Job ID 반환 (< 100ms) + - **실제 처리 시간**: 5~30초 이내 (AI API 응답 시간 포함) + + ## 캐싱 전략 + - 캐시 키: `trend:{업종}:{지역}` + - TTL: 1시간 + - 캐시 히트 시 즉시 응답 + + ## Circuit Breaker + - Failure Rate Threshold: 50% + - Timeout: 30초 + - Half-Open Wait Duration: 30초 + operationId: analyzeTrends + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TrendAnalysisRequest" + examples: + restaurant: + summary: 음식점 트렌드 분석 + value: + eventDraftId: "evt_draft_001" + industry: "음식점" + region: "서울 강남구" + purpose: "신규 고객 유치" + storeInfo: + storeName: "맛있는 고깃집" + storeSize: "중형" + monthlyRevenue: 30000000 + cafe: + summary: 카페 트렌드 분석 + value: + eventDraftId: "evt_draft_002" + industry: "카페" + region: "서울 홍대" + purpose: "재방문 유도" + storeInfo: + storeName: "커피스토리" + storeSize: "소형" + monthlyRevenue: 15000000 + responses: + "202": + description: | + 트렌드 분석 Job이 생성되었습니다. + - Job ID를 사용하여 `/jobs/{jobId}` 엔드포인트로 결과 폴링 + - 예상 처리 시간: 5~30초 + content: + application/json: + schema: + $ref: "#/components/schemas/JobCreatedResponse" + example: + jobId: "job_ai_20250122_001" + status: "PROCESSING" + message: "트렌드 분석이 진행 중입니다" + estimatedCompletionTime: "2025-01-22T10:05:30Z" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalServerError" + + /recommend-events: + post: + tags: + - AI Analysis + summary: 이벤트 추천 요청 + description: | + 트렌드 분석 결과를 기반으로 3가지 차별화된 이벤트 기획안을 생성합니다. + + ## 처리 방식 + - **비동기 처리**: Kafka를 통한 비동기 Job 생성 + - **응답 시간**: 즉시 Job ID 반환 (< 100ms) + - **실제 처리 시간**: 5~30초 이내 + + ## 3가지 추천 옵션 + 1. **저비용 옵션**: 높은 참여율 중심 + 2. **중비용 옵션**: 균형잡힌 ROI + 3. **고비용 옵션**: 높은 매출 증대 효과 + + ## 병렬 처리 + - 3가지 옵션 동시 생성 (병렬) + - 전체 처리 시간: 단일 요청 시간과 동일 + + ## 캐싱 전략 + - 캐시 키: `ai:recommendation:{eventDraftId}` + - TTL: 24시간 + operationId: recommendEvents + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventRecommendationRequest" + examples: + newCustomer: + summary: 신규 고객 유치 이벤트 + value: + eventDraftId: "evt_draft_001" + purpose: "신규 고객 유치" + industry: "음식점" + region: "서울 강남구" + storeInfo: + storeName: "맛있는 고깃집" + storeSize: "중형" + monthlyRevenue: 30000000 + trendAnalysisJobId: "job_ai_20250122_001" + responses: + "202": + description: | + 이벤트 추천 Job이 생성되었습니다. + - Job ID를 사용하여 `/jobs/{jobId}` 엔드포인트로 결과 폴링 + - 예상 처리 시간: 5~30초 + content: + application/json: + schema: + $ref: "#/components/schemas/JobCreatedResponse" + example: + jobId: "job_ai_20250122_002" + status: "PROCESSING" + message: "이벤트 추천이 진행 중입니다" + estimatedCompletionTime: "2025-01-22T10:05:30Z" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalServerError" + + /jobs/{jobId}: + get: + tags: + - Job Status + summary: Job 상태 조회 + description: | + 비동기 Job의 처리 상태 및 결과를 조회합니다. + + ## 폴링 전략 + - **초기 폴링**: 1초 간격 (처음 5회) + - **장기 폴링**: 3초 간격 (이후) + - **최대 대기 시간**: 60초 + + ## Job 상태 + - `PENDING`: 대기 중 + - `PROCESSING`: 처리 중 + - `COMPLETED`: 완료 + - `FAILED`: 실패 + + ## 캐싱 + - Redis를 통한 Job 상태 저장 + - 키: `job:{jobId}` + operationId: getJobStatus + security: + - bearerAuth: [] + parameters: + - name: jobId + in: path + required: true + description: Job ID + schema: + type: string + example: "job_ai_20250122_001" + responses: + "200": + description: Job 상태 조회 성공 + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/TrendAnalysisJobResponse" + - $ref: "#/components/schemas/EventRecommendationJobResponse" + examples: + processing: + summary: 처리 중 + value: + jobId: "job_ai_20250122_001" + status: "PROCESSING" + message: "트렌드 분석 중입니다" + progress: 50 + createdAt: "2025-01-22T10:05:00Z" + estimatedCompletionTime: "2025-01-22T10:05:30Z" + trendCompleted: + summary: 트렌드 분석 완료 + value: + jobId: "job_ai_20250122_001" + status: "COMPLETED" + message: "트렌드 분석이 완료되었습니다" + result: + industryTrends: + successfulEventTypes: + - type: "할인 이벤트" + successRate: 85 + - type: "경품 추첨" + successRate: 78 + popularPrizes: + - prize: "커피 쿠폰" + preferenceScore: 92 + - prize: "현금 할인" + preferenceScore: 88 + effectiveParticipationMethods: + - method: "간단한 설문조사" + engagementRate: 75 + regionalCharacteristics: + successRate: 82 + demographicProfile: + ageGroups: + - range: "20-29" + percentage: 35 + - range: "30-39" + percentage: 40 + genderDistribution: + male: 45 + female: 55 + seasonalPatterns: + currentSeason: "겨울" + recommendedEventTypes: + - "따뜻한 음료 할인" + - "연말 감사 이벤트" + specialOccasions: + - occasion: "설날" + daysUntil: 15 + completedAt: "2025-01-22T10:05:25Z" + recommendationCompleted: + summary: 이벤트 추천 완료 + value: + jobId: "job_ai_20250122_002" + status: "COMPLETED" + message: "이벤트 추천이 완료되었습니다" + result: + recommendations: + - option: 1 + title: "신규 고객 환영 커피 쿠폰 증정" + budget: "low" + prize: + name: "아메리카노 쿠폰" + quantity: 100 + estimatedCost: 300000 + participationMethod: + type: "간단한 설문조사" + difficulty: "low" + description: "매장 방문 후 QR 코드 스캔 및 간단한 설문" + estimatedParticipants: 150 + estimatedROI: 250 + promotionalTexts: + - "따뜻한 커피 한 잔으로 시작하는 하루!" + - "신규 방문 고객님께 특별한 선물" + hashtags: + - "#강남맛집" + - "#커피쿠폰" + - "#신규고객환영" + - option: 2 + title: "런치 세트 20% 할인 이벤트" + budget: "medium" + prize: + name: "런치 세트 할인권" + quantity: 50 + estimatedCost: 500000 + participationMethod: + type: "재방문 미션" + difficulty: "medium" + description: "첫 방문 후 리뷰 작성 시 할인권 제공" + estimatedParticipants: 80 + estimatedROI: 320 + promotionalTexts: + - "점심 시간, 특별한 할인 기회!" + - "리뷰 남기고 할인받자" + hashtags: + - "#강남맛집" + - "#런치할인" + - "#점심특가" + - option: 3 + title: "디너 코스 무료 업그레이드 추첨" + budget: "high" + prize: + name: "디너 코스 업그레이드권" + quantity: 10 + estimatedCost: 1000000 + participationMethod: + type: "바이럴 확산" + difficulty: "high" + description: "SNS 공유 및 친구 태그 3명 이상" + estimatedParticipants: 200 + estimatedROI: 450 + promotionalTexts: + - "프리미엄 디너 코스로 업그레이드!" + - "친구와 함께 즐기는 특별한 저녁" + hashtags: + - "#강남맛집" + - "#디너코스" + - "#프리미엄디너" + completedAt: "2025-01-22T10:05:35Z" + failed: + summary: 처리 실패 + value: + jobId: "job_ai_20250122_003" + status: "FAILED" + message: "AI API 오류로 인해 처리에 실패했습니다" + error: + code: "AI_API_ERROR" + detail: "External AI API timeout after 30 seconds" + failedAt: "2025-01-22T10:05:45Z" + "404": + $ref: "#/components/responses/NotFound" + "401": + $ref: "#/components/responses/Unauthorized" + "500": + $ref: "#/components/responses/InternalServerError" + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT 토큰 기반 인증 + - User Service에서 발급한 JWT 토큰 사용 + - 헤더: `Authorization: Bearer {token}` + + schemas: + TrendAnalysisRequest: + type: object + required: + - eventDraftId + - industry + - region + - purpose + properties: + eventDraftId: + type: string + description: 이벤트 초안 ID (Event Service에서 생성) + example: "evt_draft_001" + industry: + type: string + description: 업종 + enum: + - 음식점 + - 카페 + - 소매점 + - 뷰티/미용 + - 의료/헬스케어 + - 기타 + example: "음식점" + region: + type: string + description: 지역 (시/구/동) + example: "서울 강남구" + purpose: + type: string + description: 이벤트 목적 + enum: + - 신규 고객 유치 + - 재방문 유도 + - 매출 증대 + - 인지도 향상 + example: "신규 고객 유치" + storeInfo: + $ref: "#/components/schemas/StoreInfo" + + EventRecommendationRequest: + type: object + required: + - eventDraftId + - purpose + - industry + - region + - storeInfo + properties: + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt_draft_001" + purpose: + type: string + description: 이벤트 목적 + enum: + - 신규 고객 유치 + - 재방문 유도 + - 매출 증대 + - 인지도 향상 + example: "신규 고객 유치" + industry: + type: string + description: 업종 + example: "음식점" + region: + type: string + description: 지역 + example: "서울 강남구" + storeInfo: + $ref: "#/components/schemas/StoreInfo" + trendAnalysisJobId: + type: string + description: | + 트렌드 분석 Job ID (선택) + - 제공 시 해당 트렌드 분석 결과 재사용 + - 미제공 시 새로운 트렌드 분석 수행 + example: "job_ai_20250122_001" + + StoreInfo: + type: object + required: + - storeName + properties: + storeName: + type: string + description: 매장명 + example: "맛있는 고깃집" + storeSize: + type: string + description: 매장 크기 + enum: + - 소형 + - 중형 + - 대형 + example: "중형" + monthlyRevenue: + type: integer + format: int64 + description: 월 평균 매출 (원) + minimum: 0 + example: 30000000 + + JobCreatedResponse: + type: object + required: + - jobId + - status + - message + properties: + jobId: + type: string + description: Job ID (폴링 조회용) + example: "job_ai_20250122_001" + status: + type: string + enum: + - PENDING + - PROCESSING + description: Job 상태 + example: "PROCESSING" + message: + type: string + description: 상태 메시지 + example: "트렌드 분석이 진행 중입니다" + estimatedCompletionTime: + type: string + format: date-time + description: 예상 완료 시간 (ISO 8601) + example: "2025-01-22T10:05:30Z" + + TrendAnalysisJobResponse: + type: object + required: + - jobId + - status + - message + - createdAt + properties: + jobId: + type: string + description: Job ID + example: "job_ai_20250122_001" + status: + type: string + enum: + - PENDING + - PROCESSING + - COMPLETED + - FAILED + description: Job 상태 + example: "COMPLETED" + message: + type: string + description: 상태 메시지 + example: "트렌드 분석이 완료되었습니다" + progress: + type: integer + minimum: 0 + maximum: 100 + description: 진행률 (%) + example: 100 + result: + $ref: "#/components/schemas/TrendAnalysisResult" + error: + $ref: "#/components/schemas/JobError" + createdAt: + type: string + format: date-time + description: Job 생성 시간 + example: "2025-01-22T10:05:00Z" + estimatedCompletionTime: + type: string + format: date-time + description: 예상 완료 시간 (PROCESSING 상태일 때만) + example: "2025-01-22T10:05:30Z" + completedAt: + type: string + format: date-time + description: Job 완료 시간 (COMPLETED 상태일 때만) + example: "2025-01-22T10:05:25Z" + failedAt: + type: string + format: date-time + description: Job 실패 시간 (FAILED 상태일 때만) + example: "2025-01-22T10:05:45Z" + + EventRecommendationJobResponse: + type: object + required: + - jobId + - status + - message + - createdAt + properties: + jobId: + type: string + description: Job ID + example: "job_ai_20250122_002" + status: + type: string + enum: + - PENDING + - PROCESSING + - COMPLETED + - FAILED + description: Job 상태 + example: "COMPLETED" + message: + type: string + description: 상태 메시지 + example: "이벤트 추천이 완료되었습니다" + progress: + type: integer + minimum: 0 + maximum: 100 + description: 진행률 (%) + example: 100 + result: + $ref: "#/components/schemas/EventRecommendationResult" + error: + $ref: "#/components/schemas/JobError" + createdAt: + type: string + format: date-time + description: Job 생성 시간 + example: "2025-01-22T10:05:00Z" + estimatedCompletionTime: + type: string + format: date-time + description: 예상 완료 시간 (PROCESSING 상태일 때만) + example: "2025-01-22T10:05:30Z" + completedAt: + type: string + format: date-time + description: Job 완료 시간 (COMPLETED 상태일 때만) + example: "2025-01-22T10:05:35Z" + failedAt: + type: string + format: date-time + description: Job 실패 시간 (FAILED 상태일 때만) + example: "2025-01-22T10:05:45Z" + + TrendAnalysisResult: + type: object + required: + - industryTrends + - regionalCharacteristics + - seasonalPatterns + properties: + industryTrends: + $ref: "#/components/schemas/IndustryTrends" + regionalCharacteristics: + $ref: "#/components/schemas/RegionalCharacteristics" + seasonalPatterns: + $ref: "#/components/schemas/SeasonalPatterns" + + IndustryTrends: + type: object + required: + - successfulEventTypes + - popularPrizes + - effectiveParticipationMethods + properties: + successfulEventTypes: + type: array + description: 최근 성공한 이벤트 유형 (최대 5개) + items: + type: object + required: + - type + - successRate + properties: + type: + type: string + description: 이벤트 유형 + example: "할인 이벤트" + successRate: + type: integer + minimum: 0 + maximum: 100 + description: 성공률 (%) + example: 85 + popularPrizes: + type: array + description: 고객 선호 경품 Top 5 + items: + type: object + required: + - prize + - preferenceScore + properties: + prize: + type: string + description: 경품명 + example: "커피 쿠폰" + preferenceScore: + type: integer + minimum: 0 + maximum: 100 + description: 선호도 점수 + example: 92 + effectiveParticipationMethods: + type: array + description: 효과적인 참여 방법 + items: + type: object + required: + - method + - engagementRate + properties: + method: + type: string + description: 참여 방법 + example: "간단한 설문조사" + engagementRate: + type: integer + minimum: 0 + maximum: 100 + description: 참여율 (%) + example: 75 + + RegionalCharacteristics: + type: object + required: + - successRate + - demographicProfile + properties: + successRate: + type: integer + minimum: 0 + maximum: 100 + description: 해당 지역 이벤트 성공률 (%) + example: 82 + demographicProfile: + type: object + required: + - ageGroups + - genderDistribution + properties: + ageGroups: + type: array + description: 연령대별 분포 + items: + type: object + required: + - range + - percentage + properties: + range: + type: string + description: 연령대 + example: "20-29" + percentage: + type: integer + minimum: 0 + maximum: 100 + description: 비율 (%) + example: 35 + genderDistribution: + type: object + required: + - male + - female + properties: + male: + type: integer + minimum: 0 + maximum: 100 + description: 남성 비율 (%) + example: 45 + female: + type: integer + minimum: 0 + maximum: 100 + description: 여성 비율 (%) + example: 55 + + SeasonalPatterns: + type: object + required: + - currentSeason + - recommendedEventTypes + properties: + currentSeason: + type: string + description: 현재 계절 + enum: + - 봄 + - 여름 + - 가을 + - 겨울 + example: "겨울" + recommendedEventTypes: + type: array + description: 계절별 추천 이벤트 유형 + items: + type: string + example: + - "따뜻한 음료 할인" + - "연말 감사 이벤트" + specialOccasions: + type: array + description: 다가오는 특별 이벤트 (명절, 기념일 등) + items: + type: object + required: + - occasion + - daysUntil + properties: + occasion: + type: string + description: 특별 이벤트명 + example: "설날" + daysUntil: + type: integer + description: 남은 일수 + example: 15 + + EventRecommendationResult: + type: object + required: + - recommendations + properties: + recommendations: + type: array + description: 3가지 이벤트 추천안 + minItems: 3 + maxItems: 3 + items: + $ref: "#/components/schemas/EventRecommendation" + + EventRecommendation: + type: object + required: + - option + - title + - budget + - prize + - participationMethod + - estimatedParticipants + - estimatedROI + - promotionalTexts + - hashtags + properties: + option: + type: integer + description: 옵션 번호 (1, 2, 3) + enum: [1, 2, 3] + example: 1 + title: + type: string + description: 이벤트 제목 (수정 가능) + maxLength: 50 + example: "신규 고객 환영 커피 쿠폰 증정" + budget: + type: string + description: 예산 수준 + enum: + - low + - medium + - high + example: "low" + prize: + type: object + required: + - name + - quantity + - estimatedCost + properties: + name: + type: string + description: 경품명 (수정 가능) + example: "아메리카노 쿠폰" + quantity: + type: integer + minimum: 1 + description: 경품 수량 + example: 100 + estimatedCost: + type: integer + minimum: 0 + description: 예상 비용 (원) + example: 300000 + participationMethod: + type: object + required: + - type + - difficulty + - description + properties: + type: + type: string + description: 참여 방법 유형 + example: "간단한 설문조사" + difficulty: + type: string + description: 난이도 + enum: + - low + - medium + - high + example: "low" + description: + type: string + description: 참여 방법 상세 설명 + example: "매장 방문 후 QR 코드 스캔 및 간단한 설문" + estimatedParticipants: + type: integer + minimum: 0 + description: 예상 참여자 수 + example: 150 + estimatedROI: + type: integer + minimum: 0 + description: 예상 투자 대비 수익률 (%) + example: 250 + promotionalTexts: + type: array + description: 홍보 문구 (5개) + minItems: 5 + maxItems: 5 + items: + type: string + example: + - "따뜻한 커피 한 잔으로 시작하는 하루!" + - "신규 방문 고객님께 특별한 선물" + - "강남 최고의 고깃집에서 만나요" + - "지금 바로 참여하세요!" + - "한정 수량! 서두르세요!" + hashtags: + type: array + description: SNS 해시태그 (자동 생성) + items: + type: string + example: + - "#강남맛집" + - "#커피쿠폰" + - "#신규고객환영" + + JobError: + type: object + required: + - code + - detail + properties: + code: + type: string + description: 에러 코드 + enum: + - AI_API_ERROR + - TIMEOUT + - CIRCUIT_BREAKER_OPEN + - INVALID_PARAMETERS + - INTERNAL_ERROR + example: "AI_API_ERROR" + detail: + type: string + description: 에러 상세 메시지 + example: "External AI API timeout after 30 seconds" + + ErrorResponse: + type: object + required: + - error + - message + - timestamp + properties: + error: + type: string + description: 에러 타입 + example: "BAD_REQUEST" + message: + type: string + description: 에러 메시지 + example: "필수 파라미터가 누락되었습니다" + details: + type: array + description: 상세 에러 정보 + items: + type: object + properties: + field: + type: string + description: 에러 필드 + example: "industry" + message: + type: string + description: 필드별 에러 메시지 + example: "업종은 필수 입력 항목입니다" + timestamp: + type: string + format: date-time + description: 에러 발생 시간 + example: "2025-01-22T10:05:00Z" + + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + error: "BAD_REQUEST" + message: "필수 파라미터가 누락되었습니다" + details: + - field: "industry" + message: "업종은 필수 입력 항목입니다" + timestamp: "2025-01-22T10:05:00Z" + + Unauthorized: + description: 인증 실패 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + error: "UNAUTHORIZED" + message: "유효하지 않은 인증 토큰입니다" + timestamp: "2025-01-22T10:05:00Z" + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + error: "NOT_FOUND" + message: "Job을 찾을 수 없습니다" + timestamp: "2025-01-22T10:05:00Z" + + TooManyRequests: + description: 요청 제한 초과 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + error: "TOO_MANY_REQUESTS" + message: "요청 제한을 초과했습니다. 잠시 후 다시 시도해주세요" + timestamp: "2025-01-22T10:05:00Z" + headers: + Retry-After: + description: 재시도 가능 시간 (초) + schema: + type: integer + example: 60 + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + example: + error: "INTERNAL_SERVER_ERROR" + message: "서버 내부 오류가 발생했습니다" + timestamp: "2025-01-22T10:05:00Z" diff --git a/design/backend/api/analytics-service-api.yaml b/design/backend/api/analytics-service-api.yaml new file mode 100644 index 0000000..42124bb --- /dev/null +++ b/design/backend/api/analytics-service-api.yaml @@ -0,0 +1,942 @@ +openapi: 3.0.3 +info: + title: Analytics Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Analytics Service API + + 이벤트 성과 분석 및 통합 대시보드를 제공하는 서비스입니다. + + **주요 기능:** + - 실시간 성과 분석 대시보드 (UFR-ANAL-010) + - 채널별 성과 추적 + - ROI 계산 및 비교 분석 + - 참여자 프로필 분석 + + **데이터 소스:** + - Participation Service: 참여자 데이터 + - Distribution Service: 채널별 노출 수 + - 외부 API: 우리동네TV, 지니TV, SNS 통계 + - POS 시스템: 매출 데이터 (연동 시) + version: 1.0.0 + contact: + name: Analytics Service Team + email: analytics@kt-event-service.com + +servers: + - url: https://api.kt-event-service.com/analytics/v1 + description: Production Server + - url: https://dev-api.kt-event-service.com/analytics/v1 + description: Development Server + - url: http://localhost:8084/api + description: Local Development Server + +tags: + - name: Analytics + description: 성과 분석 대시보드 API + - name: Health + description: 서비스 헬스 체크 + +paths: + /health: + get: + tags: + - Health + summary: 헬스 체크 + description: Analytics Service의 상태를 확인합니다. + operationId: healthCheck + responses: + '200': + description: 서비스 정상 + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "UP" + service: + type: string + example: "analytics-service" + timestamp: + type: string + format: date-time + example: "2025-10-22T10:00:00Z" + + /events/{eventId}/analytics: + get: + tags: + - Analytics + summary: 이벤트 성과 분석 대시보드 조회 + description: | + 특정 이벤트의 실시간 성과 분석 데이터를 조회합니다. + + **유저스토리:** UFR-ANAL-010 - 성과 분석 대시보드 + + **주요 기능:** + - 4개 요약 카드 (참여자 수, 노출 수, ROI, 매출 증가율) + - 채널별 성과 분석 + - 시간대별 참여 추이 + - 참여자 프로필 분석 + - 비교 분석 (업종 평균, 이전 이벤트) + + **캐싱 전략:** + - Redis Cache-Aside 패턴 + - TTL: 300초 (5분) + - Cache HIT 시 응답 시간: 약 0.5초 + - Cache MISS 시 응답 시간: 약 3초 + + **데이터 업데이트:** + - 실시간 업데이트: Kafka 이벤트 구독 + - EventCreated: 통계 초기화 + - ParticipantRegistered: 참여자 수 증가 + - DistributionCompleted: 배포 통계 업데이트 + operationId: getEventAnalytics + security: + - BearerAuth: [] + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID (UUID) + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + responses: + '200': + description: 대시보드 데이터 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + examples: + success: + summary: 성공 응답 예시 + value: + eventId: "550e8400-e29b-41d4-a716-446655440000" + storeId: "660e8400-e29b-41d4-a716-446655440001" + eventTitle: "신규 고객 환영 이벤트" + summaryCards: + totalParticipants: + count: 1234 + targetGoal: 1000 + achievementRate: 123.4 + dailyChange: 150 + totalViews: + count: 17200 + yesterdayChange: 5.2 + channelBreakdown: + - channel: "우리동네TV" + views: 5000 + - channel: "지니TV" + views: 10000 + - channel: "Instagram" + views: 2000 + - channel: "Naver Blog" + views: 200 + roi: + value: 250.0 + industryAverage: 180.0 + comparisonRate: 138.9 + totalCost: 1000000 + totalRevenue: 3500000 + breakEvenStatus: "ACHIEVED" + salesGrowth: + rate: 15.2 + beforeEventSales: 5000000 + afterEventSales: 5760000 + periodComparison: "이벤트 전후 7일 비교" + channelPerformance: + - channel: "우리동네TV" + views: 5000 + participants: 400 + conversionRate: 8.0 + costPerAcquisition: 2500 + status: "SUCCESS" + - channel: "지니TV" + views: 10000 + participants: 500 + conversionRate: 5.0 + costPerAcquisition: 4000 + status: "SUCCESS" + - channel: "Instagram" + views: 2000 + participants: 200 + conversionRate: 10.0 + costPerAcquisition: 1000 + status: "SUCCESS" + - channel: "Naver Blog" + views: 200 + participants: 100 + conversionRate: 50.0 + costPerAcquisition: 500 + status: "SUCCESS" + - channel: "Kakao Channel" + views: 0 + participants: 34 + conversionRate: 0 + costPerAcquisition: 0 + status: "SUCCESS" + participationTrend: + timeUnit: "DAILY" + dataPoints: + - timestamp: "2025-10-15T00:00:00Z" + participantCount: 100 + - timestamp: "2025-10-16T00:00:00Z" + participantCount: 250 + - timestamp: "2025-10-17T00:00:00Z" + participantCount: 400 + - timestamp: "2025-10-18T00:00:00Z" + participantCount: 600 + - timestamp: "2025-10-19T00:00:00Z" + participantCount: 850 + - timestamp: "2025-10-20T00:00:00Z" + participantCount: 1050 + - timestamp: "2025-10-21T00:00:00Z" + participantCount: 1234 + peakTime: + timestamp: "2025-10-20T18:00:00Z" + participantCount: 200 + roiAnalysis: + totalCost: + prizeCost: 500000 + channelCosts: + - channel: "우리동네TV" + cost: 100000 + - channel: "지니TV" + cost: 200000 + - channel: "Instagram" + cost: 50000 + - channel: "Naver Blog" + cost: 50000 + - channel: "Kakao Channel" + cost: 0 + otherCosts: 100000 + total: 1000000 + expectedRevenue: + salesIncrease: 760000 + newCustomerLTV: 2740000 + total: 3500000 + roiCalculation: + formula: "(수익 - 비용) / 비용 × 100" + roi: 250.0 + breakEvenPoint: + required: 1000000 + achieved: 3500000 + status: "ACHIEVED" + participantProfile: + ageDistribution: + - ageGroup: "10대" + count: 50 + percentage: 4.1 + - ageGroup: "20대" + count: 300 + percentage: 24.3 + - ageGroup: "30대" + count: 450 + percentage: 36.5 + - ageGroup: "40대" + count: 300 + percentage: 24.3 + - ageGroup: "50대 이상" + count: 134 + percentage: 10.8 + genderDistribution: + - gender: "남성" + count: 600 + percentage: 48.6 + - gender: "여성" + count: 634 + percentage: 51.4 + regionDistribution: + - region: "서울" + count: 500 + percentage: 40.5 + - region: "경기" + count: 400 + percentage: 32.4 + - region: "기타" + count: 334 + percentage: 27.1 + timeDistribution: + - timeSlot: "오전 (06:00-12:00)" + count: 200 + percentage: 16.2 + - timeSlot: "오후 (12:00-18:00)" + count: 500 + percentage: 40.5 + - timeSlot: "저녁 (18:00-24:00)" + count: 500 + percentage: 40.5 + - timeSlot: "새벽 (00:00-06:00)" + count: 34 + percentage: 2.8 + comparativeAnalysis: + industryComparison: + - metric: "참여율" + myValue: 7.2 + industryAverage: 5.5 + percentageDifference: 30.9 + - metric: "ROI" + myValue: 250.0 + industryAverage: 180.0 + percentageDifference: 38.9 + - metric: "전환율" + myValue: 8.5 + industryAverage: 6.0 + percentageDifference: 41.7 + previousEventComparison: + - metric: "참여자 수" + currentValue: 1234 + previousBest: 1000 + improvementRate: 23.4 + - metric: "ROI" + currentValue: 250.0 + previousBest: 200.0 + improvementRate: 25.0 + - metric: "매출 증가율" + currentValue: 15.2 + previousBest: 12.0 + improvementRate: 26.7 + lastUpdated: "2025-10-22T10:30:00Z" + cacheStatus: "HIT" + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidEventId: + summary: 잘못된 이벤트 ID + value: + error: "INVALID_REQUEST" + message: "유효하지 않은 이벤트 ID 형식입니다." + timestamp: "2025-10-22T10:00:00Z" + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + unauthorized: + summary: 인증되지 않은 요청 + value: + error: "UNAUTHORIZED" + message: "인증이 필요합니다." + timestamp: "2025-10-22T10:00:00Z" + '403': + description: 권한 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + forbidden: + summary: 권한 없음 + value: + error: "FORBIDDEN" + message: "해당 이벤트의 통계를 조회할 권한이 없습니다." + timestamp: "2025-10-22T10:00:00Z" + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 이벤트 미존재 + value: + error: "EVENT_NOT_FOUND" + message: "해당 이벤트를 찾을 수 없습니다." + timestamp: "2025-10-22T10:00:00Z" + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + internalError: + summary: 서버 오류 + value: + error: "INTERNAL_SERVER_ERROR" + message: "서버 내부 오류가 발생했습니다." + timestamp: "2025-10-22T10:00:00Z" + '503': + description: 외부 API 서비스 이용 불가 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + externalServiceUnavailable: + summary: 외부 서비스 장애 + value: + error: "EXTERNAL_SERVICE_UNAVAILABLE" + message: "일부 채널 데이터를 불러올 수 없습니다. Fallback 데이터를 사용합니다." + timestamp: "2025-10-22T10:00:00Z" + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰 기반 인증. Authorization 헤더에 "Bearer {token}" 형식으로 전달합니다. + + schemas: + DashboardResponse: + type: object + required: + - eventId + - storeId + - eventTitle + - summaryCards + - channelPerformance + - participationTrend + - roiAnalysis + - participantProfile + - comparativeAnalysis + - lastUpdated + properties: + eventId: + type: string + format: uuid + description: 이벤트 ID + example: "550e8400-e29b-41d4-a716-446655440000" + storeId: + type: string + format: uuid + description: 매장 ID + example: "660e8400-e29b-41d4-a716-446655440001" + eventTitle: + type: string + description: 이벤트 제목 + example: "신규 고객 환영 이벤트" + summaryCards: + $ref: '#/components/schemas/SummaryCards' + channelPerformance: + type: array + description: 채널별 성과 분석 + items: + $ref: '#/components/schemas/ChannelPerformance' + participationTrend: + $ref: '#/components/schemas/ParticipationTrend' + roiAnalysis: + $ref: '#/components/schemas/ROIAnalysis' + participantProfile: + $ref: '#/components/schemas/ParticipantProfile' + comparativeAnalysis: + $ref: '#/components/schemas/ComparativeAnalysis' + lastUpdated: + type: string + format: date-time + description: 마지막 업데이트 시각 + example: "2025-10-22T10:30:00Z" + cacheStatus: + type: string + enum: [HIT, MISS] + description: 캐시 상태 (HIT/MISS) + example: "HIT" + + SummaryCards: + type: object + description: 4개 요약 카드 + required: + - totalParticipants + - totalViews + - roi + - salesGrowth + properties: + totalParticipants: + $ref: '#/components/schemas/TotalParticipantsCard' + totalViews: + $ref: '#/components/schemas/TotalViewsCard' + roi: + $ref: '#/components/schemas/ROICard' + salesGrowth: + $ref: '#/components/schemas/SalesGrowthCard' + + TotalParticipantsCard: + type: object + description: 총 참여자 수 카드 + required: + - count + - targetGoal + - achievementRate + properties: + count: + type: integer + description: 현재 참여자 수 + example: 1234 + targetGoal: + type: integer + description: 목표 참여자 수 + example: 1000 + achievementRate: + type: number + format: float + description: 목표 대비 달성률 (%) + example: 123.4 + dailyChange: + type: integer + description: 전일 대비 증가 수 + example: 150 + + TotalViewsCard: + type: object + description: 총 노출 수 카드 + required: + - count + - yesterdayChange + properties: + count: + type: integer + description: 총 노출 수 (채널별 노출 합계) + example: 17200 + yesterdayChange: + type: number + format: float + description: 전일 대비 증감률 (%) + example: 5.2 + channelBreakdown: + type: array + description: 채널별 노출 수 분포 + items: + type: object + properties: + channel: + type: string + description: 채널명 + example: "우리동네TV" + views: + type: integer + description: 노출 수 + example: 5000 + + ROICard: + type: object + description: 예상 투자 대비 수익률 카드 + required: + - value + - industryAverage + - comparisonRate + - totalCost + - totalRevenue + - breakEvenStatus + properties: + value: + type: number + format: float + description: 실시간 ROI (%) + example: 250.0 + industryAverage: + type: number + format: float + description: 업종 평균 ROI (%) + example: 180.0 + comparisonRate: + type: number + format: float + description: 업종 평균 대비 비율 (%) + example: 138.9 + totalCost: + type: integer + description: 총 비용 (원) + example: 1000000 + totalRevenue: + type: integer + description: 총 수익 (원) + example: 3500000 + breakEvenStatus: + type: string + enum: [ACHIEVED, NOT_ACHIEVED] + description: 손익분기점 달성 여부 + example: "ACHIEVED" + + SalesGrowthCard: + type: object + description: 매출 증가율 카드 + required: + - rate + properties: + rate: + type: number + format: float + description: 매출 증가율 (%) + example: 15.2 + beforeEventSales: + type: integer + description: 이벤트 전 매출 (원) + example: 5000000 + afterEventSales: + type: integer + description: 이벤트 후 매출 (원) + example: 5760000 + periodComparison: + type: string + description: 비교 기간 설명 + example: "이벤트 전후 7일 비교" + + ChannelPerformance: + type: object + description: 채널별 성과 분석 + required: + - channel + - views + - participants + - conversionRate + - costPerAcquisition + - status + properties: + channel: + type: string + description: 채널명 + example: "우리동네TV" + views: + type: integer + description: 노출 수 + example: 5000 + participants: + type: integer + description: 참여자 수 + example: 400 + conversionRate: + type: number + format: float + description: 전환율 (%) + example: 8.0 + costPerAcquisition: + type: integer + description: 비용 대비 효율 (CPA, 원) + example: 2500 + status: + type: string + enum: [SUCCESS, FAILED, PENDING] + description: 배포 상태 + example: "SUCCESS" + + ParticipationTrend: + type: object + description: 시간대별 참여 추이 + required: + - timeUnit + - dataPoints + properties: + timeUnit: + type: string + enum: [HOURLY, DAILY] + description: 시간 단위 (시간별/일별) + example: "DAILY" + dataPoints: + type: array + description: 시간대별 데이터 포인트 + items: + type: object + properties: + timestamp: + type: string + format: date-time + description: 시각 + example: "2025-10-15T00:00:00Z" + participantCount: + type: integer + description: 참여자 수 + example: 100 + peakTime: + type: object + description: 피크 시간대 + properties: + timestamp: + type: string + format: date-time + description: 피크 시각 + example: "2025-10-20T18:00:00Z" + participantCount: + type: integer + description: 피크 참여자 수 + example: 200 + + ROIAnalysis: + type: object + description: 투자 대비 수익률 상세 분석 + required: + - totalCost + - expectedRevenue + - roiCalculation + - breakEvenPoint + properties: + totalCost: + $ref: '#/components/schemas/TotalCost' + expectedRevenue: + $ref: '#/components/schemas/ExpectedRevenue' + roiCalculation: + $ref: '#/components/schemas/ROICalculation' + breakEvenPoint: + $ref: '#/components/schemas/BreakEvenPoint' + + TotalCost: + type: object + description: 총 비용 산출 + required: + - prizeCost + - channelCosts + - otherCosts + - total + properties: + prizeCost: + type: integer + description: 경품 비용 (원) + example: 500000 + channelCosts: + type: array + description: 채널별 플랫폼 비용 + items: + type: object + properties: + channel: + type: string + description: 채널명 + example: "우리동네TV" + cost: + type: integer + description: 비용 (원) + example: 100000 + otherCosts: + type: integer + description: 기타 비용 (원) + example: 100000 + total: + type: integer + description: 총 비용 (원) + example: 1000000 + + ExpectedRevenue: + type: object + description: 예상 수익 산출 + required: + - salesIncrease + - newCustomerLTV + - total + properties: + salesIncrease: + type: integer + description: 매출 증가액 (원) + example: 760000 + newCustomerLTV: + type: integer + description: 신규 고객 LTV (원) + example: 2740000 + total: + type: integer + description: 총 예상 수익 (원) + example: 3500000 + + ROICalculation: + type: object + description: ROI 계산 + required: + - formula + - roi + properties: + formula: + type: string + description: ROI 계산 공식 + example: "(수익 - 비용) / 비용 × 100" + roi: + type: number + format: float + description: ROI (%) + example: 250.0 + + BreakEvenPoint: + type: object + description: 손익분기점 + required: + - required + - achieved + - status + properties: + required: + type: integer + description: 손익분기점 (원) + example: 1000000 + achieved: + type: integer + description: 달성 금액 (원) + example: 3500000 + status: + type: string + enum: [ACHIEVED, NOT_ACHIEVED] + description: 달성 여부 + example: "ACHIEVED" + + ParticipantProfile: + type: object + description: 참여자 프로필 분석 + required: + - ageDistribution + - genderDistribution + - regionDistribution + - timeDistribution + properties: + ageDistribution: + type: array + description: 연령대별 분포 + items: + type: object + properties: + ageGroup: + type: string + description: 연령대 + example: "20대" + count: + type: integer + description: 인원 수 + example: 300 + percentage: + type: number + format: float + description: 비율 (%) + example: 24.3 + genderDistribution: + type: array + description: 성별 분포 + items: + type: object + properties: + gender: + type: string + description: 성별 + example: "남성" + count: + type: integer + description: 인원 수 + example: 600 + percentage: + type: number + format: float + description: 비율 (%) + example: 48.6 + regionDistribution: + type: array + description: 지역별 분포 + items: + type: object + properties: + region: + type: string + description: 지역 + example: "서울" + count: + type: integer + description: 인원 수 + example: 500 + percentage: + type: number + format: float + description: 비율 (%) + example: 40.5 + timeDistribution: + type: array + description: 참여 시간대 분석 + items: + type: object + properties: + timeSlot: + type: string + description: 시간대 + example: "오전 (06:00-12:00)" + count: + type: integer + description: 참여자 수 + example: 200 + percentage: + type: number + format: float + description: 비율 (%) + example: 16.2 + + ComparativeAnalysis: + type: object + description: 비교 분석 + required: + - industryComparison + - previousEventComparison + properties: + industryComparison: + type: array + description: 업종 평균과 비교 + items: + type: object + properties: + metric: + type: string + description: 지표명 + example: "참여율" + myValue: + type: number + format: float + description: 내 값 + example: 7.2 + industryAverage: + type: number + format: float + description: 업종 평균 + example: 5.5 + percentageDifference: + type: number + format: float + description: 차이율 (%) + example: 30.9 + previousEventComparison: + type: array + description: 내 이전 이벤트와 비교 + items: + type: object + properties: + metric: + type: string + description: 지표명 + example: "참여자 수" + currentValue: + type: number + format: float + description: 현재 값 + example: 1234 + previousBest: + type: number + format: float + description: 이전 최고 기록 + example: 1000 + improvementRate: + type: number + format: float + description: 개선율 (%) + example: 23.4 + + ErrorResponse: + type: object + description: 에러 응답 + required: + - error + - message + - timestamp + properties: + error: + type: string + description: 에러 코드 + example: "INVALID_REQUEST" + message: + type: string + description: 에러 메시지 + example: "유효하지 않은 요청입니다." + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-10-22T10:00:00Z" diff --git a/design/backend/api/content-service-api.yaml b/design/backend/api/content-service-api.yaml new file mode 100644 index 0000000..e171d60 --- /dev/null +++ b/design/backend/api/content-service-api.yaml @@ -0,0 +1,548 @@ +openapi: 3.0.3 +info: + title: Content Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Content Service API + + ## 주요 기능 + - SNS 이미지 생성 (UFR-CONT-010) + - 3가지 스타일 이미지 자동 생성 (심플, 화려한, 트렌디) + - AI 기반 이미지 생성 (Stable Diffusion / DALL-E) + - Circuit Breaker 및 Fallback 패턴 적용 + - Redis 캐싱 (TTL 7일) + + ## 비동기 처리 방식 + - Kafka 기반 Job 처리 + - 폴링 방식으로 결과 조회 + - Event Service와 느슨한 결합 + + version: 1.0.0 + contact: + name: Content Service Team + email: content-team@kt.com + +servers: + - url: https://api.kt-event.com/v1 + description: Production Server + - url: https://api-dev.kt-event.com/v1 + description: Development Server + +tags: + - name: Images + description: SNS 이미지 생성 및 조회 + +paths: + /api/content/images/generate: + post: + tags: + - Images + summary: SNS 이미지 생성 요청 + description: | + 이벤트 정보를 기반으로 3가지 스타일의 SNS 이미지 생성을 비동기로 요청합니다. + + ## 처리 방식 + - **비동기 처리**: Kafka image-job 토픽에 Job 발행 + - **폴링 조회**: jobId로 생성 상태 조회 (GET /api/content/images/{jobId}) + - **캐싱**: 동일한 eventDraftId 재요청 시 캐시 반환 (TTL 7일) + + ## 생성 스타일 + 1. **심플 스타일**: 깔끔한 디자인, 텍스트 중심 + 2. **화려한 스타일**: 눈에 띄는 디자인, 풍부한 색상 + 3. **트렌디 스타일**: 최신 트렌드, MZ세대 타겟 + + ## Resilience 패턴 + - Circuit Breaker (실패율 50% 초과 시 Open) + - Fallback (Stable Diffusion → DALL-E → 기본 템플릿) + - Timeout (20초) + + operationId: generateImages + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImageGenerationRequest' + examples: + basicEvent: + summary: 기본 이벤트 + value: + eventDraftId: "evt-draft-12345" + eventInfo: + title: "봄맞이 커피 할인 이벤트" + giftName: "아메리카노 1+1" + brandColor: "#FF5733" + logoUrl: "https://cdn.example.com/logo.png" + withoutLogo: + summary: 로고 없는 이벤트 + value: + eventDraftId: "evt-draft-67890" + eventInfo: + title: "신메뉴 출시 기념 경품 추첨" + giftName: "스타벅스 기프티콘 5000원권" + brandColor: "#00704A" + + responses: + '202': + description: | + 이미지 생성 요청이 성공적으로 접수되었습니다. + jobId를 사용하여 생성 상태를 폴링 조회하세요. + content: + application/json: + schema: + $ref: '#/components/schemas/ImageGenerationAcceptedResponse' + examples: + accepted: + summary: 요청 접수 성공 + value: + jobId: "job-img-abc123" + eventDraftId: "evt-draft-12345" + status: "PENDING" + estimatedCompletionTime: 5 + message: "이미지 생성 요청이 접수되었습니다. jobId로 결과를 조회하세요." + + '400': + description: 잘못된 요청 (필수 필드 누락, 유효하지 않은 데이터) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + missingField: + summary: 필수 필드 누락 + value: + code: "BAD_REQUEST" + message: "eventInfo.title은 필수 항목입니다." + timestamp: "2025-10-22T14:30:00Z" + invalidColor: + summary: 유효하지 않은 색상 코드 + value: + code: "BAD_REQUEST" + message: "brandColor는 HEX 색상 코드 형식이어야 합니다." + timestamp: "2025-10-22T14:30:00Z" + + '429': + description: 요청 제한 초과 (Rate Limiting) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + rateLimitExceeded: + summary: 요청 제한 초과 + value: + code: "RATE_LIMIT_EXCEEDED" + message: "요청 한도를 초과했습니다. 1분 후 다시 시도하세요." + timestamp: "2025-10-22T14:30:00Z" + retryAfter: 60 + + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + internalError: + summary: 서버 내부 오류 + value: + code: "INTERNAL_SERVER_ERROR" + message: "이미지 생성 요청 처리 중 오류가 발생했습니다." + timestamp: "2025-10-22T14:30:00Z" + + security: + - BearerAuth: [] + + /api/content/images/{jobId}: + get: + tags: + - Images + summary: 이미지 생성 상태 및 결과 조회 + description: | + jobId로 이미지 생성 상태를 조회합니다. + + ## 폴링 권장사항 + - **폴링 간격**: 2초 + - **최대 폴링 시간**: 30초 + - **Timeout 후 처리**: 에러 메시지 표시 및 재시도 옵션 제공 + + ## 상태 종류 + - **PENDING**: 대기 중 (Kafka Queue에서 대기) + - **PROCESSING**: 생성 중 (AI API 호출 진행) + - **COMPLETED**: 완료 (3가지 이미지 URL 반환) + - **FAILED**: 실패 (에러 메시지 포함) + + ## 캐싱 + - COMPLETED 상태는 Redis 캐싱 (TTL 7일) + - 동일한 eventDraftId 재요청 시 즉시 반환 + + operationId: getImageGenerationStatus + parameters: + - name: jobId + in: path + required: true + description: 이미지 생성 Job ID + schema: + type: string + example: "job-img-abc123" + + responses: + '200': + description: 이미지 생성 상태 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ImageGenerationStatusResponse' + examples: + pending: + summary: 대기 중 + value: + jobId: "job-img-abc123" + status: "PENDING" + message: "이미지 생성 대기 중입니다." + estimatedCompletionTime: 5 + + processing: + summary: 생성 중 + value: + jobId: "job-img-abc123" + status: "PROCESSING" + message: "AI가 이벤트에 어울리는 이미지를 생성하고 있어요..." + estimatedCompletionTime: 3 + + completed: + summary: 생성 완료 + value: + jobId: "job-img-abc123" + status: "COMPLETED" + message: "이미지 생성이 완료되었습니다." + images: + - style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + completedAt: "2025-10-22T14:30:05Z" + fromCache: false + + completedFromCache: + summary: 캐시에서 반환 + value: + jobId: "job-img-def456" + status: "COMPLETED" + message: "이미지 생성이 완료되었습니다. (캐시)" + images: + - style: "SIMPLE" + url: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "FANCY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-fancy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + - style: "TRENDY" + url: "https://cdn.kt-event.com/images/evt-draft-12345-trendy.png" + platform: "INSTAGRAM" + size: + width: 1080 + height: 1080 + completedAt: "2025-10-22T14:28:00Z" + fromCache: true + + failed: + summary: 생성 실패 + value: + jobId: "job-img-abc123" + status: "FAILED" + message: "이미지 생성에 실패했습니다." + error: + code: "IMAGE_GENERATION_FAILED" + detail: "외부 AI API 응답 시간 초과. 기본 템플릿으로 대체되었습니다." + completedAt: "2025-10-22T14:30:25Z" + + '404': + description: Job ID를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: Job ID 없음 + value: + code: "NOT_FOUND" + message: "Job ID를 찾을 수 없습니다." + timestamp: "2025-10-22T14:30:00Z" + + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + internalError: + summary: 서버 내부 오류 + value: + code: "INTERNAL_SERVER_ERROR" + message: "상태 조회 중 오류가 발생했습니다." + timestamp: "2025-10-22T14:30:00Z" + + security: + - BearerAuth: [] + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰을 Authorization 헤더에 포함 (Bearer {token}) + + schemas: + ImageGenerationRequest: + type: object + required: + - eventDraftId + - eventInfo + properties: + eventDraftId: + type: string + description: | + 이벤트 초안 ID (Event Service에서 발급) + 동일한 eventDraftId로 재요청 시 캐시된 이미지 반환 + example: "evt-draft-12345" + + eventInfo: + type: object + required: + - title + - giftName + properties: + title: + type: string + description: 이벤트 제목 (최대 50자) + minLength: 1 + maxLength: 50 + example: "봄맞이 커피 할인 이벤트" + + giftName: + type: string + description: 경품명 (최대 30자) + minLength: 1 + maxLength: 30 + example: "아메리카노 1+1" + + brandColor: + type: string + description: | + 브랜드 컬러 (HEX 색상 코드) + 사용자 프로필에서 가져오거나 기본값 사용 + pattern: '^#[0-9A-Fa-f]{6}$' + example: "#FF5733" + + logoUrl: + type: string + format: uri + description: | + 로고 이미지 URL (선택) + 업로드된 경우에만 제공 + example: "https://cdn.example.com/logo.png" + + ImageGenerationAcceptedResponse: + type: object + required: + - jobId + - eventDraftId + - status + - message + properties: + jobId: + type: string + description: 이미지 생성 Job ID (폴링 조회에 사용) + example: "job-img-abc123" + + eventDraftId: + type: string + description: 이벤트 초안 ID + example: "evt-draft-12345" + + status: + type: string + enum: [PENDING] + description: 초기 상태는 항상 PENDING + example: "PENDING" + + estimatedCompletionTime: + type: integer + description: 예상 완료 시간 (초) + example: 5 + + message: + type: string + description: 응답 메시지 + example: "이미지 생성 요청이 접수되었습니다. jobId로 결과를 조회하세요." + + ImageGenerationStatusResponse: + type: object + required: + - jobId + - status + - message + properties: + jobId: + type: string + description: 이미지 생성 Job ID + example: "job-img-abc123" + + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: | + Job 상태 + - PENDING: 대기 중 + - PROCESSING: 생성 중 + - COMPLETED: 완료 + - FAILED: 실패 + example: "COMPLETED" + + message: + type: string + description: 상태 메시지 + example: "이미지 생성이 완료되었습니다." + + estimatedCompletionTime: + type: integer + description: 예상 완료 시간 (초, PENDING/PROCESSING 상태에서만) + example: 3 + + images: + type: array + description: 생성된 이미지 배열 (COMPLETED 상태에서만) + items: + $ref: '#/components/schemas/GeneratedImage' + + completedAt: + type: string + format: date-time + description: 완료 시각 (COMPLETED/FAILED 상태에서만) + example: "2025-10-22T14:30:05Z" + + fromCache: + type: boolean + description: 캐시에서 반환 여부 (COMPLETED 상태에서만) + example: false + + error: + $ref: '#/components/schemas/JobError' + + GeneratedImage: + type: object + required: + - style + - url + - platform + - size + properties: + style: + type: string + enum: [SIMPLE, FANCY, TRENDY] + description: | + 이미지 스타일 + - SIMPLE: 심플 스타일 (깔끔한 디자인, 텍스트 중심) + - FANCY: 화려한 스타일 (눈에 띄는 디자인, 풍부한 색상) + - TRENDY: 트렌디 스타일 (최신 트렌드, MZ세대 타겟) + example: "SIMPLE" + + url: + type: string + format: uri + description: CDN 이미지 URL + example: "https://cdn.kt-event.com/images/evt-draft-12345-simple.png" + + platform: + type: string + enum: [INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: | + 플랫폼별 최적화 + - INSTAGRAM: 1080x1080 + - NAVER_BLOG: 800x600 + - KAKAO_CHANNEL: 800x800 + example: "INSTAGRAM" + + size: + type: object + required: + - width + - height + properties: + width: + type: integer + description: 이미지 너비 (픽셀) + example: 1080 + + height: + type: integer + description: 이미지 높이 (픽셀) + example: 1080 + + JobError: + type: object + required: + - code + - detail + description: Job 실패 시 에러 정보 (FAILED 상태에서만) + properties: + code: + type: string + description: 에러 코드 + example: "IMAGE_GENERATION_FAILED" + + detail: + type: string + description: 상세 에러 메시지 + example: "외부 AI API 응답 시간 초과. 기본 템플릿으로 대체되었습니다." + + ErrorResponse: + type: object + required: + - code + - message + - timestamp + properties: + code: + type: string + description: 에러 코드 + example: "BAD_REQUEST" + + message: + type: string + description: 에러 메시지 + example: "eventInfo.title은 필수 항목입니다." + + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: "2025-10-22T14:30:00Z" + + retryAfter: + type: integer + description: 재시도 대기 시간 (초, Rate Limiting 에러에서만) + example: 60 diff --git a/design/backend/api/distribution-service-api.yaml b/design/backend/api/distribution-service-api.yaml new file mode 100644 index 0000000..9790274 --- /dev/null +++ b/design/backend/api/distribution-service-api.yaml @@ -0,0 +1,764 @@ +openapi: 3.0.3 +info: + title: Distribution Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 배포 관리 서비스 API + + **주요 기능:** + - 다중 채널 배포 관리 + - 배포 상태 모니터링 + - 채널별 배포 결과 추적 + + **지원 배포 채널:** + - 우리동네TV + - 링고비즈 (연결음) + - 지니TV 광고 + - Instagram + - Naver Blog + - Kakao Channel + version: 1.0.0 + contact: + name: KT Event Marketing Team + email: support@kt-event-marketing.com + +servers: + - url: http://localhost:8085 + description: Local Development Server + - url: https://api-dev.kt-event-marketing.com + description: Development Server + - url: https://api.kt-event-marketing.com + description: Production Server + +tags: + - name: Distribution + description: 다중 채널 배포 관리 + - name: Status + description: 배포 상태 조회 및 모니터링 + +paths: + /api/distribution/distribute: + post: + tags: + - Distribution + summary: 다중 채널 배포 실행 + description: | + 선택된 모든 채널에 동시 배포를 실행합니다. + + **배포 프로세스:** + 1. 배포 이력 초기화 (상태: PENDING) + 2. 각 채널별 병렬 배포 처리 + 3. 배포 결과 집계 및 저장 + 4. Kafka 이벤트 발행 (Analytics Service 구독) + 5. Redis 캐시 저장 (TTL: 1시간) + + **Sprint 2 제약사항:** + - 외부 API 호출 없음 (Mock 처리) + - 모든 배포 요청은 성공으로 처리 + - 배포 로그만 DB에 기록 + + **유저스토리:** UFR-DIST-010 + operationId: distributeToChannels + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionRequest' + examples: + multiChannel: + summary: 다중 채널 배포 예시 + value: + eventId: "evt-12345" + channels: + - type: "WOORIDONGNE_TV" + config: + radius: "1km" + timeSlots: + - "weekday_evening" + - "weekend_lunch" + - type: "INSTAGRAM" + config: + scheduledTime: "2025-11-01T10:00:00Z" + - type: "NAVER_BLOG" + config: + scheduledTime: "2025-11-01T10:30:00Z" + contentUrls: + instagram: "https://cdn.example.com/images/event-instagram.jpg" + naverBlog: "https://cdn.example.com/images/event-naver.jpg" + kakaoChannel: "https://cdn.example.com/images/event-kakao.jpg" + responses: + '200': + description: 배포 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionResponse' + examples: + allSuccess: + summary: 모든 채널 배포 성공 + value: + distributionId: "dist-12345" + eventId: "evt-12345" + status: "COMPLETED" + completedAt: "2025-11-01T09:00:00Z" + results: + - channel: "WOORIDONGNE_TV" + status: "SUCCESS" + distributionId: "wtv-uuid-12345" + estimatedViews: 1000 + message: "배포 완료" + - channel: "INSTAGRAM" + status: "SUCCESS" + postUrl: "https://instagram.com/p/generated-post-id" + postId: "ig-post-12345" + message: "게시 완료" + - channel: "NAVER_BLOG" + status: "SUCCESS" + postUrl: "https://blog.naver.com/store123/generated-post" + message: "게시 완료" + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidEventId: + summary: 유효하지 않은 이벤트 ID + value: + error: "BAD_REQUEST" + message: "유효하지 않은 이벤트 ID입니다" + timestamp: "2025-11-01T09:00:00Z" + noChannels: + summary: 선택된 채널 없음 + value: + error: "BAD_REQUEST" + message: "최소 1개 이상의 채널을 선택해야 합니다" + timestamp: "2025-11-01T09:00:00Z" + '404': + description: 이벤트를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + eventNotFound: + summary: 존재하지 않는 이벤트 + value: + error: "NOT_FOUND" + message: "이벤트를 찾을 수 없습니다: evt-12345" + timestamp: "2025-11-01T09:00:00Z" + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + internalError: + summary: 서버 오류 + value: + error: "INTERNAL_SERVER_ERROR" + message: "배포 처리 중 오류가 발생했습니다" + timestamp: "2025-11-01T09:00:00Z" + + /api/distribution/{eventId}/status: + get: + tags: + - Status + summary: 배포 상태 조회 + description: | + 이벤트의 배포 상태를 조회합니다. + + **조회 프로세스:** + 1. Cache-Aside 패턴 적용 (Redis 캐시 우선 조회) + 2. 캐시 MISS 시 DB에서 배포 이력 조회 + 3. 진행중(IN_PROGRESS) 상태일 때만 외부 API로 실시간 상태 확인 + 4. Circuit Breaker 패턴 적용 (외부 API 호출 시) + 5. 배포 상태 캐싱 (TTL: 1시간) + + **응답 시간:** + - 캐시 HIT: 0.1초 + - 캐시 MISS: 0.5초 ~ 2초 + + **유저스토리:** UFR-DIST-020 + operationId: getDistributionStatus + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt-12345" + responses: + '200': + description: 배포 상태 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionStatusResponse' + examples: + completed: + summary: 배포 완료 상태 + value: + eventId: "evt-12345" + overallStatus: "COMPLETED" + completedAt: "2025-11-01T09:00:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + completedAt: "2025-11-01T09:00:00Z" + - channel: "RINGO_BIZ" + status: "COMPLETED" + updateTimestamp: "2025-11-01T09:00:00Z" + - channel: "GENIE_TV" + status: "COMPLETED" + adId: "gtv-uuid-12345" + impressionSchedule: + - "2025-11-01 18:00-20:00" + - "2025-11-02 12:00-14:00" + - channel: "INSTAGRAM" + status: "COMPLETED" + postUrl: "https://instagram.com/p/generated-post-id" + postId: "ig-post-12345" + - channel: "NAVER_BLOG" + status: "COMPLETED" + postUrl: "https://blog.naver.com/store123/generated-post" + - channel: "KAKAO_CHANNEL" + status: "COMPLETED" + messageId: "kakao-msg-12345" + inProgress: + summary: 배포 진행중 상태 + value: + eventId: "evt-12345" + overallStatus: "IN_PROGRESS" + startedAt: "2025-11-01T08:58:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + - channel: "INSTAGRAM" + status: "IN_PROGRESS" + progress: 50 + - channel: "NAVER_BLOG" + status: "PENDING" + partialFailure: + summary: 일부 채널 실패 상태 + value: + eventId: "evt-12345" + overallStatus: "PARTIAL_FAILURE" + completedAt: "2025-11-01T09:00:00Z" + channels: + - channel: "WOORIDONGNE_TV" + status: "COMPLETED" + distributionId: "wtv-uuid-12345" + estimatedViews: 1500 + - channel: "INSTAGRAM" + status: "FAILED" + errorMessage: "Instagram API 타임아웃" + retries: 3 + lastRetryAt: "2025-11-01T08:59:30Z" + - channel: "NAVER_BLOG" + status: "COMPLETED" + postUrl: "https://blog.naver.com/store123/generated-post" + '404': + description: 배포 이력을 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 배포 이력 없음 + value: + error: "NOT_FOUND" + message: "배포 이력을 찾을 수 없습니다: evt-12345" + timestamp: "2025-11-01T09:00:00Z" + '500': + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/distribution/{eventId}/retry: + post: + tags: + - Distribution + summary: 실패한 채널 재시도 + description: | + 실패한 채널에 대해 배포를 재시도합니다. + + **재시도 프로세스:** + 1. 실패한 채널 목록 검증 + 2. 새로운 배포 시도 로그 생성 + 3. Circuit Breaker 및 Retry 로직 적용 + 4. 캐시 무효화 + 5. 재시도 결과 반환 + + **재시도 제한:** + - 최대 재시도 횟수: 3회 + - Circuit Breaker가 OPEN 상태일 경우 30초 대기 후 시도 + operationId: retryDistribution + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt-12345" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RetryRequest' + examples: + retryFailedChannels: + summary: 실패한 채널 재시도 + value: + channels: + - "INSTAGRAM" + - "KAKAO_CHANNEL" + responses: + '200': + description: 재시도 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/RetryResponse' + examples: + success: + summary: 재시도 성공 + value: + eventId: "evt-12345" + retryStatus: "COMPLETED" + retriedAt: "2025-11-01T09:05:00Z" + results: + - channel: "INSTAGRAM" + status: "SUCCESS" + postUrl: "https://instagram.com/p/retry-post-id" + - channel: "KAKAO_CHANNEL" + status: "SUCCESS" + messageId: "kakao-retry-12345" + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 배포 이력을 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + schemas: + DistributionRequest: + type: object + required: + - eventId + - channels + - contentUrls + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + channels: + type: array + description: 배포할 채널 목록 + minItems: 1 + items: + $ref: '#/components/schemas/ChannelConfig' + contentUrls: + type: object + description: 플랫폼별 콘텐츠 URL + properties: + wooridongneTV: + type: string + description: 우리동네TV 영상 URL (15초) + example: "https://cdn.example.com/videos/event-15s.mp4" + ringoBiz: + type: string + description: 링고비즈 연결음 파일 URL + example: "https://cdn.example.com/audio/ringtone.mp3" + genieTV: + type: string + description: 지니TV 광고 영상 URL + example: "https://cdn.example.com/videos/event-ad.mp4" + instagram: + type: string + description: Instagram 이미지 URL (1080x1080) + example: "https://cdn.example.com/images/event-instagram.jpg" + naverBlog: + type: string + description: Naver Blog 이미지 URL (800x600) + example: "https://cdn.example.com/images/event-naver.jpg" + kakaoChannel: + type: string + description: Kakao Channel 이미지 URL (800x800) + example: "https://cdn.example.com/images/event-kakao.jpg" + + ChannelConfig: + type: object + required: + - type + properties: + type: + type: string + description: 채널 타입 + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + config: + type: object + description: 채널별 설정 (채널에 따라 다름) + additionalProperties: true + example: + scheduledTime: "2025-11-01T10:00:00Z" + caption: "이벤트 안내" + hashtags: + - "이벤트" + - "할인" + + DistributionResponse: + type: object + required: + - distributionId + - eventId + - status + - results + properties: + distributionId: + type: string + description: 배포 ID + example: "dist-12345" + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + status: + type: string + description: 전체 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - PARTIAL_FAILURE + - FAILED + example: "COMPLETED" + startedAt: + type: string + format: date-time + description: 배포 시작 시각 + example: "2025-11-01T08:59:00Z" + completedAt: + type: string + format: date-time + description: 배포 완료 시각 + example: "2025-11-01T09:00:00Z" + results: + type: array + description: 채널별 배포 결과 + items: + $ref: '#/components/schemas/ChannelResult' + + ChannelResult: + type: object + required: + - channel + - status + properties: + channel: + type: string + description: 채널 타입 + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + status: + type: string + description: 채널별 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - SUCCESS + - FAILED + example: "SUCCESS" + distributionId: + type: string + description: 채널별 배포 ID (우리동네TV, 지니TV) + example: "wtv-uuid-12345" + estimatedViews: + type: integer + description: 예상 노출 수 (우리동네TV, 지니TV) + example: 1500 + updateTimestamp: + type: string + format: date-time + description: 업데이트 완료 시각 (링고비즈) + example: "2025-11-01T09:00:00Z" + adId: + type: string + description: 광고 ID (지니TV) + example: "gtv-uuid-12345" + impressionSchedule: + type: array + description: 노출 스케줄 (지니TV) + items: + type: string + example: + - "2025-11-01 18:00-20:00" + - "2025-11-02 12:00-14:00" + postUrl: + type: string + description: 게시물 URL (Instagram, Naver Blog) + example: "https://instagram.com/p/generated-post-id" + postId: + type: string + description: 게시물 ID (Instagram) + example: "ig-post-12345" + messageId: + type: string + description: 메시지 ID (Kakao Channel) + example: "kakao-msg-12345" + message: + type: string + description: 결과 메시지 + example: "배포 완료" + errorMessage: + type: string + description: 오류 메시지 (실패 시) + example: "Instagram API 타임아웃" + retries: + type: integer + description: 재시도 횟수 + example: 0 + lastRetryAt: + type: string + format: date-time + description: 마지막 재시도 시각 + example: "2025-11-01T08:59:30Z" + + DistributionStatusResponse: + type: object + required: + - eventId + - overallStatus + - channels + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + overallStatus: + type: string + description: 전체 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - PARTIAL_FAILURE + - FAILED + - NOT_FOUND + example: "COMPLETED" + startedAt: + type: string + format: date-time + description: 배포 시작 시각 + example: "2025-11-01T08:59:00Z" + completedAt: + type: string + format: date-time + description: 배포 완료 시각 + example: "2025-11-01T09:00:00Z" + channels: + type: array + description: 채널별 배포 상태 + items: + $ref: '#/components/schemas/ChannelStatus' + + ChannelStatus: + type: object + required: + - channel + - status + properties: + channel: + type: string + description: 채널 타입 + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: "INSTAGRAM" + status: + type: string + description: 채널별 배포 상태 + enum: + - PENDING + - IN_PROGRESS + - COMPLETED + - FAILED + example: "COMPLETED" + progress: + type: integer + description: 진행률 (0-100, IN_PROGRESS 상태일 때) + minimum: 0 + maximum: 100 + example: 75 + distributionId: + type: string + description: 채널별 배포 ID + example: "wtv-uuid-12345" + estimatedViews: + type: integer + description: 예상 노출 수 + example: 1500 + updateTimestamp: + type: string + format: date-time + description: 업데이트 완료 시각 + example: "2025-11-01T09:00:00Z" + adId: + type: string + description: 광고 ID + example: "gtv-uuid-12345" + impressionSchedule: + type: array + description: 노출 스케줄 + items: + type: string + example: + - "2025-11-01 18:00-20:00" + postUrl: + type: string + description: 게시물 URL + example: "https://instagram.com/p/generated-post-id" + postId: + type: string + description: 게시물 ID + example: "ig-post-12345" + messageId: + type: string + description: 메시지 ID + example: "kakao-msg-12345" + completedAt: + type: string + format: date-time + description: 완료 시각 + example: "2025-11-01T09:00:00Z" + errorMessage: + type: string + description: 오류 메시지 + example: "Instagram API 타임아웃" + retries: + type: integer + description: 재시도 횟수 + example: 3 + lastRetryAt: + type: string + format: date-time + description: 마지막 재시도 시각 + example: "2025-11-01T08:59:30Z" + + RetryRequest: + type: object + required: + - channels + properties: + channels: + type: array + description: 재시도할 채널 목록 + minItems: 1 + items: + type: string + enum: + - WOORIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - INSTAGRAM + - NAVER_BLOG + - KAKAO_CHANNEL + example: + - "INSTAGRAM" + - "KAKAO_CHANNEL" + + RetryResponse: + type: object + required: + - eventId + - retryStatus + - results + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-12345" + retryStatus: + type: string + description: 재시도 전체 상태 + enum: + - COMPLETED + - PARTIAL_FAILURE + - FAILED + example: "COMPLETED" + retriedAt: + type: string + format: date-time + description: 재시도 시각 + example: "2025-11-01T09:05:00Z" + results: + type: array + description: 채널별 재시도 결과 + items: + $ref: '#/components/schemas/ChannelResult' + + ErrorResponse: + type: object + required: + - error + - message + - timestamp + properties: + error: + type: string + description: 오류 코드 + enum: + - BAD_REQUEST + - NOT_FOUND + - INTERNAL_SERVER_ERROR + example: "BAD_REQUEST" + message: + type: string + description: 오류 메시지 + example: "유효하지 않은 이벤트 ID입니다" + timestamp: + type: string + format: date-time + description: 오류 발생 시각 + example: "2025-11-01T09:00:00Z" + details: + type: object + description: 추가 오류 정보 (선택 사항) + additionalProperties: true diff --git a/design/backend/api/event-service-api.yaml b/design/backend/api/event-service-api.yaml new file mode 100644 index 0000000..1430972 --- /dev/null +++ b/design/backend/api/event-service-api.yaml @@ -0,0 +1,1058 @@ +openapi: 3.0.3 +info: + title: Event Service API + version: 1.0.0 + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스의 Event Service API 명세입니다. + + **주요 기능:** + - 이벤트 대시보드 조회 + - 이벤트 목적 선택 및 초안 생성 + - AI 이벤트 추천 요청 및 결과 조회 + - 이미지 생성 요청 및 결과 조회 + - 콘텐츠 선택 및 편집 + - 최종 승인 및 배포 + - 이벤트 상세 조회 및 목록 관리 + +servers: + - url: http://localhost:8080 + description: 로컬 개발 서버 + - url: https://api-dev.kt-event-marketing.com + description: 개발 서버 + - url: https://api.kt-event-marketing.com + description: 프로덕션 서버 + +tags: + - name: Dashboard + description: 대시보드 관리 + - name: EventDraft + description: 이벤트 초안 관리 + - name: AIRecommendation + description: AI 추천 관리 + - name: ContentGeneration + description: 콘텐츠 생성 관리 + - name: EventPublish + description: 이벤트 발행 관리 + - name: EventManagement + description: 이벤트 조회 및 관리 + - name: Job + description: 비동기 작업 관리 + +paths: + /api/events/dashboard: + get: + tags: + - Dashboard + summary: 대시보드 이벤트 목록 조회 + description: | + UFR-EVENT-010: 소상공인의 대시보드에서 진행중/예정/종료된 이벤트를 조회합니다. + + **비즈니스 로직:** + - 상태별로 최대 5개씩 표시 (최신순) + - Redis 캐시 우선 조회 (TTL 1분) + - 참여자 수, 조회수 등 기본 통계 포함 + operationId: getDashboard + security: + - bearerAuth: [] + responses: + '200': + description: 대시보드 데이터 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/purposes: + post: + tags: + - EventDraft + summary: 이벤트 목적 선택 및 초안 생성 + description: | + UFR-EVENT-020: 이벤트 목적을 선택하고 초안을 생성합니다. + + **비즈니스 로직:** + - 목적 유효성 검증 (신규 고객 유치, 재방문 유도, 매출 증대, 인지도 향상) + - EventDraft 엔티티 생성 및 DB 저장 + - Redis 캐시 저장 (TTL 30분) + - Kafka EventDraftCreated 이벤트 발행 + operationId: createEventDraft + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateEventDraftRequest' + responses: + '200': + description: 이벤트 초안 생성 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDraftResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{id}/ai-recommendations: + post: + tags: + - AIRecommendation + summary: AI 이벤트 추천 요청 + description: | + UFR-EVENT-030: AI 트렌드 분석 및 이벤트 추천을 요청합니다. + + **비동기 처리:** + - Kafka ai-job-topic에 Job 발행 + - Redis에 Job 상태 저장 (TTL 1시간) + - 202 Accepted 응답 (jobId 반환) + - 클라이언트는 폴링으로 결과 조회 + operationId: requestAIRecommendation + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + format: uuid + responses: + '202': + description: AI 추천 요청 접수 + content: + application/json: + schema: + $ref: '#/components/schemas/JobResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/jobs/{jobId}/status: + get: + tags: + - Job + summary: Job 상태 조회 (폴링) + description: | + AI 추천 또는 이미지 생성 Job의 상태를 조회합니다. + + **폴링 패턴:** + - AI 추천: 2초 간격, 최대 30초 (15회) + - 이미지 생성: 3초 간격, 최대 30초 (10회) + - 상태: PENDING, PROCESSING, COMPLETED, FAILED + operationId: getJobStatus + security: + - bearerAuth: [] + parameters: + - name: jobId + in: path + required: true + description: Job ID + schema: + type: string + format: uuid + responses: + '200': + description: Job 상태 조회 성공 + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/AIRecommendationJobResult' + - $ref: '#/components/schemas/ImageGenerationJobResult' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{id}/content-generation: + post: + tags: + - ContentGeneration + summary: 이미지 생성 요청 + description: | + UFR-CONT-010: SNS용 이미지 생성을 요청합니다. + + **비동기 처리:** + - Kafka image-job-topic에 Job 발행 + - Redis에 Job 상태 저장 (TTL 1시간) + - 202 Accepted 응답 (jobId 반환) + - Content Service가 3가지 스타일 이미지 생성 + operationId: requestImageGeneration + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + format: uuid + responses: + '202': + description: 이미지 생성 요청 접수 + content: + application/json: + schema: + $ref: '#/components/schemas/JobResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/drafts/{id}/content: + put: + tags: + - ContentGeneration + summary: 선택한 콘텐츠 저장 + description: | + UFR-CONT-020: 선택한 이미지와 편집한 콘텐츠를 저장합니다. + + **비즈니스 로직:** + - 선택한 이미지 URL 검증 + - 편집 내용 적용 (텍스트, 색상) + - EventDraft 업데이트 + - Redis 캐시 무효화 + operationId: updateEventContent + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateContentRequest' + responses: + '200': + description: 콘텐츠 저장 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventContentResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events/{id}/publish: + post: + tags: + - EventPublish + summary: 이벤트 최종 승인 및 배포 + description: | + UFR-EVENT-050: 이벤트를 최종 승인하고 배포를 시작합니다. + + **비즈니스 로직:** + - 발행 준비 검증 (목적, AI 추천, 콘텐츠, 채널 선택) + - 상태 변경: DRAFT → APPROVED → ACTIVE + - Kafka EventCreated 이벤트 발행 + - Distribution Service 동기 호출 (Timeout 70초, Circuit Breaker 적용) + - Redis 캐시 무효화 + operationId: publishEvent + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: 이벤트 초안 ID + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PublishEventRequest' + responses: + '200': + description: 이벤트 발행 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/PublishEventResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + description: Distribution Service 호출 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/events/{id}: + get: + tags: + - EventManagement + summary: 이벤트 상세 조회 + description: | + UFR-EVENT-060: 이벤트의 상세 정보를 조회합니다. + + **비즈니스 로직:** + - Redis 캐시 우선 조회 (TTL 5분) + - 경품 정보 및 배포 이력 JOIN 조회 + - 사용자 권한 검증 + operationId: getEventDetail + security: + - bearerAuth: [] + parameters: + - name: id + in: path + required: true + description: 이벤트 ID + schema: + type: string + format: uuid + responses: + '200': + description: 이벤트 상세 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventDetailResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/events: + get: + tags: + - EventManagement + summary: 이벤트 목록 조회 (필터/검색) + description: | + UFR-EVENT-070: 이벤트 목록을 조회하고 필터링/검색합니다. + + **비즈니스 로직:** + - Redis 캐시 우선 조회 (TTL 1분) + - 상태별 필터링 + - 키워드 검색 (제목, 설명) + - 페이지네이션 (기본 20개/페이지) + - 정렬 (최신순, 참여자순, ROI순) + operationId: getEventList + security: + - bearerAuth: [] + parameters: + - name: status + in: query + description: 이벤트 상태 필터 + schema: + type: string + enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] + - name: keyword + in: query + description: 검색 키워드 (제목, 설명) + schema: + type: string + - name: page + in: query + description: 페이지 번호 (0부터 시작) + schema: + type: integer + minimum: 0 + default: 0 + - name: size + in: query + description: 페이지 크기 + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - name: sort + in: query + description: 정렬 기준 + schema: + type: string + enum: [createdAt,desc, participantCount,desc, roi,desc] + default: createdAt,desc + responses: + '200': + description: 이벤트 목록 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/EventListResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: JWT 토큰을 사용한 인증 (User Service에서 발급) + + schemas: + # Dashboard Schemas + DashboardResponse: + type: object + required: + - active + - approved + - completed + properties: + active: + type: array + description: 진행중 이벤트 목록 (최대 5개) + maxItems: 5 + items: + $ref: '#/components/schemas/EventSummary' + approved: + type: array + description: 배포 대기중 이벤트 목록 (최대 5개) + maxItems: 5 + items: + $ref: '#/components/schemas/EventSummary' + completed: + type: array + description: 종료된 이벤트 목록 (최대 5개) + maxItems: 5 + items: + $ref: '#/components/schemas/EventSummary' + + EventSummary: + type: object + required: + - eventId + - title + - status + - createdAt + properties: + eventId: + type: string + format: uuid + description: 이벤트 ID + title: + type: string + description: 이벤트 제목 + maxLength: 100 + period: + $ref: '#/components/schemas/EventPeriod' + status: + type: string + enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] + description: 이벤트 상태 + participantCount: + type: integer + description: 참여자 수 + default: 0 + viewCount: + type: integer + description: 조회수 + default: 0 + createdAt: + type: string + format: date-time + description: 생성일시 + + EventPeriod: + type: object + properties: + startDate: + type: string + format: date + description: 시작일 + endDate: + type: string + format: date + description: 종료일 + + # Event Draft Schemas + CreateEventDraftRequest: + type: object + required: + - objective + - storeInfo + properties: + objective: + type: string + enum: [NEW_CUSTOMER, REVISIT, SALES_INCREASE, BRAND_AWARENESS] + description: | + 이벤트 목적 + - NEW_CUSTOMER: 신규 고객 유치 + - REVISIT: 재방문 유도 + - SALES_INCREASE: 매출 증대 + - BRAND_AWARENESS: 인지도 향상 + storeInfo: + $ref: '#/components/schemas/StoreInfo' + + StoreInfo: + type: object + required: + - storeName + - industry + - region + properties: + storeName: + type: string + description: 매장명 + maxLength: 50 + industry: + type: string + description: 업종 + maxLength: 30 + region: + type: string + description: 지역 + maxLength: 50 + address: + type: string + description: 주소 + maxLength: 200 + + EventDraftResponse: + type: object + required: + - eventDraftId + - objective + - status + properties: + eventDraftId: + type: string + format: uuid + description: 이벤트 초안 ID + objective: + type: string + enum: [NEW_CUSTOMER, REVISIT, SALES_INCREASE, BRAND_AWARENESS] + description: 이벤트 목적 + status: + type: string + enum: [DRAFT] + description: 초안 상태 + createdAt: + type: string + format: date-time + description: 생성일시 + + # Job Schemas + JobResponse: + type: object + required: + - jobId + - status + properties: + jobId: + type: string + format: uuid + description: Job ID + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + description: Job 상태 + + AIRecommendationJobResult: + type: object + required: + - jobId + - status + properties: + jobId: + type: string + format: uuid + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + recommendations: + type: array + description: AI 추천 결과 (3가지) + minItems: 3 + maxItems: 3 + items: + $ref: '#/components/schemas/EventRecommendation' + error: + type: string + description: 에러 메시지 (FAILED 상태일 때만) + + EventRecommendation: + type: object + required: + - title + - prize + - participationMethod + - estimatedCost + - estimatedParticipants + - estimatedROI + properties: + title: + type: string + description: 이벤트 제목 (수정 가능) + maxLength: 50 + prize: + type: string + description: 경품 (수정 가능) + maxLength: 100 + participationMethod: + type: string + description: 참여 방법 + maxLength: 200 + estimatedCost: + type: integer + description: 예상 비용 (원) + minimum: 0 + estimatedParticipants: + type: integer + description: 예상 참여자 수 + minimum: 0 + estimatedROI: + type: number + format: double + description: 예상 투자 대비 수익률 (%) + + ImageGenerationJobResult: + type: object + required: + - jobId + - status + properties: + jobId: + type: string + format: uuid + status: + type: string + enum: [PENDING, PROCESSING, COMPLETED, FAILED] + progress: + type: integer + description: 진행률 (%) - PROCESSING 상태일 때 + minimum: 0 + maximum: 100 + imageUrls: + type: object + description: 생성된 이미지 URL (3가지 스타일) - COMPLETED 상태일 때 + properties: + simple: + type: string + format: uri + description: 심플 스타일 이미지 URL + fancy: + type: string + format: uri + description: 화려한 스타일 이미지 URL + trendy: + type: string + format: uri + description: 트렌디 스타일 이미지 URL + error: + type: string + description: 에러 메시지 (FAILED 상태일 때만) + + # Content Schemas + UpdateContentRequest: + type: object + required: + - selectedImageUrl + - editedContent + properties: + selectedImageUrl: + type: string + format: uri + description: 선택한 이미지 URL (simple, fancy, trendy 중 하나) + editedContent: + $ref: '#/components/schemas/EditedContent' + + EditedContent: + type: object + properties: + title: + type: string + description: 편집된 제목 + maxLength: 100 + prizeText: + type: string + description: 편집된 경품 정보 텍스트 + maxLength: 200 + participationText: + type: string + description: 편집된 참여 안내 텍스트 + maxLength: 300 + backgroundColor: + type: string + pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' + description: 배경색 (Hex 코드) + textColor: + type: string + pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' + description: 텍스트 색상 (Hex 코드) + accentColor: + type: string + pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' + description: 강조 색상 (Hex 코드) + + EventContentResponse: + type: object + required: + - eventDraftId + - selectedImageUrl + - editedContent + properties: + eventDraftId: + type: string + format: uuid + selectedImageUrl: + type: string + format: uri + editedContent: + $ref: '#/components/schemas/EditedContent' + + # Publish Schemas + PublishEventRequest: + type: object + required: + - selectedChannels + properties: + selectedChannels: + type: array + description: 배포 채널 목록 (최소 1개) + minItems: 1 + items: + $ref: '#/components/schemas/DistributionChannel' + + DistributionChannel: + type: object + required: + - channelType + properties: + channelType: + type: string + enum: [URINEIGHBOR_TV, RINGO_BIZ, GENIE_TV, INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + description: | + 배포 채널 타입 + - URINEIGHBOR_TV: 우리동네TV + - RINGO_BIZ: 링고비즈 + - GENIE_TV: 지니TV + - INSTAGRAM: Instagram + - NAVER_BLOG: Naver Blog + - KAKAO_CHANNEL: Kakao Channel + settings: + type: object + description: 채널별 설정 (채널마다 다름) + additionalProperties: true + + PublishEventResponse: + type: object + required: + - eventId + - status + - distributionResults + properties: + eventId: + type: string + format: uuid + description: 발행된 이벤트 ID + status: + type: string + enum: [ACTIVE] + description: 이벤트 상태 + distributionResults: + type: array + description: 채널별 배포 결과 + items: + $ref: '#/components/schemas/ChannelDistributionResult' + + ChannelDistributionResult: + type: object + required: + - channelType + - status + properties: + channelType: + type: string + enum: [URINEIGHBOR_TV, RINGO_BIZ, GENIE_TV, INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + status: + type: string + enum: [SUCCESS, FAILED] + description: 배포 상태 + distributionId: + type: string + description: 배포 ID (채널에서 발급) + estimatedReach: + type: integer + description: 예상 노출 수 + error: + type: string + description: 에러 메시지 (FAILED 상태일 때) + + # Event Management Schemas + EventDetailResponse: + type: object + required: + - event + - prizes + - distributionStatus + properties: + event: + $ref: '#/components/schemas/EventDetail' + prizes: + type: array + description: 경품 목록 + items: + $ref: '#/components/schemas/Prize' + distributionStatus: + $ref: '#/components/schemas/DistributionStatus' + + EventDetail: + type: object + required: + - eventId + - title + - objective + - status + - createdAt + properties: + eventId: + type: string + format: uuid + title: + type: string + maxLength: 100 + objective: + type: string + enum: [NEW_CUSTOMER, REVISIT, SALES_INCREASE, BRAND_AWARENESS] + period: + $ref: '#/components/schemas/EventPeriod' + status: + type: string + enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] + participationMethod: + type: string + description: 참여 방법 + maxLength: 500 + selectedImageUrl: + type: string + format: uri + description: 선택한 이미지 URL + editedContent: + $ref: '#/components/schemas/EditedContent' + createdAt: + type: string + format: date-time + publishedAt: + type: string + format: date-time + description: 발행일시 + + Prize: + type: object + required: + - prizeId + - prizeName + - quantity + properties: + prizeId: + type: string + format: uuid + prizeName: + type: string + description: 경품명 + maxLength: 100 + quantity: + type: integer + description: 수량 + minimum: 1 + + DistributionStatus: + type: object + properties: + channels: + type: array + description: 배포된 채널 목록 + items: + $ref: '#/components/schemas/ChannelStatus' + + ChannelStatus: + type: object + required: + - channelType + - status + properties: + channelType: + type: string + enum: [URINEIGHBOR_TV, RINGO_BIZ, GENIE_TV, INSTAGRAM, NAVER_BLOG, KAKAO_CHANNEL] + status: + type: string + enum: [PENDING, DISTRIBUTING, ACTIVE, FAILED] + distributionId: + type: string + estimatedReach: + type: integer + actualReach: + type: integer + description: 실제 노출 수 + + EventListResponse: + type: object + required: + - events + - totalCount + - totalPages + - currentPage + properties: + events: + type: array + items: + $ref: '#/components/schemas/EventListItem' + totalCount: + type: integer + description: 전체 이벤트 수 + totalPages: + type: integer + description: 전체 페이지 수 + currentPage: + type: integer + description: 현재 페이지 (0부터 시작) + + EventListItem: + type: object + required: + - eventId + - title + - status + - createdAt + properties: + eventId: + type: string + format: uuid + title: + type: string + maxLength: 100 + period: + $ref: '#/components/schemas/EventPeriod' + status: + type: string + enum: [DRAFT, APPROVED, ACTIVE, COMPLETED] + participantCount: + type: integer + description: 참여자 수 + default: 0 + roi: + type: number + format: double + description: 투자 대비 수익률 (%) + createdAt: + type: string + format: date-time + + # Error Schemas + ErrorResponse: + type: object + required: + - error + - message + - timestamp + properties: + error: + type: string + description: 에러 코드 + message: + type: string + description: 에러 메시지 + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + details: + type: string + description: 상세 에러 정보 + + responses: + BadRequest: + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: BAD_REQUEST + message: 요청 파라미터가 유효하지 않습니다 + timestamp: '2025-10-22T10:00:00Z' + + Unauthorized: + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: UNAUTHORIZED + message: 인증 토큰이 유효하지 않습니다 + timestamp: '2025-10-22T10:00:00Z' + + Forbidden: + description: 권한 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: FORBIDDEN + message: 해당 리소스에 접근 권한이 없습니다 + timestamp: '2025-10-22T10:00:00Z' + + NotFound: + description: 리소스를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: NOT_FOUND + message: 요청한 리소스를 찾을 수 없습니다 + timestamp: '2025-10-22T10:00:00Z' + + InternalServerError: + description: 서버 내부 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + example: + error: INTERNAL_SERVER_ERROR + message: 서버 내부 오류가 발생했습니다 + timestamp: '2025-10-22T10:00:00Z' diff --git a/design/backend/api/participation-service-api.yaml b/design/backend/api/participation-service-api.yaml new file mode 100644 index 0000000..6bf25d3 --- /dev/null +++ b/design/backend/api/participation-service-api.yaml @@ -0,0 +1,658 @@ +openapi: 3.0.3 +info: + title: Participation Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - Participation Service + + ## 주요 기능 + - 이벤트 참여 접수 (비회원 가능) + - 참여자 목록 조회 (사장님 전용) + - 당첨자 추첨 (사장님 전용) + + ## 인증 정보 + - 이벤트 참여: 인증 불필요 (비회원 참여 가능) + - 참여자 목록 조회 및 당첨자 추첨: JWT 토큰 필수 (사장님 권한) + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event.com + +servers: + - url: https://api.kt-event.com/participation + description: Production Server + - url: https://dev-api.kt-event.com/participation + description: Development Server + - url: http://localhost:8083 + description: Local Server + +tags: + - name: Participation + description: 이벤트 참여 관리 + - name: Participants + description: 참여자 목록 관리 + - name: Draw + description: 당첨자 추첨 + +paths: + /api/v1/participations: + post: + tags: + - Participation + summary: 이벤트 참여 + description: | + 고객이 이벤트에 참여합니다. (UFR-PART-010) + + **특징:** + - 비회원 참여 가능 (인증 불필요) + - 전화번호 기반 중복 체크 (1인 1회) + - Redis 캐싱으로 중복 체크 성능 최적화 + - 응모 번호 자동 발급 + + **처리 흐름:** + 1. 요청 데이터 유효성 검증 + 2. Redis 캐시에서 중복 체크 (빠른 응답) + 3. 캐시 MISS 시 DB 조회 + 4. 신규 참여: 응모번호 발급 및 저장 + 5. 중복 방지 캐시 저장 (TTL: 7일) + 6. Kafka 이벤트 발행 (Analytics 연동) + operationId: registerParticipation + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipationRegisterRequest' + examples: + 신규참여: + value: + eventId: "evt-12345-abcde" + name: "홍길동" + phoneNumber: "010-1234-5678" + entryPath: "SNS" + consentMarketing: true + 매장방문참여: + value: + eventId: "evt-12345-abcde" + name: "김철수" + phoneNumber: "010-9876-5432" + entryPath: "STORE_VISIT" + consentMarketing: false + responses: + '201': + description: 참여 접수 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipationRegisterResponse' + examples: + 성공: + value: + applicationNumber: "EVT-20251022-A1B2C3" + drawDate: "2025-11-05" + message: "이벤트 참여가 완료되었습니다. 당첨자 발표일은 2025년 11월 5일입니다." + '400': + description: 잘못된 요청 (유효성 검증 실패) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 이름오류: + value: + error: "VALIDATION_ERROR" + message: "이름은 2자 이상이어야 합니다." + timestamp: "2025-10-22T10:30:00Z" + 전화번호오류: + value: + error: "VALIDATION_ERROR" + message: "올바른 전화번호 형식이 아닙니다." + timestamp: "2025-10-22T10:30:00Z" + 동의누락: + value: + error: "VALIDATION_ERROR" + message: "개인정보 수집 및 이용에 대한 동의가 필요합니다." + timestamp: "2025-10-22T10:30:00Z" + '409': + description: 중복 참여 (이미 참여한 이벤트) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 중복참여: + value: + error: "DUPLICATE_PARTICIPATION" + message: "이미 참여하신 이벤트입니다." + timestamp: "2025-10-22T10:30:00Z" + + /api/v1/events/{eventId}/participants: + get: + tags: + - Participants + summary: 참여자 목록 조회 + description: | + 이벤트의 참여자 목록을 조회합니다. (UFR-PART-020) + + **특징:** + - 사장님 전용 기능 (JWT 인증 필수) + - Redis 캐싱 (TTL: 10분) - 실시간 정확도와 성능 균형 + - 동적 필터링 (참여 경로, 당첨 여부) + - 검색 기능 (이름, 전화번호) + - 페이지네이션 지원 + - 전화번호 마스킹 (010-****-1234) + + **성능 최적화:** + - 복합 인덱스: idx_participants_event_filters + - Redis 캐싱으로 반복 조회 성능 개선 + operationId: getParticipantList + security: + - bearerAuth: [] + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt-12345-abcde" + - name: entryPath + in: query + required: false + description: 참여 경로 필터 + schema: + type: string + enum: + - SNS + - URIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - STORE_VISIT + example: "SNS" + - name: isWinner + in: query + required: false + description: 당첨 여부 필터 + schema: + type: boolean + example: false + - name: name + in: query + required: false + description: 이름 검색 (부분 일치) + schema: + type: string + example: "홍길동" + - name: phone + in: query + required: false + description: 전화번호 검색 (부분 일치) + schema: + type: string + example: "010-1234" + - name: page + in: query + required: false + description: 페이지 번호 (0부터 시작) + schema: + type: integer + minimum: 0 + default: 0 + example: 0 + - name: size + in: query + required: false + description: 페이지당 항목 수 + schema: + type: integer + minimum: 10 + maximum: 100 + default: 20 + example: 20 + responses: + '200': + description: 참여자 목록 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ParticipantListResponse' + examples: + 전체목록: + value: + participants: + - participantId: "part-001" + applicationNumber: "EVT-20251022-A1B2C3" + name: "홍길동" + phoneNumber: "010-****-5678" + entryPath: "SNS" + participatedAt: "2025-10-22T10:30:00Z" + isWinner: false + - participantId: "part-002" + applicationNumber: "EVT-20251022-D4E5F6" + name: "김철수" + phoneNumber: "010-****-5432" + entryPath: "STORE_VISIT" + participatedAt: "2025-10-22T11:15:00Z" + isWinner: false + pagination: + currentPage: 0 + totalPages: 5 + totalElements: 100 + size: 20 + 당첨자필터: + value: + participants: + - participantId: "part-050" + applicationNumber: "EVT-20251022-Z9Y8X7" + name: "박영희" + phoneNumber: "010-****-1111" + entryPath: "SNS" + participatedAt: "2025-10-23T14:20:00Z" + isWinner: true + wonAt: "2025-10-25T09:00:00Z" + pagination: + currentPage: 0 + totalPages: 1 + totalElements: 5 + size: 20 + '400': + description: 잘못된 요청 (유효성 검증 실패) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 페이지오류: + value: + error: "VALIDATION_ERROR" + message: "페이지 번호는 0 이상이어야 합니다." + timestamp: "2025-10-22T10:30:00Z" + 크기오류: + value: + error: "VALIDATION_ERROR" + message: "페이지 크기는 10~100 사이여야 합니다." + timestamp: "2025-10-22T10:30:00Z" + '401': + $ref: '#/components/responses/UnauthorizedError' + + /api/v1/events/{eventId}/draw-winners: + post: + tags: + - Draw + summary: 당첨자 추첨 + description: | + 이벤트의 당첨자를 추첨합니다. (UFR-PART-030) + + **특징:** + - 사장님 전용 기능 (JWT 인증 필수) + - Fisher-Yates Shuffle 알고리즘 (공정성 보장) + - 난수 기반 무작위 추첨 (Crypto.randomBytes) + - 매장 방문 고객 가산점 옵션 (가중치 2배) + - 추첨 과정 로그 자동 기록 (감사 추적) + - 재추첨 가능 (이전 로그 보관) + + **알고리즘 특징:** + - 시간 복잡도: O(n log n) + - 공간 복잡도: O(n) + - 예측 불가능한 난수 시드 (암호학적 안전성) + + **트랜잭션 처리:** + - 당첨자 업데이트 + 추첨 로그 저장 (원자성 보장) + - 실패 시 자동 롤백 + operationId: drawWinners + security: + - bearerAuth: [] + parameters: + - name: eventId + in: path + required: true + description: 이벤트 ID + schema: + type: string + example: "evt-12345-abcde" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersRequest' + examples: + 기본추첨: + value: + winnerCount: 5 + visitBonus: false + 가산점추첨: + value: + winnerCount: 10 + visitBonus: true + responses: + '200': + description: 당첨자 추첨 완료 + content: + application/json: + schema: + $ref: '#/components/schemas/DrawWinnersResponse' + examples: + 성공: + value: + drawLogId: "draw-log-001" + winners: + - participantId: "part-050" + applicationNumber: "EVT-20251022-Z9Y8X7" + name: "박영희" + phoneNumber: "010-****-1111" + entryPath: "SNS" + - participantId: "part-023" + applicationNumber: "EVT-20251022-K3L4M5" + name: "이순신" + phoneNumber: "010-****-2222" + entryPath: "STORE_VISIT" + - participantId: "part-087" + applicationNumber: "EVT-20251022-N6O7P8" + name: "김유신" + phoneNumber: "010-****-3333" + entryPath: "GENIE_TV" + drawMethod: "RANDOM" + algorithm: "FISHER_YATES_SHUFFLE" + visitBonusApplied: false + drawnAt: "2025-10-25T09:00:00Z" + message: "당첨자 추첨이 완료되었습니다." + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 당첨자수오류: + value: + error: "VALIDATION_ERROR" + message: "당첨자 수는 1명 이상이어야 합니다." + timestamp: "2025-10-25T09:00:00Z" + 참여자부족: + value: + error: "INSUFFICIENT_PARTICIPANTS" + message: "참여자 수가 부족합니다. (요청: 10명, 참여자: 5명)" + timestamp: "2025-10-25T09:00:00Z" + '401': + $ref: '#/components/responses/UnauthorizedError' + '409': + description: 이미 추첨 완료된 이벤트 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 추첨완료: + value: + error: "ALREADY_DRAWN" + message: "이미 추첨이 완료된 이벤트입니다. 재추첨을 원하시면 기존 추첨을 취소해주세요." + timestamp: "2025-10-25T09:00:00Z" + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT 토큰을 사용한 인증 + + **헤더 형식:** + ``` + Authorization: Bearer {token} + ``` + + schemas: + ParticipationRegisterRequest: + type: object + required: + - eventId + - name + - phoneNumber + - entryPath + properties: + eventId: + type: string + description: 이벤트 ID + example: "evt-12345-abcde" + name: + type: string + minLength: 2 + description: 참여자 이름 (2자 이상) + example: "홍길동" + phoneNumber: + type: string + pattern: '^\d{3}-\d{4}-\d{4}$' + description: 전화번호 (하이픈 포함, 010-1234-5678 형식) + example: "010-1234-5678" + entryPath: + type: string + enum: + - SNS + - URIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - STORE_VISIT + description: | + 참여 경로 + - SNS: Instagram, Naver Blog, Kakao Channel + - URIDONGNE_TV: 우리동네TV + - RINGO_BIZ: 링고비즈 연결음 + - GENIE_TV: 지니TV 광고 + - STORE_VISIT: 매장 방문 + example: "SNS" + consentMarketing: + type: boolean + description: 마케팅 활용 동의 (선택) + default: false + example: true + + ParticipationRegisterResponse: + type: object + properties: + applicationNumber: + type: string + description: 응모 번호 (형식 EVT-{timestamp}-{random}) + example: "EVT-20251022-A1B2C3" + drawDate: + type: string + format: date + description: 당첨자 발표일 (이벤트 종료일 + 3일) + example: "2025-11-05" + message: + type: string + description: 참여 완료 메시지 + example: "이벤트 참여가 완료되었습니다. 당첨자 발표일은 2025년 11월 5일입니다." + + ParticipantListResponse: + type: object + properties: + participants: + type: array + items: + $ref: '#/components/schemas/ParticipantInfo' + pagination: + $ref: '#/components/schemas/PaginationInfo' + + ParticipantInfo: + type: object + properties: + participantId: + type: string + description: 참여자 ID + example: "part-001" + applicationNumber: + type: string + description: 응모 번호 + example: "EVT-20251022-A1B2C3" + name: + type: string + description: 참여자 이름 + example: "홍길동" + phoneNumber: + type: string + description: 전화번호 (마스킹됨, 010-****-5678) + example: "010-****-5678" + entryPath: + type: string + enum: + - SNS + - URIDONGNE_TV + - RINGO_BIZ + - GENIE_TV + - STORE_VISIT + description: 참여 경로 + example: "SNS" + participatedAt: + type: string + format: date-time + description: 참여 일시 + example: "2025-10-22T10:30:00Z" + isWinner: + type: boolean + description: 당첨 여부 + example: false + wonAt: + type: string + format: date-time + description: 당첨 일시 (당첨자인 경우만) + example: "2025-10-25T09:00:00Z" + nullable: true + + PaginationInfo: + type: object + properties: + currentPage: + type: integer + description: 현재 페이지 번호 (0부터 시작) + example: 0 + totalPages: + type: integer + description: 전체 페이지 수 + example: 5 + totalElements: + type: integer + description: 전체 항목 수 + example: 100 + size: + type: integer + description: 페이지당 항목 수 + example: 20 + + DrawWinnersRequest: + type: object + required: + - winnerCount + properties: + winnerCount: + type: integer + minimum: 1 + description: 당첨자 수 (경품 수량 기반) + example: 5 + visitBonus: + type: boolean + description: | + 매장 방문 고객 가산점 적용 여부 + - true: 매장 방문 고객 가중치 2배 + - false: 모든 참여자 동일 가중치 + default: false + example: false + + DrawWinnersResponse: + type: object + properties: + drawLogId: + type: string + description: 추첨 로그 ID (감사 추적용) + example: "draw-log-001" + winners: + type: array + description: 당첨자 목록 + items: + $ref: '#/components/schemas/WinnerInfo' + drawMethod: + type: string + description: 추첨 방식 + example: "RANDOM" + algorithm: + type: string + description: 추첨 알고리즘 + example: "FISHER_YATES_SHUFFLE" + visitBonusApplied: + type: boolean + description: 매장 방문 가산점 적용 여부 + example: false + drawnAt: + type: string + format: date-time + description: 추첨 일시 + example: "2025-10-25T09:00:00Z" + message: + type: string + description: 추첨 완료 메시지 + example: "당첨자 추첨이 완료되었습니다." + + WinnerInfo: + type: object + properties: + participantId: + type: string + description: 참여자 ID + example: "part-050" + applicationNumber: + type: string + description: 응모 번호 + example: "EVT-20251022-Z9Y8X7" + name: + type: string + description: 당첨자 이름 + example: "박영희" + phoneNumber: + type: string + description: 전화번호 (마스킹됨) + example: "010-****-1111" + entryPath: + type: string + description: 참여 경로 + example: "SNS" + + ErrorResponse: + type: object + properties: + error: + type: string + description: 오류 코드 + example: "VALIDATION_ERROR" + message: + type: string + description: 오류 메시지 + example: "요청 데이터가 올바르지 않습니다." + timestamp: + type: string + format: date-time + description: 오류 발생 시각 + example: "2025-10-22T10:30:00Z" + + responses: + UnauthorizedError: + description: 인증 실패 (JWT 토큰 없음 또는 유효하지 않음) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 토큰없음: + value: + error: "UNAUTHORIZED" + message: "인증 토큰이 필요합니다." + timestamp: "2025-10-22T10:30:00Z" + 토큰만료: + value: + error: "UNAUTHORIZED" + message: "토큰이 만료되었습니다. 다시 로그인해주세요." + timestamp: "2025-10-22T10:30:00Z" + 권한없음: + value: + error: "FORBIDDEN" + message: "이 작업을 수행할 권한이 없습니다." + timestamp: "2025-10-22T10:30:00Z" diff --git a/design/backend/api/user-service-api.yaml b/design/backend/api/user-service-api.yaml new file mode 100644 index 0000000..b6f036a --- /dev/null +++ b/design/backend/api/user-service-api.yaml @@ -0,0 +1,875 @@ +openapi: 3.0.3 +info: + title: User Service API + description: | + KT AI 기반 소상공인 이벤트 자동 생성 서비스 - User Service API + + 사용자 인증 및 매장정보 관리를 담당하는 마이크로서비스 + + **주요 기능:** + - 회원가입 (사업자번호 검증 포함) + - 로그인/로그아웃 + - 프로필 조회 및 수정 + - 비밀번호 변경 + + **보안:** + - JWT Bearer 토큰 기반 인증 + - bcrypt 비밀번호 해싱 + - AES-256-GCM 사업자번호 암호화 + version: 1.0.0 + contact: + name: Digital Garage Team + email: support@kt-event-marketing.com + +servers: + - url: https://api.kt-event-marketing.com/user/v1 + description: Production Server + - url: https://dev-api.kt-event-marketing.com/user/v1 + description: Development Server + - url: https://virtserver.swaggerhub.com/kt-event-marketing/user-service/1.0.0 + description: SwaggerHub Mock Server + - url: http://localhost:8081 + description: Local Development Server + +tags: + - name: Authentication + description: 인증 관련 API (로그인, 로그아웃, 회원가입) + - name: Profile + description: 프로필 관련 API (조회, 수정, 비밀번호 변경) + +paths: + /api/users/register: + post: + tags: + - Authentication + summary: 회원가입 + description: | + 소상공인 회원가입 API + + **유저스토리:** UFR-USER-010 + + **주요 기능:** + - 기본 정보 및 매장 정보 등록 + - 사업자번호 검증 (국세청 API 연동) + - 비밀번호 bcrypt 해싱 + - JWT 토큰 자동 발급 + + **처리 흐름:** + 1. 중복 사용자 확인 (전화번호 기반) + 2. 사업자번호 검증 (국세청 API, Circuit Breaker 패턴) + 3. 비밀번호 해싱 (bcrypt) + 4. 사업자번호 암호화 (AES-256-GCM) + 5. User/Store 데이터베이스 트랜잭션 처리 + 6. JWT 토큰 생성 및 세션 저장 (Redis) + operationId: registerUser + x-user-story: UFR-USER-010 + x-controller: UserController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + examples: + restaurant: + summary: 음식점 회원가입 예시 + value: + name: 홍길동 + phoneNumber: "01012345678" + email: hong@example.com + password: "Password123!" + storeName: 맛있는집 + industry: 음식점 + address: 서울시 강남구 테헤란로 123 + businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00" + businessNumber: "1234567890" + cafe: + summary: 카페 회원가입 예시 + value: + name: 김철수 + phoneNumber: "01087654321" + email: kim@example.com + password: "SecurePass456!" + storeName: 아메리카노 카페 + industry: 카페 + address: 서울시 서초구 서초대로 456 + businessHours: "매일 09:00-20:00" + businessNumber: "9876543210" + responses: + '201': + description: 회원가입 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterResponse' + examples: + success: + summary: 회원가입 성공 응답 + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: 123 + userName: 홍길동 + storeId: 456 + storeName: 맛있는집 + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + duplicateUser: + summary: 중복 사용자 + value: + code: USER_001 + error: 이미 가입된 전화번호입니다 + timestamp: 2025-10-22T10:30:00Z + invalidBusinessNumber: + summary: 사업자번호 검증 실패 + value: + code: USER_002 + error: 유효하지 않은 사업자번호입니다. 휴폐업 여부를 확인해주세요. + timestamp: 2025-10-22T10:30:00Z + validationError: + summary: 입력 검증 오류 + value: + code: VALIDATION_ERROR + error: 비밀번호는 8자 이상이어야 합니다 + timestamp: 2025-10-22T10:30:00Z + '500': + description: 서버 오류 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/users/login: + post: + tags: + - Authentication + summary: 로그인 + description: | + 소상공인 로그인 API + + **유저스토리:** UFR-USER-020 + + **주요 기능:** + - 전화번호/비밀번호 인증 + - JWT 토큰 발급 + - Redis 세션 저장 + - 최종 로그인 시각 업데이트 (비동기) + + **보안:** + - Timing Attack 방어 (에러 메시지 통일) + - bcrypt 비밀번호 검증 + - JWT 토큰 7일 만료 + operationId: loginUser + x-user-story: UFR-USER-020 + x-controller: UserController + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + examples: + default: + summary: 로그인 요청 예시 + value: + phoneNumber: "01012345678" + password: "Password123!" + responses: + '200': + description: 로그인 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + examples: + success: + summary: 로그인 성공 응답 + value: + token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: 123 + userName: 홍길동 + role: OWNER + email: hong@example.com + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + authFailed: + summary: 인증 실패 + value: + code: AUTH_001 + error: 전화번호 또는 비밀번호를 확인해주세요 + timestamp: 2025-10-22T10:30:00Z + + /api/users/logout: + post: + tags: + - Authentication + summary: 로그아웃 + description: | + 로그아웃 API + + **유저스토리:** UFR-USER-040 + + **주요 기능:** + - Redis 세션 삭제 + - JWT 토큰 Blacklist 추가 + - 멱등성 보장 + + **처리 흐름:** + 1. JWT 토큰 검증 + 2. Redis 세션 삭제 + 3. JWT Blacklist 추가 (남은 만료 시간만큼 TTL 설정) + 4. 로그아웃 이벤트 발행 + operationId: logoutUser + x-user-story: UFR-USER-040 + x-controller: UserController + security: + - BearerAuth: [] + responses: + '200': + description: 로그아웃 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/LogoutResponse' + examples: + success: + summary: 로그아웃 성공 응답 + value: + success: true + message: 안전하게 로그아웃되었습니다 + '401': + description: 인증 실패 (유효하지 않은 토큰) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidToken: + summary: 유효하지 않은 토큰 + value: + code: AUTH_002 + error: 유효하지 않은 토큰입니다 + timestamp: 2025-10-22T10:30:00Z + + /api/users/profile: + get: + tags: + - Profile + summary: 프로필 조회 + description: | + 사용자 프로필 조회 API + + **유저스토리:** UFR-USER-030 + + **조회 정보:** + - 기본 정보 (이름, 전화번호, 이메일) + - 매장 정보 (매장명, 업종, 주소, 영업시간) + operationId: getProfile + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + responses: + '200': + description: 프로필 조회 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ProfileResponse' + examples: + success: + summary: 프로필 조회 성공 응답 + value: + userId: 123 + userName: 홍길동 + phoneNumber: "01012345678" + email: hong@example.com + role: OWNER + storeId: 456 + storeName: 맛있는집 + industry: 음식점 + address: 서울시 강남구 테헤란로 123 + businessHours: "월-금 11:00-22:00, 토-일 12:00-21:00" + createdAt: 2025-09-01T10:00:00Z + lastLoginAt: 2025-10-22T09:00:00Z + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 사용자를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + notFound: + summary: 사용자 없음 + value: + code: USER_003 + error: 사용자를 찾을 수 없습니다 + timestamp: 2025-10-22T10:30:00Z + + put: + tags: + - Profile + summary: 프로필 수정 + description: | + 사용자 프로필 수정 API + + **유저스토리:** UFR-USER-030 + + **수정 가능 항목:** + - 기본 정보: 이름, 전화번호, 이메일 + - 매장 정보: 매장명, 업종, 주소, 영업시간 + + **주의사항:** + - 비밀번호 변경은 별도 API 사용 (/api/users/password) + - 전화번호 변경 시 향후 재인증 필요 (현재는 직접 변경 가능) + - Optimistic Locking으로 동시성 제어 + operationId: updateProfile + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProfileRequest' + examples: + fullUpdate: + summary: 전체 정보 수정 + value: + name: 홍길동 + phoneNumber: "01012345678" + email: hong.new@example.com + storeName: 맛있는집 (리뉴얼) + industry: 퓨전음식점 + address: 서울시 강남구 테헤란로 456 + businessHours: "매일 11:00-23:00" + partialUpdate: + summary: 일부 정보 수정 (이메일, 영업시간) + value: + email: hong.updated@example.com + businessHours: "월-금 10:00-22:00, 토-일 휴무" + responses: + '200': + description: 프로필 수정 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProfileResponse' + examples: + success: + summary: 프로필 수정 성공 응답 + value: + userId: 123 + userName: 홍길동 + email: hong.new@example.com + storeId: 456 + storeName: 맛있는집 (리뉴얼) + '400': + description: 잘못된 요청 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 사용자를 찾을 수 없음 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: 동시성 충돌 (다른 세션에서 수정) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + conflict: + summary: 동시성 충돌 + value: + code: USER_005 + error: 다른 세션에서 프로필을 수정했습니다. 새로고침 후 다시 시도하세요 + timestamp: 2025-10-22T10:30:00Z + + /api/users/password: + put: + tags: + - Profile + summary: 비밀번호 변경 + description: | + 비밀번호 변경 API + + **유저스토리:** UFR-USER-030 + + **주요 기능:** + - 현재 비밀번호 확인 필수 + - 새 비밀번호 규칙 검증 (8자 이상, 영문/숫자/특수문자 포함) + - bcrypt 해싱 + + **보안:** + - 현재 비밀번호 검증 실패 시 400 Bad Request + - 비밀번호 변경 후 기존 세션 유지 (로그아웃 불필요) + operationId: changePassword + x-user-story: UFR-USER-030 + x-controller: UserController + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordRequest' + examples: + default: + summary: 비밀번호 변경 요청 + value: + currentPassword: "Password123!" + newPassword: "NewSecurePass456!" + responses: + '200': + description: 비밀번호 변경 성공 + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordResponse' + examples: + success: + summary: 비밀번호 변경 성공 응답 + value: + success: true + message: 비밀번호가 성공적으로 변경되었습니다 + '400': + description: 현재 비밀번호 불일치 또는 새 비밀번호 규칙 위반 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + invalidCurrentPassword: + summary: 현재 비밀번호 불일치 + value: + code: USER_004 + error: 현재 비밀번호가 일치하지 않습니다 + timestamp: 2025-10-22T10:30:00Z + invalidNewPassword: + summary: 새 비밀번호 규칙 위반 + value: + code: VALIDATION_ERROR + error: 비밀번호는 8자 이상이어야 하며 영문/숫자/특수문자를 포함해야 합니다 + timestamp: 2025-10-22T10:30:00Z + '401': + description: 인증 실패 + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + description: | + JWT Bearer 토큰 인증 + + **형식:** Authorization: Bearer {JWT_TOKEN} + + **토큰 만료:** 7일 + + **Claims:** + - userId: 사용자 ID + - role: 사용자 역할 (OWNER) + - iat: 발급 시각 + - exp: 만료 시각 + + schemas: + RegisterRequest: + type: object + required: + - name + - phoneNumber + - email + - password + - storeName + - industry + - address + - businessNumber + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: 사용자 이름 (2자 이상, 한글/영문) + example: 홍길동 + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 (010XXXXXXXX) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: 이메일 주소 + example: hong@example.com + password: + type: string + minLength: 8 + maxLength: 100 + description: 비밀번호 (8자 이상, 영문/숫자/특수문자 포함) + example: "Password123!" + storeName: + type: string + minLength: 2 + maxLength: 100 + description: 매장명 + example: 맛있는집 + industry: + type: string + maxLength: 50 + description: 업종 (예 음식점, 카페, 소매점 등) + example: 음식점 + address: + type: string + minLength: 5 + maxLength: 200 + description: 매장 주소 + example: 서울시 강남구 테헤란로 123 + businessHours: + type: string + maxLength: 200 + description: 영업시간 (선택 사항) + example: "월-금 11:00-22:00, 토-일 12:00-21:00" + businessNumber: + type: string + pattern: '^\d{10}$' + description: 사업자번호 (10자리 숫자) + example: "1234567890" + + RegisterResponse: + type: object + required: + - token + - userId + - userName + - storeId + - storeName + properties: + token: + type: string + description: JWT 토큰 (7일 만료) + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 + + LoginRequest: + type: object + required: + - phoneNumber + - password + properties: + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 + example: "01012345678" + password: + type: string + minLength: 8 + description: 비밀번호 + example: "Password123!" + + LoginResponse: + type: object + required: + - token + - userId + - userName + - role + - email + properties: + token: + type: string + description: JWT 토큰 (7일 만료) + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6Ik9XTkVSIiwiaWF0IjoxNjE2MjM5MDIyLCJleHAiOjE2MTY4NDM4MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + role: + type: string + enum: [OWNER, ADMIN] + description: 사용자 역할 + example: OWNER + email: + type: string + format: email + description: 이메일 주소 + example: hong@example.com + + LogoutResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: 로그아웃 성공 여부 + example: true + message: + type: string + description: 응답 메시지 + example: 안전하게 로그아웃되었습니다 + + ProfileResponse: + type: object + required: + - userId + - userName + - phoneNumber + - email + - role + - storeId + - storeName + - industry + - address + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + phoneNumber: + type: string + description: 휴대폰 번호 + example: "01012345678" + email: + type: string + format: email + description: 이메일 주소 + example: hong@example.com + role: + type: string + enum: [OWNER, ADMIN] + description: 사용자 역할 + example: OWNER + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 + industry: + type: string + description: 업종 + example: 음식점 + address: + type: string + description: 매장 주소 + example: 서울시 강남구 테헤란로 123 + businessHours: + type: string + description: 영업시간 + example: "월-금 11:00-22:00, 토-일 12:00-21:00" + createdAt: + type: string + format: date-time + description: 가입 일시 + example: 2025-09-01T10:00:00Z + lastLoginAt: + type: string + format: date-time + description: 최종 로그인 일시 + example: 2025-10-22T09:00:00Z + + UpdateProfileRequest: + type: object + properties: + name: + type: string + minLength: 2 + maxLength: 50 + description: 사용자 이름 (선택 사항) + example: 홍길동 + phoneNumber: + type: string + pattern: '^010\d{8}$' + description: 휴대폰 번호 (선택 사항, 향후 재인증 필요) + example: "01012345678" + email: + type: string + format: email + maxLength: 100 + description: 이메일 주소 (선택 사항) + example: hong.new@example.com + storeName: + type: string + minLength: 2 + maxLength: 100 + description: 매장명 (선택 사항) + example: 맛있는집 (리뉴얼) + industry: + type: string + maxLength: 50 + description: 업종 (선택 사항) + example: 퓨전음식점 + address: + type: string + minLength: 5 + maxLength: 200 + description: 매장 주소 (선택 사항) + example: 서울시 강남구 테헤란로 456 + businessHours: + type: string + maxLength: 200 + description: 영업시간 (선택 사항) + example: "매일 11:00-23:00" + + UpdateProfileResponse: + type: object + required: + - userId + - userName + - email + - storeId + - storeName + properties: + userId: + type: integer + format: int64 + description: 사용자 ID + example: 123 + userName: + type: string + description: 사용자 이름 + example: 홍길동 + email: + type: string + format: email + description: 이메일 주소 + example: hong.new@example.com + storeId: + type: integer + format: int64 + description: 매장 ID + example: 456 + storeName: + type: string + description: 매장명 + example: 맛있는집 (리뉴얼) + + ChangePasswordRequest: + type: object + required: + - currentPassword + - newPassword + properties: + currentPassword: + type: string + minLength: 8 + description: 현재 비밀번호 + example: "Password123!" + newPassword: + type: string + minLength: 8 + maxLength: 100 + description: 새 비밀번호 (8자 이상, 영문/숫자/특수문자 포함) + example: "NewSecurePass456!" + + ChangePasswordResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: 비밀번호 변경 성공 여부 + example: true + message: + type: string + description: 응답 메시지 + example: 비밀번호가 성공적으로 변경되었습니다 + + ErrorResponse: + type: object + required: + - code + - error + - timestamp + properties: + code: + type: string + description: 에러 코드 + example: USER_001 + enum: + - USER_001 # 중복 사용자 + - USER_002 # 사업자번호 검증 실패 + - USER_003 # 사용자 없음 + - USER_004 # 현재 비밀번호 불일치 + - USER_005 # 동시성 충돌 + - AUTH_001 # 인증 실패 + - AUTH_002 # 유효하지 않은 토큰 + - VALIDATION_ERROR # 입력 검증 오류 + error: + type: string + description: 에러 메시지 + example: 이미 가입된 전화번호입니다 + timestamp: + type: string + format: date-time + description: 에러 발생 시각 + example: 2025-10-22T10:30:00Z