mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 14:46:23 +00:00
- 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>
659 lines
21 KiB
YAML
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"
|