mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 10:56:23 +00:00
428 lines
11 KiB
TypeScript
428 lines
11 KiB
TypeScript
import axios, { AxiosInstance } from 'axios';
|
|
|
|
// Event Service API 클라이언트
|
|
const EVENT_API_BASE_URL = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080';
|
|
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1';
|
|
|
|
export const eventApiClient: AxiosInstance = axios.create({
|
|
baseURL: `${EVENT_API_BASE_URL}/api/${API_VERSION}`,
|
|
timeout: 30000, // Job 폴링 고려
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
withCredentials: false, // CORS 설정
|
|
});
|
|
|
|
// Request interceptor
|
|
eventApiClient.interceptors.request.use(
|
|
(config) => {
|
|
console.log('📅 Event API Request:', {
|
|
method: config.method?.toUpperCase(),
|
|
url: config.url,
|
|
baseURL: config.baseURL,
|
|
fullURL: `${config.baseURL}${config.url}`,
|
|
});
|
|
|
|
// POST/PUT 요청일 경우 payload를 JSON 형태로 출력
|
|
if (config.data) {
|
|
console.log('📦 Request Payload (JSON):', JSON.stringify(config.data, null, 2));
|
|
console.log('📦 Request Payload (Object):', config.data);
|
|
}
|
|
|
|
const token = localStorage.getItem('accessToken');
|
|
if (token && config.headers) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
return config;
|
|
},
|
|
(error) => {
|
|
console.error('❌ Event API Request Error:', error);
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Response interceptor
|
|
eventApiClient.interceptors.response.use(
|
|
(response) => {
|
|
console.log('✅ Event API Response:', {
|
|
status: response.status,
|
|
url: response.config.url,
|
|
data: response.data,
|
|
});
|
|
return response;
|
|
},
|
|
(error) => {
|
|
console.error('❌ Event API Error:', {
|
|
message: error.message,
|
|
status: error.response?.status,
|
|
url: error.config?.url,
|
|
requestData: error.config?.data,
|
|
responseData: error.response?.data,
|
|
});
|
|
|
|
// 400 에러일 경우 더 상세한 정보 출력
|
|
if (error.response?.status === 400) {
|
|
console.error('🚨 400 Bad Request 상세 정보:');
|
|
console.error(' 요청 URL:', `${error.config?.baseURL}${error.config?.url}`);
|
|
console.error(' 요청 본문:', JSON.stringify(error.config?.data, null, 2));
|
|
console.error(' 응답 본문:', JSON.stringify(error.response?.data, null, 2));
|
|
}
|
|
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Types
|
|
export interface EventObjectiveRequest {
|
|
objective: string; // "신규 고객 유치", "재방문 유도", "매출 증대", "브랜드 인지도 향상"
|
|
}
|
|
|
|
export interface EventCreatedResponse {
|
|
eventId: string;
|
|
status: 'DRAFT' | 'PUBLISHED' | 'ENDED';
|
|
objective: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface AiRecommendationRequest {
|
|
objective: string;
|
|
storeInfo: {
|
|
storeId: string;
|
|
storeName: string;
|
|
category: string;
|
|
description?: string;
|
|
};
|
|
region?: string;
|
|
targetAudience?: string;
|
|
budget?: number;
|
|
}
|
|
|
|
export interface JobAcceptedResponse {
|
|
jobId: string;
|
|
status: 'PENDING';
|
|
message: string;
|
|
}
|
|
|
|
export interface EventJobStatusResponse {
|
|
jobId: string;
|
|
jobType: 'AI_RECOMMENDATION' | 'IMAGE_GENERATION';
|
|
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
|
|
progress: number;
|
|
resultKey?: string;
|
|
errorMessage?: string;
|
|
createdAt: string;
|
|
completedAt?: string;
|
|
}
|
|
|
|
export interface TrendKeyword {
|
|
keyword: string;
|
|
relevance: number;
|
|
description: string;
|
|
}
|
|
|
|
export interface TrendAnalysis {
|
|
industryTrends: TrendKeyword[];
|
|
regionalTrends: TrendKeyword[];
|
|
seasonalTrends: TrendKeyword[];
|
|
}
|
|
|
|
export interface ExpectedMetrics {
|
|
newCustomers: {
|
|
min: number;
|
|
max: number;
|
|
};
|
|
repeatVisits?: {
|
|
min: number;
|
|
max: number;
|
|
};
|
|
revenueIncrease: {
|
|
min: number;
|
|
max: number;
|
|
};
|
|
roi: {
|
|
min: number;
|
|
max: number;
|
|
};
|
|
socialEngagement?: {
|
|
estimatedPosts: number;
|
|
estimatedReach: number;
|
|
};
|
|
}
|
|
|
|
export interface EventRecommendation {
|
|
optionNumber: number;
|
|
concept: string;
|
|
title: string;
|
|
description: string;
|
|
targetAudience: string;
|
|
duration: {
|
|
recommendedDays: number;
|
|
recommendedPeriod?: string;
|
|
};
|
|
mechanics: {
|
|
type: 'DISCOUNT' | 'GIFT' | 'STAMP' | 'EXPERIENCE' | 'LOTTERY' | 'COMBO';
|
|
details: string;
|
|
};
|
|
promotionChannels: string[];
|
|
estimatedCost: {
|
|
min: number;
|
|
max: number;
|
|
breakdown?: {
|
|
material?: number;
|
|
promotion?: number;
|
|
discount?: number;
|
|
};
|
|
};
|
|
expectedMetrics: ExpectedMetrics;
|
|
differentiator: string;
|
|
}
|
|
|
|
export interface AiRecommendationResult {
|
|
eventId: string;
|
|
trendAnalysis: TrendAnalysis;
|
|
recommendations: EventRecommendation[];
|
|
generatedAt: string;
|
|
expiresAt?: string;
|
|
aiProvider: 'CLAUDE' | 'GPT4';
|
|
}
|
|
|
|
export interface SelectRecommendationRequest {
|
|
recommendationId: string;
|
|
customizations?: {
|
|
eventName?: string;
|
|
description?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
discountRate?: number;
|
|
};
|
|
}
|
|
|
|
export interface ImageGenerationRequest {
|
|
eventInfo: {
|
|
eventName: string;
|
|
description: string;
|
|
promotionType: string;
|
|
};
|
|
imageCount?: number;
|
|
}
|
|
|
|
export interface SelectChannelsRequest {
|
|
channels: ('WEBSITE' | 'KAKAO' | 'INSTAGRAM' | 'FACEBOOK' | 'NAVER_BLOG')[];
|
|
}
|
|
|
|
export interface ChannelDistributionResult {
|
|
channel: string;
|
|
success: boolean;
|
|
url?: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface EventPublishedResponse {
|
|
eventId: string;
|
|
status: 'PUBLISHED';
|
|
publishedAt: string;
|
|
channels: string[];
|
|
distributionResults: ChannelDistributionResult[];
|
|
}
|
|
|
|
export interface EventSummary {
|
|
eventId: string;
|
|
eventName: string;
|
|
objective: string;
|
|
status: 'DRAFT' | 'PUBLISHED' | 'ENDED';
|
|
startDate: string;
|
|
endDate: string;
|
|
thumbnailUrl?: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface PageInfo {
|
|
page: number;
|
|
size: number;
|
|
totalElements: number;
|
|
totalPages: number;
|
|
}
|
|
|
|
export interface EventListResponse {
|
|
content: EventSummary[];
|
|
page: PageInfo;
|
|
}
|
|
|
|
export interface GeneratedImage {
|
|
imageId: string;
|
|
imageUrl: string;
|
|
isSelected: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export interface AiRecommendation {
|
|
recommendationId: string;
|
|
eventName: string;
|
|
description: string;
|
|
promotionType: string;
|
|
targetAudience: string;
|
|
isSelected: boolean;
|
|
}
|
|
|
|
export interface EventDetailResponse {
|
|
eventId: string;
|
|
userId: string;
|
|
storeId: string;
|
|
eventName: string;
|
|
objective: string;
|
|
description: string;
|
|
targetAudience: string;
|
|
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;
|
|
}
|
|
|
|
export interface UpdateEventRequest {
|
|
eventName?: string;
|
|
description?: string;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
discountRate?: number;
|
|
}
|
|
|
|
export interface EndEventRequest {
|
|
reason: string;
|
|
}
|
|
|
|
// API Functions
|
|
export const eventApi = {
|
|
// Step 1: 목적 선택 및 이벤트 생성
|
|
selectObjective: async (objective: string): Promise<EventCreatedResponse> => {
|
|
const response = await eventApiClient.post<EventCreatedResponse>('/events/objectives', {
|
|
objective,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// Step 2: AI 추천 요청 (POST)
|
|
requestAiRecommendations: async (
|
|
eventId: string,
|
|
request: AiRecommendationRequest
|
|
): Promise<JobAcceptedResponse> => {
|
|
const response = await eventApiClient.post<JobAcceptedResponse>(
|
|
`/events/${eventId}/ai-recommendations`,
|
|
request
|
|
);
|
|
console.log('✅ AI 추천 요청 성공:', response.data);
|
|
return response.data;
|
|
},
|
|
|
|
// AI 추천 결과 조회 (GET)
|
|
getAiRecommendations: async (eventId: string): Promise<AiRecommendationResult> => {
|
|
const response = await eventApiClient.get<{ success: boolean; data: AiRecommendationResult; timestamp: string }>(
|
|
`/events/${eventId}/ai-recommendations`
|
|
);
|
|
console.log('✅ AI 추천 결과 조회 (전체 응답):', response.data);
|
|
console.log('✅ AI 추천 데이터:', response.data.data);
|
|
return response.data.data; // 래퍼에서 실제 데이터 추출
|
|
},
|
|
|
|
// AI 추천 선택
|
|
selectRecommendation: async (
|
|
eventId: string,
|
|
request: SelectRecommendationRequest
|
|
): Promise<EventDetailResponse> => {
|
|
const response = await eventApiClient.put<EventDetailResponse>(
|
|
`/events/${eventId}/recommendations`,
|
|
request
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
// Step 3: 이미지 생성 요청
|
|
requestImageGeneration: async (
|
|
eventId: string,
|
|
request: ImageGenerationRequest
|
|
): Promise<JobAcceptedResponse> => {
|
|
const response = await eventApiClient.post<JobAcceptedResponse>(`/events/${eventId}/images`, request);
|
|
return response.data;
|
|
},
|
|
|
|
// 이미지 선택
|
|
selectImage: async (eventId: string, imageId: string): Promise<EventDetailResponse> => {
|
|
const response = await eventApiClient.put<EventDetailResponse>(
|
|
`/events/${eventId}/images/${imageId}/select`
|
|
);
|
|
return response.data;
|
|
},
|
|
|
|
// Step 4: 이미지 편집
|
|
editImage: async (
|
|
eventId: string,
|
|
imageId: string,
|
|
editRequest: any
|
|
): Promise<{ imageId: string; imageUrl: string; editedAt: string }> => {
|
|
const response = await eventApiClient.put(`/events/${eventId}/images/${imageId}/edit`, editRequest);
|
|
return response.data;
|
|
},
|
|
|
|
// Step 5: 배포 채널 선택
|
|
selectChannels: async (eventId: string, channels: string[]): Promise<EventDetailResponse> => {
|
|
const response = await eventApiClient.put<EventDetailResponse>(`/events/${eventId}/channels`, {
|
|
channels,
|
|
});
|
|
return response.data;
|
|
},
|
|
|
|
// Step 6: 최종 배포
|
|
publishEvent: async (eventId: string): Promise<EventPublishedResponse> => {
|
|
const response = await eventApiClient.post<EventPublishedResponse>(`/events/${eventId}/publish`);
|
|
return response.data;
|
|
},
|
|
|
|
// 이벤트 목록 조회
|
|
getEvents: async (params?: {
|
|
status?: 'DRAFT' | 'PUBLISHED' | 'ENDED';
|
|
objective?: string;
|
|
search?: string;
|
|
page?: number;
|
|
size?: number;
|
|
sort?: string;
|
|
order?: 'asc' | 'desc';
|
|
}): Promise<EventListResponse> => {
|
|
const response = await eventApiClient.get<EventListResponse>('/events', { params });
|
|
return response.data;
|
|
},
|
|
|
|
// 이벤트 상세 조회
|
|
getEventDetail: async (eventId: string): Promise<EventDetailResponse> => {
|
|
const response = await eventApiClient.get<EventDetailResponse>(`/events/${eventId}`);
|
|
return response.data;
|
|
},
|
|
|
|
// 이벤트 수정
|
|
updateEvent: async (eventId: string, request: UpdateEventRequest): Promise<EventDetailResponse> => {
|
|
const response = await eventApiClient.put<EventDetailResponse>(`/events/${eventId}`, request);
|
|
return response.data;
|
|
},
|
|
|
|
// 이벤트 삭제
|
|
deleteEvent: async (eventId: string): Promise<void> => {
|
|
await eventApiClient.delete(`/events/${eventId}`);
|
|
},
|
|
|
|
// 이벤트 조기 종료
|
|
endEvent: async (eventId: string, reason: string): Promise<EventDetailResponse> => {
|
|
const response = await eventApiClient.post<EventDetailResponse>(`/events/${eventId}/end`, {
|
|
reason,
|
|
});
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
export default eventApi;
|