mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 11:36:24 +00:00
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
32 KiB
32 KiB
API 매핑 설계서
목차
1. API 경로 매핑
1.1 Runtime 환경 변수 설정
public/runtime-env.js
/**
* 런타임 환경 변수 설정
* - 빌드 시점이 아닌 실행 시점에 환경별 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
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 환경별 설정
2. 화면별 API 매핑
2.1 인증 영역
AUTH-01: 로그인 화면
| 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 |
|---|---|---|---|---|
| 로그인 | User Service | /users/login |
POST | ❌ |
요청 데이터
interface LoginRequest {
phoneNumber: string; // "010XXXXXXXX" 형식
password: string; // 최소 8자
}
응답 데이터
interface LoginResponse {
token: string; // JWT 토큰 (7일 만료)
userId: number;
userName: string;
role: 'OWNER' | 'ADMIN';
email: string;
}
AUTH-02: 회원가입 화면
| 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 |
|---|---|---|---|---|
| 회원가입 | User Service | /users/register |
POST | ❌ |
요청 데이터
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; // 영업시간 (선택)
}
응답 데이터
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 | ✅ |
프로필 조회 응답
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
}
프로필 수정 요청
interface UpdateProfileRequest {
name?: string;
phoneNumber?: string;
email?: string;
storeName?: string;
industry?: string;
address?: string;
businessHours?: string;
}
비밀번호 변경 요청
interface ChangePasswordRequest {
currentPassword: string;
newPassword: string; // 8자 이상, 영문/숫자/특수문자
}
AUTH-04: 로그아웃
| 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 |
|---|---|---|---|---|
| 로그아웃 | User Service | /users/logout |
POST | ✅ |
응답 데이터
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 | ✅ |
이벤트 목록 응답
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, ENDEDobjective: 이벤트 목적 필터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 | ✅ |
요청 데이터
interface SelectObjectiveRequest {
objective: string; // "신규 고객 유치", "재방문 유도", "매출 증대", "브랜드 인지도 향상"
}
응답 데이터
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 추천 요청
interface AiRecommendationRequest {
storeInfo: {
storeId: string;
storeName: string;
category: string;
description?: string;
};
}
Job 접수 응답 (202 Accepted)
interface JobAcceptedResponse {
jobId: string; // UUID
status: 'PENDING';
message: string; // "AI 추천 생성 요청이 접수되었습니다..."
}
Job 상태 조회 응답
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 추천 선택 요청
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 | ✅ |
이미지 생성 요청
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 | ✅ |
이미지 편집 요청
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
};
}
이미지 편집 응답
interface ImageEditResponse {
imageId: string;
imageUrl: string; // 편집된 이미지 URL
editedAt: string;
}
EVENT-05: 배포 채널 선택 (Step 5)
| 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 |
|---|---|---|---|---|
| 배포 채널 선택 | Event Service | /events/{eventId}/channels |
PUT | ✅ |
배포 채널 선택 요청
interface SelectChannelsRequest {
channels: ('WEBSITE' | 'KAKAO' | 'INSTAGRAM' | 'FACEBOOK' | 'NAVER_BLOG')[];
// 최소 1개 이상 선택
}
EVENT-06: 최종 승인 (Step 6)
| 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 |
|---|---|---|---|---|
| 이벤트 배포 | Event Service | /events/{eventId}/publish |
POST | ✅ |
이벤트 배포 응답
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 | ✅ |
이벤트 상세 응답
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;
}
이벤트 수정 요청
interface UpdateEventRequest {
eventName?: string;
description?: string;
startDate?: string;
endDate?: string;
discountRate?: number;
}
이벤트 조기 종료 요청
interface EndEventRequest {
reason: string; // "목표 달성으로 조기 종료" 등
}
MANAGE-02: 참여자 목록 화면
| 기능 | 백엔드 서비스 | API 경로 | HTTP 메서드 | 인증 필요 |
|---|---|---|---|---|
| 참여자 목록 조회 | Participation Service | /events/{eventId}/participants |
GET | ✅ |
| 참여자 상세 조회 | Participation Service | /events/{eventId}/participants/{participantId} |
GET | ✅ |
참여자 목록 응답
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 | ❌ |
참여 요청
interface ParticipationRequest {
name: string; // 2-50자
phoneNumber: string; // "010-XXXX-XXXX" 형식
email?: string;
agreeMarketing: boolean; // 마케팅 정보 수신 동의
agreePrivacy: boolean; // 개인정보 수집 동의 (필수)
storeVisited: boolean; // 매장 방문 여부 (보너스 응모권)
}
참여 응답
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 | ✅ |
당첨자 추첨 요청
interface DrawWinnersRequest {
winnerCount: number; // 당첨자 수 (최소 1)
applyStoreVisitBonus?: boolean; // 매장 방문 보너스 적용 (기본 true)
}
당첨자 추첨 응답
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등, ...)
}
당첨자 목록 응답
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)
계산 지표
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 로그인 플로우
// 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;
},
};
호출 예시
// 컴포넌트에서 사용
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 이벤트 생성 플로우
// 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 컴포넌트에서 사용 예시
// 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 참여자 관리
// 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를 사용한 데이터 패칭 예시
// 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 에러 처리
// 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 캐싱 키
['user', 'profile'] // 프로필 정보
['events', { status, page }] // 이벤트 목록
['event', eventId] // 이벤트 상세
['participants', eventId, page] // 참여자 목록
['winners', eventId] // 당첨자 목록
['job', jobId] // Job 상태
문서 버전: 1.0 작성일: 2025-01-24 작성자: Frontend Design Team