all apis
This commit is contained in:
parent
fb63850f9d
commit
65a9f8161b
111
claude/api-design.md
Normal file
111
claude/api-design.md
Normal file
@ -0,0 +1,111 @@
|
||||
# API설계가이드
|
||||
|
||||
[요청사항]
|
||||
- <작성원칙>을 준용하여 설계
|
||||
- <작성순서>에 따라 설계
|
||||
- [결과파일] 안내에 따라 파일 작성
|
||||
- 최종 완료 후 API 확인 방법 안내
|
||||
- https://editor.swagger.io/ 접근
|
||||
- 생성된 swagger yaml파일을 붙여서 확인 및 테스트
|
||||
|
||||
[가이드]
|
||||
<작성 원칙>
|
||||
- 각 서비스 API는 독립적으로 완전한 명세를 포함
|
||||
- 공통 스키마는 각 서비스에서 필요에 따라 직접 정의
|
||||
- 서비스 간 의존성을 최소화하여 독립 배포 가능
|
||||
- 중복되는 스키마가 많아질 경우에만 공통 파일 도입 검토
|
||||
<작성순서>
|
||||
- 준비:
|
||||
- 유저스토리, 외부시퀀스설계서, 내부시퀀스설계서 분석 및 이해
|
||||
- 실행:
|
||||
- <병렬처리> 안내에 따라 동시 수행
|
||||
- <API선정원칙>에 따라 API 선정
|
||||
- <파일작성안내>에 따라 작성
|
||||
- <검증방법>에 따라 작성된 YAML의 문법 및 구조 검증 수행
|
||||
- 검토:
|
||||
- <작성원칙> 준수 검토
|
||||
- 스쿼드 팀원 리뷰: 누락 및 개선 사항 검토
|
||||
- 수정 사항 선택 및 반영
|
||||
|
||||
<API선정원칙>
|
||||
- 유저스토리와 매칭 되어야 함. 불필요한 추가 설계 금지
|
||||
(유저스토리 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 형식)
|
||||
1036
design/backend/api/ai-service-api.yaml
Normal file
1036
design/backend/api/ai-service-api.yaml
Normal file
File diff suppressed because it is too large
Load Diff
942
design/backend/api/analytics-service-api.yaml
Normal file
942
design/backend/api/analytics-service-api.yaml
Normal file
@ -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"
|
||||
548
design/backend/api/content-service-api.yaml
Normal file
548
design/backend/api/content-service-api.yaml
Normal file
@ -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
|
||||
764
design/backend/api/distribution-service-api.yaml
Normal file
764
design/backend/api/distribution-service-api.yaml
Normal file
@ -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
|
||||
1058
design/backend/api/event-service-api.yaml
Normal file
1058
design/backend/api/event-service-api.yaml
Normal file
File diff suppressed because it is too large
Load Diff
658
design/backend/api/participation-service-api.yaml
Normal file
658
design/backend/api/participation-service-api.yaml
Normal file
@ -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"
|
||||
875
design/backend/api/user-service-api.yaml
Normal file
875
design/backend/api/user-service-api.yaml
Normal file
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user