2025-10-31 14:29:44 +09:00

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;