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"