kt-event-marketing/design/backend/api/participation-service-api.yaml
doyeon a2d9d8f969 전화번호 형식 변경: 하이픈 제거하고 숫자만 저장
- ParticipationRegisterRequest 스키마 수정
  - 패턴: ^\d{3}-\d{4}-\d{4}$ → ^\d{10,11}$
  - 설명 및 예시 업데이트
- 요청 예시 전화번호 형식 변경
- 검색 파라미터 설명 및 예시 업데이트

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 16:50:33 +09:00

659 lines
21 KiB
YAML

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: "01012345678"
entryPath: "SNS"
consentMarketing: true
매장방문참여:
value:
eventId: "evt-12345-abcde"
name: "김철수"
phoneNumber: "01098765432"
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: "01012"
- 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{10,11}$'
description: 전화번호 (숫자만, 10~11자리)
example: "01012345678"
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"