mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 09:36:23 +00:00
feature/event 브랜치를 develop에 병합
- 최신 변경사항으로 충돌 해결 - RecommendationStep, eventApi, aiApi, eventApi 업데이트 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
e50cc86ece
@ -16,6 +16,7 @@ export type EventMethod = 'online' | 'offline';
|
|||||||
|
|
||||||
export interface EventData {
|
export interface EventData {
|
||||||
eventDraftId?: number;
|
eventDraftId?: number;
|
||||||
|
eventId?: string;
|
||||||
objective?: EventObjective;
|
objective?: EventObjective;
|
||||||
recommendation?: {
|
recommendation?: {
|
||||||
recommendation: {
|
recommendation: {
|
||||||
@ -95,13 +96,14 @@ export default function EventCreatePage() {
|
|||||||
<funnel.Render
|
<funnel.Render
|
||||||
objective={({ history }) => (
|
objective={({ history }) => (
|
||||||
<ObjectiveStep
|
<ObjectiveStep
|
||||||
onNext={(objective) => {
|
onNext={({ objective, eventId }) => {
|
||||||
history.push('recommendation', { objective });
|
history.push('recommendation', { objective, eventId });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
recommendation={({ context, history }) => (
|
recommendation={({ context, history }) => (
|
||||||
<RecommendationStep
|
<RecommendationStep
|
||||||
|
eventId={context.eventId}
|
||||||
objective={context.objective}
|
objective={context.objective}
|
||||||
onNext={(recommendation) => {
|
onNext={(recommendation) => {
|
||||||
history.push('channel', { ...context, recommendation });
|
history.push('channel', { ...context, recommendation });
|
||||||
|
|||||||
@ -67,15 +67,44 @@ const objectives: ObjectiveOption[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface ObjectiveStepProps {
|
interface ObjectiveStepProps {
|
||||||
onNext: (objective: EventObjective) => void;
|
onNext: (data: { objective: EventObjective; eventId: string }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eventId 생성 함수
|
||||||
|
const generateEventId = () => {
|
||||||
|
return `evt_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 쿠키 저장 함수
|
||||||
|
const setCookie = (name: string, value: string, days: number = 1) => {
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 쿠키 삭제 함수
|
||||||
|
const deleteCookie = (name: string) => {
|
||||||
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||||
const [selected, setSelected] = useState<EventObjective | null>(null);
|
const [selected, setSelected] = useState<EventObjective | null>(null);
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
onNext(selected);
|
// 이전 쿠키 삭제 (깨끗한 상태에서 시작)
|
||||||
|
deleteCookie('eventId');
|
||||||
|
deleteCookie('jobId');
|
||||||
|
|
||||||
|
// 새로운 eventId 생성
|
||||||
|
const eventId = generateEventId();
|
||||||
|
console.log('✅ 새로운 eventId 생성:', eventId);
|
||||||
|
|
||||||
|
// 쿠키에 저장
|
||||||
|
setCookie('eventId', eventId, 1); // 1일 동안 유지
|
||||||
|
|
||||||
|
// objective와 eventId를 함께 전달
|
||||||
|
onNext({ objective: selected, eventId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
Container,
|
||||||
@ -19,7 +19,6 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ArrowBack, Edit, Insights } from '@mui/icons-material';
|
import { ArrowBack, Edit, Insights } from '@mui/icons-material';
|
||||||
import { EventObjective, BudgetLevel, EventMethod } from '../page';
|
|
||||||
import { aiApi, eventApi, AIRecommendationResult, EventRecommendation } from '@/shared/api';
|
import { aiApi, eventApi, AIRecommendationResult, EventRecommendation } from '@/shared/api';
|
||||||
|
|
||||||
// 디자인 시스템 색상
|
// 디자인 시스템 색상
|
||||||
@ -41,8 +40,8 @@ const colors = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RecommendationStepProps {
|
interface RecommendationStepProps {
|
||||||
objective?: EventObjective;
|
|
||||||
eventId?: string; // 이전 단계에서 생성된 eventId
|
eventId?: string; // 이전 단계에서 생성된 eventId
|
||||||
|
objective?: string; // 이전 단계에서 선택된 objective
|
||||||
onNext: (data: {
|
onNext: (data: {
|
||||||
recommendation: EventRecommendation;
|
recommendation: EventRecommendation;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
@ -50,9 +49,30 @@ interface RecommendationStepProps {
|
|||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 쿠키에서 값 가져오기
|
||||||
|
const getCookie = (name: string): string | null => {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${name}=`);
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return parts.pop()?.split(';').shift() || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 쿠키 저장 함수
|
||||||
|
const setCookie = (name: string, value: string, days: number = 1) => {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
|
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
|
||||||
|
};
|
||||||
|
|
||||||
export default function RecommendationStep({
|
export default function RecommendationStep({
|
||||||
objective,
|
|
||||||
eventId: initialEventId,
|
eventId: initialEventId,
|
||||||
|
objective,
|
||||||
onNext,
|
onNext,
|
||||||
onBack
|
onBack
|
||||||
}: RecommendationStepProps) {
|
}: RecommendationStepProps) {
|
||||||
@ -66,56 +86,94 @@ export default function RecommendationStep({
|
|||||||
const [selected, setSelected] = useState<number | null>(null);
|
const [selected, setSelected] = useState<number | null>(null);
|
||||||
const [editedData, setEditedData] = useState<Record<number, { title: string; description: string }>>({});
|
const [editedData, setEditedData] = useState<Record<number, { title: string; description: string }>>({});
|
||||||
|
|
||||||
|
// 중복 호출 방지를 위한 ref
|
||||||
|
const requestedEventIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// 컴포넌트 마운트 시 AI 추천 요청
|
// 컴포넌트 마운트 시 AI 추천 요청
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!eventId && objective) {
|
// props에서만 eventId를 받음 (쿠키 사용 안 함)
|
||||||
// Step 1: 이벤트 생성
|
if (initialEventId) {
|
||||||
createEventAndRequestAI();
|
// 이미 요청한 eventId면 중복 요청하지 않음
|
||||||
} else if (eventId) {
|
if (requestedEventIdRef.current === initialEventId) {
|
||||||
// 이미 eventId가 있으면 AI 추천 요청
|
console.log('⚠️ 이미 요청한 eventId입니다. 중복 요청 방지:', initialEventId);
|
||||||
requestAIRecommendations(eventId);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedEventIdRef.current = initialEventId;
|
||||||
|
setEventId(initialEventId);
|
||||||
|
console.log('✅ RecommendationStep - eventId 설정:', initialEventId);
|
||||||
|
// eventId가 있으면 바로 AI 추천 요청
|
||||||
|
requestAIRecommendations(initialEventId);
|
||||||
|
} else {
|
||||||
|
console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.');
|
||||||
|
setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.');
|
||||||
}
|
}
|
||||||
}, []);
|
}, [initialEventId]);
|
||||||
|
|
||||||
const createEventAndRequestAI = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Step 1: 이벤트 목적 선택 및 생성
|
|
||||||
const eventResponse = await eventApi.selectObjective(objective || '신규 고객 유치');
|
|
||||||
const newEventId = eventResponse.eventId;
|
|
||||||
setEventId(newEventId);
|
|
||||||
|
|
||||||
// Step 2: AI 추천 요청
|
|
||||||
await requestAIRecommendations(newEventId);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('이벤트 생성 실패:', err);
|
|
||||||
setError(err.response?.data?.message || err.message || '이벤트 생성에 실패했습니다');
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestAIRecommendations = async (evtId: string) => {
|
const requestAIRecommendations = async (evtId: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// 사용자 정보에서 매장 정보 가져오기
|
// 로그인한 사용자 정보에서 매장 정보 가져오기
|
||||||
const userProfile = JSON.parse(localStorage.getItem('userProfile') || '{}');
|
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
||||||
const storeInfo = {
|
console.log('📋 localStorage user:', user);
|
||||||
storeId: userProfile.storeId || '1',
|
|
||||||
storeName: userProfile.storeName || '내 매장',
|
// UUID v4 생성 함수 (테스트용)
|
||||||
category: userProfile.industry || '음식점',
|
const generateUUID = () => {
|
||||||
description: userProfile.businessHours || '',
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// AI 추천 요청
|
// storeId: 로그인한 경우 user.storeId 사용, 아니면 테스트용 UUID 생성
|
||||||
const jobResponse = await eventApi.requestAiRecommendations(evtId, storeInfo);
|
const storeInfo = {
|
||||||
setJobId(jobResponse.jobId);
|
storeId: user.storeId ? String(user.storeId) : generateUUID(),
|
||||||
|
storeName: user.storeName || '테스트 매장',
|
||||||
|
category: user.industry || '음식점',
|
||||||
|
description: user.businessHours || '테스트 설명',
|
||||||
|
};
|
||||||
|
|
||||||
// Job 폴링 시작
|
console.log('📤 전송할 storeInfo:', storeInfo);
|
||||||
pollJobStatus(jobResponse.jobId, evtId);
|
console.log('🎯 eventId:', evtId);
|
||||||
|
console.log('🎯 objective:', objective || '테스트 목적');
|
||||||
|
|
||||||
|
// AI 추천 요청
|
||||||
|
const jobResponse = await eventApi.requestAiRecommendations(
|
||||||
|
evtId,
|
||||||
|
objective || '테스트 목적',
|
||||||
|
storeInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('📦 백엔드 응답 (전체):', JSON.stringify(jobResponse, null, 2));
|
||||||
|
|
||||||
|
// 백엔드 응답 구조 확인: { success, data: { jobId, status, message }, timestamp }
|
||||||
|
const actualJobId = (jobResponse as any).data?.jobId || jobResponse.jobId;
|
||||||
|
|
||||||
|
console.log('📦 jobResponse.data?.jobId:', (jobResponse as any).data?.jobId);
|
||||||
|
console.log('📦 jobResponse.jobId:', jobResponse.jobId);
|
||||||
|
console.log('📦 실제 사용할 jobId:', actualJobId);
|
||||||
|
|
||||||
|
if (!actualJobId) {
|
||||||
|
console.error('❌ 백엔드에서 jobId를 반환하지 않았습니다!');
|
||||||
|
console.error('📦 응답 구조:', JSON.stringify(jobResponse, null, 2));
|
||||||
|
setError('백엔드에서 Job ID를 받지 못했습니다. 백엔드 응답을 확인해주세요.');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// jobId를 쿠키에 저장
|
||||||
|
setCookie('jobId', actualJobId, 1);
|
||||||
|
setJobId(actualJobId);
|
||||||
|
console.log('✅ AI 추천 Job 생성 완료, jobId:', actualJobId);
|
||||||
|
console.log('🍪 jobId를 쿠키에 저장:', actualJobId);
|
||||||
|
|
||||||
|
// Job 폴링 시작 (2초 후 시작하여 백엔드에서 Job 저장 시간 확보)
|
||||||
|
setTimeout(() => {
|
||||||
|
pollJobStatus(actualJobId, evtId);
|
||||||
|
}, 2000);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('AI 추천 요청 실패:', err);
|
console.error('AI 추천 요청 실패:', err);
|
||||||
setError(err.response?.data?.message || 'AI 추천 요청에 실패했습니다');
|
setError(err.response?.data?.message || 'AI 추천 요청에 실패했습니다');
|
||||||
@ -130,8 +188,22 @@ export default function RecommendationStep({
|
|||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
const status = await eventApi.getJobStatus(jId);
|
// jobId 확인: 파라미터 우선, 없으면 쿠키에서 읽기
|
||||||
console.log('Job 상태:', status);
|
const currentJobId = jId || getCookie('jobId');
|
||||||
|
|
||||||
|
if (!currentJobId) {
|
||||||
|
console.error('❌ jobId를 찾을 수 없습니다 (파라미터와 쿠키 모두 없음)');
|
||||||
|
setError('jobId를 찾을 수 없습니다');
|
||||||
|
setLoading(false);
|
||||||
|
setPolling(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔄 Job 상태 조회 시도 (${attempts + 1}/${maxAttempts}), jobId: ${currentJobId}`);
|
||||||
|
console.log(`🍪 jobId 출처: ${jId ? '파라미터' : '쿠키'}`);
|
||||||
|
|
||||||
|
const status = await eventApi.getJobStatus(currentJobId);
|
||||||
|
console.log('✅ Job 상태:', status);
|
||||||
|
|
||||||
if (status.status === 'COMPLETED') {
|
if (status.status === 'COMPLETED') {
|
||||||
// AI 추천 결과 조회
|
// AI 추천 결과 조회
|
||||||
@ -157,7 +229,23 @@ export default function RecommendationStep({
|
|||||||
setPolling(false);
|
setPolling(false);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Job 상태 조회 실패:', err);
|
console.error('❌ Job 상태 조회 실패:', err);
|
||||||
|
|
||||||
|
// Job을 찾을 수 없는 경우 (404 또는 JOB_001) - 초기 몇 번은 재시도
|
||||||
|
if (err.response?.data?.errorCode === 'JOB_001' || err.response?.status === 404) {
|
||||||
|
attempts++;
|
||||||
|
if (attempts < 5) { // 처음 5번 시도는 Job 생성 대기
|
||||||
|
console.log(`⏳ Job이 아직 준비되지 않음. ${attempts}/5 재시도 예정...`);
|
||||||
|
setTimeout(poll, 3000); // 3초 후 재시도
|
||||||
|
return;
|
||||||
|
} else if (attempts < maxAttempts) {
|
||||||
|
console.log(`⏳ Job 폴링 계속... ${attempts}/${maxAttempts}`);
|
||||||
|
setTimeout(poll, 5000); // 5초 후 재시도
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다른 에러이거나 재시도 횟수 초과
|
||||||
setError(err.response?.data?.message || 'Job 상태 조회에 실패했습니다');
|
setError(err.response?.data?.message || 'Job 상태 조회에 실패했습니다');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPolling(false);
|
setPolling(false);
|
||||||
@ -289,10 +377,13 @@ export default function RecommendationStep({
|
|||||||
size="large"
|
size="large"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setError(null);
|
setError(null);
|
||||||
if (eventId) {
|
// props에서 eventId가 없으면 쿠키에서 읽어오기
|
||||||
requestAIRecommendations(eventId);
|
const evtId = initialEventId || getCookie('eventId');
|
||||||
|
|
||||||
|
if (evtId) {
|
||||||
|
requestAIRecommendations(evtId);
|
||||||
} else {
|
} else {
|
||||||
createEventAndRequestAI();
|
setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@ -23,17 +23,24 @@ import type {
|
|||||||
*
|
*
|
||||||
* 현재는 apiClient를 사용하되, baseURL을 오버라이드합니다.
|
* 현재는 apiClient를 사용하되, baseURL을 오버라이드합니다.
|
||||||
*/
|
*/
|
||||||
|
const EVENT_API_BASE = '/api/v1/events';
|
||||||
|
const EVENT_HOST = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Service용 API 클라이언트
|
||||||
|
* Event Service는 별도 포트(8080)에서 실행되므로 별도 클라이언트 생성
|
||||||
|
*/
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const EVENT_API_BASE = '/api/v1/events';
|
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1';
|
||||||
const API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
|
||||||
|
|
||||||
const eventApiClient = axios.create({
|
const eventApiClient = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: `${EVENT_HOST}/api/${API_VERSION}`,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
withCredentials: false, // CORS 설정
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor - JWT 토큰 추가
|
// Request interceptor - JWT 토큰 추가
|
||||||
|
|||||||
@ -57,14 +57,25 @@ export const useAuth = () => {
|
|||||||
try {
|
try {
|
||||||
const response = await userApi.login(credentials);
|
const response = await userApi.login(credentials);
|
||||||
|
|
||||||
|
// 토큰을 먼저 저장 (프로필 조회에 필요)
|
||||||
|
localStorage.setItem(TOKEN_KEY, response.token);
|
||||||
|
|
||||||
|
// 프로필 조회하여 storeId 포함한 전체 정보 가져오기
|
||||||
|
const profile = await userApi.getProfile();
|
||||||
|
|
||||||
const user: User = {
|
const user: User = {
|
||||||
userId: response.userId,
|
userId: profile.userId,
|
||||||
userName: response.userName,
|
userName: profile.userName,
|
||||||
email: response.email,
|
email: profile.email,
|
||||||
role: response.role,
|
role: profile.role,
|
||||||
|
phoneNumber: profile.phoneNumber,
|
||||||
|
storeId: profile.storeId,
|
||||||
|
storeName: profile.storeName,
|
||||||
|
industry: profile.industry,
|
||||||
|
address: profile.address,
|
||||||
|
businessHours: profile.businessHours,
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem(TOKEN_KEY, response.token);
|
|
||||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||||
|
|
||||||
setAuthState({
|
setAuthState({
|
||||||
@ -76,6 +87,8 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
return { success: true, user };
|
return { success: true, user };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 로그인 실패 시 저장된 토큰 삭제
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : '로그인에 실패했습니다.',
|
error: error instanceof Error ? error.message : '로그인에 실패했습니다.',
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
// AI Service API 클라이언트
|
// AI Service API 클라이언트
|
||||||
const AI_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
const AI_API_BASE_URL = process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083';
|
||||||
|
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1';
|
||||||
|
|
||||||
export const aiApiClient: AxiosInstance = axios.create({
|
export const aiApiClient: AxiosInstance = axios.create({
|
||||||
baseURL: AI_API_BASE_URL,
|
baseURL: `${AI_API_BASE_URL}/api/${API_VERSION}`,
|
||||||
timeout: 300000, // AI 생성은 최대 5분
|
timeout: 300000, // AI 생성은 최대 5분
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
withCredentials: false, // CORS 설정
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor
|
// Request interceptor
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
// Event Service API 클라이언트
|
// Event Service API 클라이언트
|
||||||
const EVENT_API_BASE_URL = 'http://kt-event-marketing-api.20.214.196.128.nip.io';
|
const EVENT_API_BASE_URL = process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080';
|
||||||
const API_VERSION = 'v1';
|
const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1';
|
||||||
const BASE_URL = `${EVENT_API_BASE_URL}/api/${API_VERSION}`;
|
|
||||||
|
|
||||||
export const eventApiClient: AxiosInstance = axios.create({
|
export const eventApiClient: AxiosInstance = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: `${EVENT_API_BASE_URL}/api/${API_VERSION}`,
|
||||||
timeout: 30000, // Job 폴링 고려
|
timeout: 30000, // Job 폴링 고려
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
withCredentials: false, // CORS 설정
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor
|
// Request interceptor
|
||||||
@ -20,9 +20,15 @@ eventApiClient.interceptors.request.use(
|
|||||||
method: config.method?.toUpperCase(),
|
method: config.method?.toUpperCase(),
|
||||||
url: config.url,
|
url: config.url,
|
||||||
baseURL: config.baseURL,
|
baseURL: config.baseURL,
|
||||||
data: config.data,
|
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');
|
const token = localStorage.getItem('accessToken');
|
||||||
if (token && config.headers) {
|
if (token && config.headers) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
@ -50,8 +56,18 @@ eventApiClient.interceptors.response.use(
|
|||||||
message: error.message,
|
message: error.message,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
url: error.config?.url,
|
url: error.config?.url,
|
||||||
data: error.response?.data,
|
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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -69,6 +85,7 @@ export interface EventCreatedResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AiRecommendationRequest {
|
export interface AiRecommendationRequest {
|
||||||
|
objective: string;
|
||||||
storeInfo: {
|
storeInfo: {
|
||||||
storeId: string;
|
storeId: string;
|
||||||
storeName: string;
|
storeName: string;
|
||||||
@ -219,18 +236,29 @@ export const eventApi = {
|
|||||||
// Step 2: AI 추천 요청
|
// Step 2: AI 추천 요청
|
||||||
requestAiRecommendations: async (
|
requestAiRecommendations: async (
|
||||||
eventId: string,
|
eventId: string,
|
||||||
|
objective: string,
|
||||||
storeInfo: AiRecommendationRequest['storeInfo']
|
storeInfo: AiRecommendationRequest['storeInfo']
|
||||||
): Promise<JobAcceptedResponse> => {
|
): Promise<JobAcceptedResponse> => {
|
||||||
const response = await eventApiClient.post<JobAcceptedResponse>(
|
const response = await eventApiClient.post<JobAcceptedResponse>(
|
||||||
`/events/${eventId}/ai-recommendations`,
|
`/events/${eventId}/ai-recommendations`,
|
||||||
{ storeInfo }
|
{ objective, storeInfo }
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Job 상태 폴링
|
// Job 상태 폴링
|
||||||
getJobStatus: async (jobId: string): Promise<EventJobStatusResponse> => {
|
getJobStatus: async (jobId: string): Promise<EventJobStatusResponse> => {
|
||||||
const response = await eventApiClient.get<EventJobStatusResponse>(`/jobs/${jobId}`);
|
const response = await eventApiClient.get<any>(`/jobs/${jobId}`);
|
||||||
|
// 백엔드 응답 구조: { success, data: { jobId, jobType, status, ... }, timestamp }
|
||||||
|
console.log('📦 getJobStatus 원본 응답:', response.data);
|
||||||
|
|
||||||
|
// data 안에 실제 job 정보가 있는지 확인
|
||||||
|
if (response.data.data) {
|
||||||
|
console.log('✅ response.data.data 사용');
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ response.data 직접 사용');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user