# API 매핑 설계서 ## 목차 1. [API 경로 매핑](#1-api-경로-매핑) 2. [화면별 API 매핑](#2-화면별-api-매핑) 3. [API 호출 예시](#3-api-호출-예시) --- ## 1. API 경로 매핑 ### 1.1 Runtime 환경 변수 설정 **public/runtime-env.js** ```javascript /** * 런타임 환경 변수 설정 * - 빌드 시점이 아닌 실행 시점에 환경별 API 호스트 설정 * - 도커 컨테이너 환경에서 환경변수 주입 가능 */ window.__runtime_config__ = { // API 그룹 경로 (버전 포함) API_GROUP: "/api/v1", // 7개 마이크로서비스 호스트 USER_HOST: process.env.NEXT_PUBLIC_USER_HOST || "http://localhost:8081", EVENT_HOST: process.env.NEXT_PUBLIC_EVENT_HOST || "http://localhost:8080", CONTENT_HOST: process.env.NEXT_PUBLIC_CONTENT_HOST || "http://localhost:8082", AI_HOST: process.env.NEXT_PUBLIC_AI_HOST || "http://localhost:8083", PARTICIPATION_HOST: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || "http://localhost:8084", DISTRIBUTION_HOST: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || "http://localhost:8085", ANALYTICS_HOST: process.env.NEXT_PUBLIC_ANALYTICS_HOST || "http://localhost:8086", }; ``` ### 1.2 API 클라이언트 초기화 **src/lib/api/client.ts** ```typescript import axios, { AxiosInstance } from 'axios'; // 런타임 환경 변수 타입 정의 declare global { interface Window { __runtime_config__: { API_GROUP: string; USER_HOST: string; EVENT_HOST: string; CONTENT_HOST: string; AI_HOST: string; PARTICIPATION_HOST: string; DISTRIBUTION_HOST: string; ANALYTICS_HOST: string; }; } } const config = typeof window !== 'undefined' ? window.__runtime_config__ : { API_GROUP: '/api/v1', USER_HOST: process.env.NEXT_PUBLIC_USER_HOST || 'http://localhost:8081', EVENT_HOST: process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080', CONTENT_HOST: process.env.NEXT_PUBLIC_CONTENT_HOST || 'http://localhost:8082', AI_HOST: process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083', PARTICIPATION_HOST: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || 'http://localhost:8084', DISTRIBUTION_HOST: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://localhost:8085', ANALYTICS_HOST: process.env.NEXT_PUBLIC_ANALYTICS_HOST || 'http://localhost:8086', }; // JWT 토큰 가져오기 헬퍼 const getAuthToken = (): string | null => { if (typeof window === 'undefined') return null; return localStorage.getItem('accessToken'); }; // Request Interceptor (JWT 토큰 자동 추가) const authInterceptor = (instance: AxiosInstance) => { instance.interceptors.request.use( (config) => { const token = getAuthToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response Interceptor (401 처리) instance.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // 토큰 만료 또는 인증 실패 localStorage.removeItem('accessToken'); window.location.href = '/login'; } return Promise.reject(error); } ); }; // 1. User Service API Client (인증, 프로필 관리) export const userClient = axios.create({ baseURL: `${config.USER_HOST}`, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); authInterceptor(userClient); // 2. Event Service API Client (이벤트 생명주기 관리) export const eventClient = axios.create({ baseURL: `${config.EVENT_HOST}`, timeout: 30000, // Job 폴링 고려 headers: { 'Content-Type': 'application/json', }, }); authInterceptor(eventClient); // 3. Content Service API Client (이미지 생성 및 편집) export const contentClient = axios.create({ baseURL: `${config.CONTENT_HOST}`, timeout: 30000, // 이미지 생성 Job 폴링 headers: { 'Content-Type': 'application/json', }, }); authInterceptor(contentClient); // 4. AI Service API Client (AI 추천 생성) export const aiClient = axios.create({ baseURL: `${config.AI_HOST}`, timeout: 30000, // AI 생성 Job 폴링 headers: { 'Content-Type': 'application/json', }, }); authInterceptor(aiClient); // 5. Participation Service API Client (참여자/당첨자 관리) export const participationClient = axios.create({ baseURL: `${config.PARTICIPATION_HOST}`, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); authInterceptor(participationClient); // 6. Distribution Service API Client (다중 채널 배포) export const distributionClient = axios.create({ baseURL: `${config.DISTRIBUTION_HOST}`, timeout: 20000, // 다중 채널 배포 시간 고려 headers: { 'Content-Type': 'application/json', }, }); authInterceptor(distributionClient); // 7. Analytics Service API Client (성과 분석 및 대시보드) export const analyticsClient = axios.create({ baseURL: `${config.ANALYTICS_HOST}`, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); authInterceptor(analyticsClient); ``` ### 1.3 환경별 설정 | 환경 | User Service | Event Service | Content Service | AI Service | Participation Service | Distribution Service | Analytics Service | |------|-------------|---------------|----------------|-----------|---------------------|---------------------|-------------------| | **로컬 개발** | http://localhost:8081 | http://localhost:8080 | http://localhost:8082 | http://localhost:8083 | http://localhost:8084 | http://localhost:8085 | http://localhost:8086 | | **개발 서버** | https://dev-api.kt-event-marketing.com/user/v1 | https://dev-api.kt-event-marketing.com/event/v1 | https://dev-api.kt-event-marketing.com/content/v1 | https://dev-api.kt-event-marketing.com/ai/v1 | https://dev-api.kt-event-marketing.com/participation/v1 | https://dev-api.kt-event-marketing.com/distribution/v1 | https://dev-api.kt-event-marketing.com/analytics/v1 | | **프로덕션** | https://api.kt-event-marketing.com/user/v1 | https://api.kt-event-marketing.com/event/v1 | https://api.kt-event-marketing.com/content/v1 | https://api.kt-event-marketing.com/ai/v1 | https://api.kt-event-marketing.com/participation/v1 | https://api.kt-event-marketing.com/distribution/v1 | https://api.kt-event-marketing.com/analytics/v1 | --- ## 2. 화면별 API 매핑 ### 2.1 인증 영역 #### AUTH-01: 로그인 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 로그인 | User Service | `/users/login` | POST | ❌ | **요청 데이터** ```typescript interface LoginRequest { phoneNumber: string; // "010XXXXXXXX" 형식 password: string; // 최소 8자 } ``` **응답 데이터** ```typescript interface LoginResponse { token: string; // JWT 토큰 (7일 만료) userId: number; userName: string; role: 'OWNER' | 'ADMIN'; email: string; } ``` #### AUTH-02: 회원가입 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 회원가입 | User Service | `/users/register` | POST | ❌ | **요청 데이터** ```typescript interface RegisterRequest { name: string; // 2-50자 phoneNumber: string; // "010XXXXXXXX" email: string; // 이메일 형식 password: string; // 8자 이상, 영문/숫자/특수문자 storeName: string; // 2-100자 industry: string; // 업종 (예: 음식점, 카페) address: string; // 5-200자 businessHours?: string; // 영업시간 (선택) } ``` **응답 데이터** ```typescript interface RegisterResponse { token: string; // JWT 토큰 (자동 로그인) userId: number; userName: string; storeId: number; storeName: string; } ``` #### AUTH-03: 프로필 관리 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 프로필 조회 | User Service | `/users/profile` | GET | ✅ | | 프로필 수정 | User Service | `/users/profile` | PUT | ✅ | | 비밀번호 변경 | User Service | `/users/password` | PUT | ✅ | **프로필 조회 응답** ```typescript interface ProfileResponse { userId: number; userName: string; phoneNumber: string; email: string; role: 'OWNER' | 'ADMIN'; storeId: number; storeName: string; industry: string; address: string; businessHours?: string; createdAt: string; // ISO 8601 lastLoginAt: string; // ISO 8601 } ``` **프로필 수정 요청** ```typescript interface UpdateProfileRequest { name?: string; phoneNumber?: string; email?: string; storeName?: string; industry?: string; address?: string; businessHours?: string; } ``` **비밀번호 변경 요청** ```typescript interface ChangePasswordRequest { currentPassword: string; newPassword: string; // 8자 이상, 영문/숫자/특수문자 } ``` #### AUTH-04: 로그아웃 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 로그아웃 | User Service | `/users/logout` | POST | ✅ | **응답 데이터** ```typescript interface LogoutResponse { success: boolean; message: string; // "안전하게 로그아웃되었습니다" } ``` --- ### 2.2 대시보드 영역 #### DASH-01: 메인 대시보드 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이벤트 요약 조회 | Event Service | `/events?status=PUBLISHED&page=0&size=5` | GET | ✅ | | 최근 이벤트 조회 | Event Service | `/events?sort=createdAt&order=desc&page=0&size=3` | GET | ✅ | **이벤트 목록 응답** ```typescript interface EventListResponse { content: EventSummary[]; page: PageInfo; } interface EventSummary { eventId: string; // UUID eventName: string; objective: string; // "신규 고객 유치", "재방문 유도" 등 status: 'DRAFT' | 'PUBLISHED' | 'ENDED'; startDate: string; // "YYYY-MM-DD" endDate: string; // "YYYY-MM-DD" thumbnailUrl?: string; createdAt: string; // ISO 8601 } interface PageInfo { page: number; // 현재 페이지 (0부터 시작) size: number; // 페이지 크기 totalElements: number; // 전체 요소 수 totalPages: number; // 전체 페이지 수 } ``` #### DASH-02: 이벤트 목록 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 전체 이벤트 조회 | Event Service | `/events` | GET | ✅ | | 상태별 필터링 | Event Service | `/events?status={status}` | GET | ✅ | | 검색 | Event Service | `/events?search={keyword}` | GET | ✅ | **Query Parameters** - `status`: DRAFT, PUBLISHED, ENDED - `objective`: 이벤트 목적 필터 - `search`: 이벤트명 검색 - `page`: 페이지 번호 (기본 0) - `size`: 페이지 크기 (기본 20, 최대 100) - `sort`: 정렬 기준 (createdAt, startDate, endDate) - `order`: 정렬 순서 (asc, desc) --- ### 2.3 이벤트 생성 플로우 (Funnel) #### EVENT-01: 목적 선택 (Step 1) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 목적 선택 및 이벤트 생성 | Event Service | `/events/objectives` | POST | ✅ | **요청 데이터** ```typescript interface SelectObjectiveRequest { objective: string; // "신규 고객 유치", "재방문 유도", "매출 증대", "브랜드 인지도 향상" } ``` **응답 데이터** ```typescript interface EventCreatedResponse { eventId: string; // UUID (생성된 이벤트 ID) status: 'DRAFT'; // 항상 DRAFT objective: string; createdAt: string; // ISO 8601 } ``` #### EVENT-02: AI 추천 확인 (Step 2) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | AI 추천 요청 | Event Service | `/events/{eventId}/ai-recommendations` | POST | ✅ | | Job 상태 폴링 | Event Service | `/jobs/{jobId}` | GET | ✅ | | AI 추천 선택 | Event Service | `/events/{eventId}/recommendations` | PUT | ✅ | **AI 추천 요청** ```typescript interface AiRecommendationRequest { storeInfo: { storeId: string; storeName: string; category: string; description?: string; }; } ``` **Job 접수 응답 (202 Accepted)** ```typescript interface JobAcceptedResponse { jobId: string; // UUID status: 'PENDING'; message: string; // "AI 추천 생성 요청이 접수되었습니다..." } ``` **Job 상태 조회 응답** ```typescript interface JobStatusResponse { jobId: string; jobType: 'AI_RECOMMENDATION' | 'IMAGE_GENERATION'; status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'; progress: number; // 0-100 (%) resultKey?: string; // Redis 결과 키 (COMPLETED 시) errorMessage?: string; // 에러 메시지 (FAILED 시) createdAt: string; completedAt?: string; } ``` **AI 추천 선택 요청** ```typescript interface SelectRecommendationRequest { recommendationId: string; // 선택한 추천 ID customizations?: { eventName?: string; description?: string; startDate?: string; // "YYYY-MM-DD" endDate?: string; // "YYYY-MM-DD" discountRate?: number; }; } ``` #### EVENT-03: 콘텐츠 미리보기 (Step 3) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이미지 생성 요청 | Event Service | `/events/{eventId}/images` | POST | ✅ | | Job 상태 폴링 | Event Service | `/jobs/{jobId}` | GET | ✅ | | 이미지 선택 | Event Service | `/events/{eventId}/images/{imageId}/select` | PUT | ✅ | **이미지 생성 요청** ```typescript interface ImageGenerationRequest { eventInfo: { eventName: string; description: string; promotionType: string; // "할인", "증정", "쿠폰" 등 }; imageCount?: number; // 1-5, 기본 3 } ``` #### EVENT-04: 콘텐츠 편집 (Step 4) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이미지 편집 | Event Service | `/events/{eventId}/images/{imageId}/edit` | PUT | ✅ | **이미지 편집 요청** ```typescript interface ImageEditRequest { editType: 'TEXT_OVERLAY' | 'COLOR_ADJUST' | 'CROP' | 'FILTER'; parameters: { // TEXT_OVERLAY 예시 text?: string; fontSize?: number; color?: string; // HEX 코드 position?: 'center' | 'top' | 'bottom'; // COLOR_ADJUST 예시 brightness?: number; // -100 ~ 100 contrast?: number; // -100 ~ 100 saturation?: number; // -100 ~ 100 }; } ``` **이미지 편집 응답** ```typescript interface ImageEditResponse { imageId: string; imageUrl: string; // 편집된 이미지 URL editedAt: string; } ``` #### EVENT-05: 배포 채널 선택 (Step 5) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 배포 채널 선택 | Event Service | `/events/{eventId}/channels` | PUT | ✅ | **배포 채널 선택 요청** ```typescript interface SelectChannelsRequest { channels: ('WEBSITE' | 'KAKAO' | 'INSTAGRAM' | 'FACEBOOK' | 'NAVER_BLOG')[]; // 최소 1개 이상 선택 } ``` #### EVENT-06: 최종 승인 (Step 6) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이벤트 배포 | Event Service | `/events/{eventId}/publish` | POST | ✅ | **이벤트 배포 응답** ```typescript interface EventPublishedResponse { eventId: string; status: 'PUBLISHED'; publishedAt: string; channels: string[]; // 배포된 채널 목록 distributionResults: DistributionResult[]; } interface DistributionResult { channel: string; // "WEBSITE", "KAKAO" 등 success: boolean; url?: string; // 배포된 URL message: string; } ``` --- ### 2.4 이벤트 관리 영역 #### MANAGE-01: 이벤트 상세 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이벤트 상세 조회 | Event Service | `/events/{eventId}` | GET | ✅ | | 이벤트 수정 | Event Service | `/events/{eventId}` | PUT | ✅ | | 이벤트 삭제 | Event Service | `/events/{eventId}` | DELETE | ✅ | | 이벤트 조기 종료 | Event Service | `/events/{eventId}/end` | POST | ✅ | **이벤트 상세 응답** ```typescript interface EventDetailResponse { eventId: string; userId: string; storeId: string; eventName: string; objective: string; description: string; targetAudience: string; // "20-30대 여성" 등 promotionType: string; // "할인", "증정", "쿠폰" discountRate?: number; // 할인율 (%) startDate: string; endDate: string; status: 'DRAFT' | 'PUBLISHED' | 'ENDED'; selectedImageId?: string; selectedImageUrl?: string; generatedImages?: GeneratedImage[]; channels?: string[]; aiRecommendations?: AiRecommendation[]; createdAt: string; updatedAt: string; } interface GeneratedImage { imageId: string; imageUrl: string; isSelected: boolean; createdAt: string; } interface AiRecommendation { recommendationId: string; eventName: string; description: string; promotionType: string; targetAudience: string; isSelected: boolean; } ``` **이벤트 수정 요청** ```typescript interface UpdateEventRequest { eventName?: string; description?: string; startDate?: string; endDate?: string; discountRate?: number; } ``` **이벤트 조기 종료 요청** ```typescript interface EndEventRequest { reason: string; // "목표 달성으로 조기 종료" 등 } ``` #### MANAGE-02: 참여자 목록 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 참여자 목록 조회 | Participation Service | `/events/{eventId}/participants` | GET | ✅ | | 참여자 상세 조회 | Participation Service | `/events/{eventId}/participants/{participantId}` | GET | ✅ | **참여자 목록 응답** ```typescript interface ParticipantListResponse { success: boolean; message: string; data: { participants: ParticipantInfo[]; pagination: Pagination; }; } interface ParticipantInfo { participantId: string; eventId: string; name: string; phoneNumber: string; // "010-1234-5678" 형식 email?: string; participatedAt: string; // ISO 8601 storeVisited: boolean; bonusEntries: number; // 보너스 응모권 (매장 방문 시 +1) isWinner: boolean; } interface Pagination { currentPage: number; pageSize: number; totalElements: number; totalPages: number; hasNext: boolean; hasPrevious: boolean; } ``` **Query Parameters** - `page`: 페이지 번호 (기본 0) - `size`: 페이지 크기 (기본 20, 최대 100) - `storeVisited`: 매장 방문 여부 필터 (true/false) #### MANAGE-03: 고객 참여 화면 (Public) | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이벤트 참여 | Participation Service | `/events/{eventId}/participate` | POST | ❌ | | 이벤트 정보 조회 | Event Service | `/events/{eventId}` | GET | ❌ | **참여 요청** ```typescript interface ParticipationRequest { name: string; // 2-50자 phoneNumber: string; // "010-XXXX-XXXX" 형식 email?: string; agreeMarketing: boolean; // 마케팅 정보 수신 동의 agreePrivacy: boolean; // 개인정보 수집 동의 (필수) storeVisited: boolean; // 매장 방문 여부 (보너스 응모권) } ``` **참여 응답** ```typescript interface ParticipationResponse { success: boolean; message: string; // "이벤트 참여가 완료되었습니다" data: ParticipantInfo; } ``` #### MANAGE-04: 당첨자 추첨 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 당첨자 추첨 | Participation Service | `/events/{eventId}/draw-winners` | POST | ✅ | | 당첨자 목록 조회 | Participation Service | `/events/{eventId}/winners` | GET | ✅ | **당첨자 추첨 요청** ```typescript interface DrawWinnersRequest { winnerCount: number; // 당첨자 수 (최소 1) applyStoreVisitBonus?: boolean; // 매장 방문 보너스 적용 (기본 true) } ``` **당첨자 추첨 응답** ```typescript interface DrawWinnersResponse { success: boolean; message: string; data: { eventId: string; totalParticipants: number; winnerCount: number; drawnAt: string; winners: WinnerSummary[]; }; } interface WinnerSummary { participantId: string; name: string; phoneNumber: string; rank: number; // 당첨 순위 (1등, 2등, ...) } ``` **당첨자 목록 응답** ```typescript interface WinnerListResponse { success: boolean; message: string; data: { eventId: string; drawnAt: string; totalWinners: number; winners: WinnerInfo[]; pagination: Pagination; }; } interface WinnerInfo { participantId: string; name: string; phoneNumber: string; email?: string; rank: number; wonAt: string; } ``` #### MANAGE-05: 성과 분석 화면 | 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 | |-----|------------|---------|-----------|---------| | 이벤트 상세 조회 | Event Service | `/events/{eventId}` | GET | ✅ | | 참여자 통계 | Participation Service | `/events/{eventId}/participants` | GET | ✅ | **성과 분석용 데이터 조합** - 이벤트 기본 정보 (Event Service) - 참여자 수, 매장 방문율 (Participation Service) - 당첨자 수 (Participation Service) **계산 지표** ```typescript interface EventAnalytics { // 이벤트 정보 eventName: string; eventPeriod: { start: string; end: string }; status: string; // 참여 지표 totalParticipants: number; storeVisitCount: number; storeVisitRate: number; // (매장 방문 / 전체 참여) * 100 // 당첨 지표 totalWinners: number; winnerDrawnDate?: string; // 채널별 배포 현황 distributionChannels: string[]; // 시간대별 참여 추이 (프론트엔드에서 계산) participationTrend: { date: string; count: number; }[]; } ``` --- ## 3. API 호출 예시 ### 3.1 로그인 플로우 ```typescript // src/lib/api/auth.ts import { userClient } from './client'; export const authApi = { // 로그인 async login(phoneNumber: string, password: string) { const response = await userClient.post('/users/login', { phoneNumber, password, }); // JWT 토큰 저장 localStorage.setItem('accessToken', response.data.token); return response.data; }, // 로그아웃 async logout() { const response = await userClient.post('/users/logout'); // 토큰 삭제 localStorage.removeItem('accessToken'); return response.data; }, // 회원가입 async register(data: RegisterRequest) { const response = await userClient.post('/users/register', data); // 자동 로그인 (JWT 토큰 저장) localStorage.setItem('accessToken', response.data.token); return response.data; }, }; ``` **호출 예시** ```typescript // 컴포넌트에서 사용 const handleLogin = async () => { try { const result = await authApi.login('01012345678', 'Password123!'); console.log('로그인 성공:', result.userName); router.push('/'); // 대시보드로 이동 } catch (error) { console.error('로그인 실패:', error); toast.error('로그인에 실패했습니다'); } }; ``` ### 3.2 이벤트 생성 플로우 ```typescript // src/lib/api/events.ts import { eventClient } from './client'; export const eventApi = { // Step 1: 목적 선택 async selectObjective(objective: string) { const response = await eventClient.post('/events/objectives', { objective, }); return response.data; // { eventId, status, objective, createdAt } }, // Step 2: AI 추천 요청 async requestAiRecommendations(eventId: string, storeInfo: any) { const response = await eventClient.post( `/events/${eventId}/ai-recommendations`, { storeInfo } ); return response.data; // { jobId, status, message } }, // Job 상태 폴링 async getJobStatus(jobId: string) { const response = await eventClient.get(`/jobs/${jobId}`); return response.data; }, // AI 추천 선택 async selectRecommendation( eventId: string, recommendationId: string, customizations?: any ) { const response = await eventClient.put( `/events/${eventId}/recommendations`, { recommendationId, customizations } ); return response.data; }, // Step 3: 이미지 생성 요청 async requestImageGeneration(eventId: string, eventInfo: any) { const response = await eventClient.post(`/events/${eventId}/images`, { eventInfo, imageCount: 3, }); return response.data; // { jobId, status, message } }, // 이미지 선택 async selectImage(eventId: string, imageId: string) { const response = await eventClient.put( `/events/${eventId}/images/${imageId}/select` ); return response.data; }, // Step 4: 이미지 편집 async editImage(eventId: string, imageId: string, editRequest: any) { const response = await eventClient.put( `/events/${eventId}/images/${imageId}/edit`, editRequest ); return response.data; }, // Step 5: 배포 채널 선택 async selectChannels(eventId: string, channels: string[]) { const response = await eventClient.put(`/events/${eventId}/channels`, { channels, }); return response.data; }, // Step 6: 최종 배포 async publishEvent(eventId: string) { const response = await eventClient.post(`/events/${eventId}/publish`); return response.data; }, }; ``` **Funnel 컴포넌트에서 사용 예시** ```typescript // Step 2: AI 추천 컴포넌트 const RecommendationStep = ({ eventId, onNext }) => { const [jobId, setJobId] = useState(null); const [recommendations, setRecommendations] = useState([]); // AI 추천 요청 const requestRecommendations = async () => { const storeInfo = { /* User Service에서 조회한 매장 정보 */ }; const result = await eventApi.requestAiRecommendations(eventId, storeInfo); setJobId(result.jobId); // Job 폴링 시작 pollJobStatus(result.jobId); }; // Job 상태 폴링 (3초 간격) const pollJobStatus = async (jobId: string) => { const interval = setInterval(async () => { const status = await eventApi.getJobStatus(jobId); if (status.status === 'COMPLETED') { clearInterval(interval); // Redis에서 결과 조회 (resultKey 사용) fetchRecommendations(status.resultKey); } else if (status.status === 'FAILED') { clearInterval(interval); toast.error('AI 추천 생성에 실패했습니다'); } }, 3000); }; // 추천 선택 및 다음 단계 const handleSelectRecommendation = async (recommendationId: string) => { await eventApi.selectRecommendation(eventId, recommendationId); onNext(); // Step 3으로 이동 }; return (
{/* AI 추천 UI */} {recommendations.map(rec => ( handleSelectRecommendation(rec.recommendationId)} /> ))}
); }; ``` ### 3.3 참여자 관리 ```typescript // src/lib/api/participants.ts import { participationClient } from './client'; export const participantApi = { // 참여자 목록 조회 async getParticipants( eventId: string, page: number = 0, size: number = 20, storeVisited?: boolean ) { const params = new URLSearchParams({ page: page.toString(), size: size.toString(), }); if (storeVisited !== undefined) { params.append('storeVisited', storeVisited.toString()); } const response = await participationClient.get( `/events/${eventId}/participants?${params}` ); return response.data.data; }, // 참여자 상세 조회 async getParticipantDetail(eventId: string, participantId: string) { const response = await participationClient.get( `/events/${eventId}/participants/${participantId}` ); return response.data.data; }, // 당첨자 추첨 async drawWinners( eventId: string, winnerCount: number, applyStoreVisitBonus: boolean = true ) { const response = await participationClient.post( `/events/${eventId}/draw-winners`, { winnerCount, applyStoreVisitBonus } ); return response.data.data; }, // 당첨자 목록 조회 async getWinners(eventId: string, page: number = 0, size: number = 20) { const response = await participationClient.get( `/events/${eventId}/winners?page=${page}&size=${size}` ); return response.data.data; }, // 고객 참여 (인증 불필요) async participate(eventId: string, data: ParticipationRequest) { const response = await participationClient.post( `/events/${eventId}/participate`, data ); return response.data.data; }, }; ``` **React Query를 사용한 데이터 패칭 예시** ```typescript // src/hooks/useParticipants.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { participantApi } from '@/lib/api/participants'; export const useParticipants = (eventId: string, page: number = 0) => { return useQuery({ queryKey: ['participants', eventId, page], queryFn: () => participantApi.getParticipants(eventId, page), enabled: !!eventId, }); }; export const useDrawWinners = (eventId: string) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ winnerCount }: { winnerCount: number }) => participantApi.drawWinners(eventId, winnerCount), onSuccess: () => { // 참여자 목록 갱신 queryClient.invalidateQueries({ queryKey: ['participants', eventId] }); // 당첨자 목록 갱신 queryClient.invalidateQueries({ queryKey: ['winners', eventId] }); }, }); }; ``` ### 3.4 에러 처리 ```typescript // src/lib/api/errorHandler.ts import { AxiosError } from 'axios'; export interface ApiError { code: string; message: string; timestamp: string; details?: string[]; } export const handleApiError = (error: unknown): string => { if (error instanceof AxiosError) { const apiError = error.response?.data as ApiError; // 에러 코드별 처리 switch (apiError.code) { case 'USER_001': return '이미 가입된 전화번호입니다'; case 'AUTH_001': return '전화번호 또는 비밀번호를 확인해주세요'; case 'DUPLICATE_PARTICIPATION': return '이미 참여하신 이벤트입니다'; case 'INVALID_WINNER_COUNT': return '당첨자 수가 참여자 수보다 많습니다'; default: return apiError.message || '요청 처리 중 오류가 발생했습니다'; } } return '알 수 없는 오류가 발생했습니다'; }; ``` --- ## 4. 요약 ### 4.1 주요 API 엔드포인트 요약 | 서비스 | 주요 기능 | 엔드포인트 수 | |--------|---------|------------| | **User Service** | 인증, 프로필 관리 | 6개 | | **Event Service** | 이벤트 생성/관리, Job 폴링 | 14개 | | **Participation Service** | 참여자/당첨자 관리 | 5개 | ### 4.2 비동기 작업 처리 **AI 추천 생성 & 이미지 생성** - 요청 시 `202 Accepted` 응답과 함께 `jobId` 반환 - `/jobs/{jobId}`로 3초 간격 폴링 - `status: COMPLETED` 시 `resultKey`로 Redis 결과 조회 - `status: FAILED` 시 에러 메시지 표시 ### 4.3 인증 처리 **JWT 토큰 관리** - 로그인/회원가입 성공 시 `localStorage`에 저장 - 모든 API 요청 시 `Authorization: Bearer {token}` 헤더 자동 추가 - 401 응답 시 자동 로그아웃 및 로그인 페이지 리다이렉트 ### 4.4 데이터 캐싱 전략 **React Query 캐싱 키** ```typescript ['user', 'profile'] // 프로필 정보 ['events', { status, page }] // 이벤트 목록 ['event', eventId] // 이벤트 상세 ['participants', eventId, page] // 참여자 목록 ['winners', eventId] // 당첨자 목록 ['job', jobId] // Job 상태 ``` --- **문서 버전**: 1.0 **작성일**: 2025-01-24 **작성자**: Frontend Design Team