# 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<string | null>(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 (
    <div>
      {/* AI 추천 UI */}
      {recommendations.map(rec => (
        <RecommendationCard
          key={rec.recommendationId}
          recommendation={rec}
          onSelect={() => handleSelectRecommendation(rec.recommendationId)}
        />
      ))}
    </div>
  );
};
```

### 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
