mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 19:26:24 +00:00
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1159 lines
32 KiB
Markdown
1159 lines
32 KiB
Markdown
# 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
|