'use client'; import { useState, useEffect, useRef } from 'react'; import { Box, Container, Typography, Card, CardContent, Button, Grid, Chip, TextField, Radio, RadioGroup, FormControlLabel, IconButton, CircularProgress, Alert, } from '@mui/material'; import { ArrowBack, Edit, Insights } from '@mui/icons-material'; import { eventApi } from '@/shared/api'; import type { AiRecommendationResult, EventRecommendation } from '@/shared/api/eventApi'; // 디자인 시스템 색상 const colors = { pink: '#F472B6', purple: '#C084FC', purpleLight: '#E9D5FF', blue: '#60A5FA', mint: '#34D399', orange: '#FB923C', yellow: '#FBBF24', gray: { 900: '#1A1A1A', 700: '#4A4A4A', 500: '#9E9E9E', 300: '#D9D9D9', 100: '#F5F5F5', }, }; interface RecommendationStepProps { eventId?: string; // 이전 단계에서 생성된 eventId objective?: string; // 이전 단계에서 선택된 objective onNext: (data: { recommendation: EventRecommendation; eventId: string; }) => 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({ eventId: initialEventId, objective, onNext, onBack }: RecommendationStepProps) { const [eventId, setEventId] = useState(initialEventId || null); const [jobId, setJobId] = useState(null); const [loading, setLoading] = useState(false); const [polling, setPolling] = useState(false); const [error, setError] = useState(null); const [aiResult, setAiResult] = useState(null); const [selected, setSelected] = useState(null); const [editedData, setEditedData] = useState>({}); // 중복 호출 방지를 위한 ref const requestedEventIdRef = useRef(null); // 컴포넌트 마운트 시 AI 추천 요청 useEffect(() => { // props에서만 eventId를 받음 (쿠키 사용 안 함) if (initialEventId) { // 이미 요청한 eventId면 중복 요청하지 않음 if (requestedEventIdRef.current === initialEventId) { console.log('⚠️ 이미 요청한 eventId입니다. 중복 요청 방지:', initialEventId); return; } requestedEventIdRef.current = initialEventId; setEventId(initialEventId); console.log('✅ RecommendationStep - eventId 설정:', initialEventId); // eventId가 있으면 바로 AI 추천 요청 requestAIRecommendations(initialEventId); } else { console.error('❌ eventId가 없습니다. ObjectiveStep으로 돌아가세요.'); setError('이벤트 ID가 없습니다. 이전 단계로 돌아가서 다시 시도하세요.'); } }, [initialEventId]); const requestAIRecommendations = async (evtId: string) => { try { setLoading(true); setError(null); // 로그인한 사용자 정보에서 매장 정보 가져오기 const user = JSON.parse(localStorage.getItem('user') || '{}'); console.log('📋 localStorage user:', user); // UUID v4 생성 함수 (테스트용) const generateUUID = () => { 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); }); }; // storeId: 로그인한 경우 user.storeId 사용, 아니면 테스트용 UUID 생성 const storeInfo = { storeId: user.storeId ? String(user.storeId) : generateUUID(), storeName: user.storeName || '테스트 매장', category: user.industry || '음식점', description: user.businessHours || '테스트 설명', }; console.log('📤 전송할 storeInfo:', storeInfo); 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) { console.error('AI 추천 요청 실패:', err); setError(err.response?.data?.message || 'AI 추천 요청에 실패했습니다'); setLoading(false); } }; const pollJobStatus = async (jId: string, evtId: string) => { setPolling(true); const maxAttempts = 60; // 최대 5분 (5초 간격) let attempts = 0; const poll = async () => { try { // jobId 확인: 파라미터 우선, 없으면 쿠키에서 읽기 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') { // AI 추천 결과 조회 (Event Service API 사용) const recommendations = await eventApi.getAiRecommendations(evtId); setAiResult(recommendations); setLoading(false); setPolling(false); return; } else if (status.status === 'FAILED') { setError(status.errorMessage || 'AI 추천 생성에 실패했습니다'); setLoading(false); setPolling(false); return; } // 계속 폴링 attempts++; if (attempts < maxAttempts) { setTimeout(poll, 5000); // 5초 후 재시도 } else { setError('AI 추천 생성 시간이 초과되었습니다'); setLoading(false); setPolling(false); } } catch (err: any) { 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 상태 조회에 실패했습니다'); setLoading(false); setPolling(false); } }; poll(); }; const handleNext = async () => { if (selected === null || !aiResult || !eventId) return; const selectedRec = aiResult.recommendations[selected - 1]; const edited = editedData[selected]; try { setLoading(true); // AI 추천 선택 API 호출 await eventApi.selectRecommendation(eventId, { recommendationId: `${eventId}-opt${selected}`, customizations: { eventName: edited?.title || selectedRec.title, description: edited?.description || selectedRec.description, }, }); // 다음 단계로 이동 onNext({ recommendation: { ...selectedRec, title: edited?.title || selectedRec.title, description: edited?.description || selectedRec.description, }, eventId, }); } catch (err: any) { console.error('추천 선택 실패:', err); setError(err.response?.data?.message || '추천 선택에 실패했습니다'); } finally { setLoading(false); } }; const handleEditTitle = (optionNumber: number, title: string) => { setEditedData((prev) => ({ ...prev, [optionNumber]: { ...prev[optionNumber], title }, })); }; const handleEditDescription = (optionNumber: number, description: string) => { setEditedData((prev) => ({ ...prev, [optionNumber]: { ...prev[optionNumber], description }, })); }; // 로딩 상태 표시 if (loading || polling) { return ( AI 이벤트 추천 AI가 최적의 이벤트를 생성하고 있습니다... 업종, 지역, 시즌 트렌드를 분석하여 맞춤형 이벤트를 추천합니다 ); } // 에러 상태 표시 if (error) { return ( AI 이벤트 추천 {error} ); } // AI 결과가 없으면 로딩 표시 if (!aiResult) { return ( ); } return ( {/* Header */} AI 이벤트 추천 {/* Trends Analysis */} AI 트렌드 분석 📍 업종 트렌드 {aiResult.trendAnalysis.industryTrends.slice(0, 3).map((trend, idx) => ( • {trend.description} ))} 🗺️ 지역 트렌드 {aiResult.trendAnalysis.regionalTrends.slice(0, 3).map((trend, idx) => ( • {trend.description} ))} ☀️ 시즌 트렌드 {aiResult.trendAnalysis.seasonalTrends.slice(0, 3).map((trend, idx) => ( • {trend.description} ))} {/* AI Recommendations */} AI 추천 이벤트 ({aiResult.recommendations.length}가지 옵션) 각 옵션은 차별화된 컨셉으로 구성되어 있습니다. 원하시는 옵션을 선택하고 수정할 수 있습니다. {/* Recommendations */} setSelected(Number(e.target.value))}> {aiResult.recommendations.map((rec) => ( setSelected(rec.optionNumber)} > } label="" sx={{ m: 0 }} /> handleEditTitle(rec.optionNumber, e.target.value)} onClick={(e) => e.stopPropagation()} sx={{ mb: 4 }} InputProps={{ endAdornment: , sx: { fontSize: '1.1rem', fontWeight: 600, py: 2 }, }} /> handleEditDescription(rec.optionNumber, e.target.value)} onClick={(e) => e.stopPropagation()} sx={{ mb: 4 }} InputProps={{ sx: { fontSize: '1rem' }, }} /> 타겟 고객 {rec.targetAudience} 예상 비용 {(rec.estimatedCost.min / 10000).toFixed(0)}~{(rec.estimatedCost.max / 10000).toFixed(0)}만원 예상 신규 고객 {rec.expectedMetrics.newCustomers.min}~{rec.expectedMetrics.newCustomers.max}명 ROI {rec.expectedMetrics.roi.min}~{rec.expectedMetrics.roi.max}% 차별점 {rec.differentiator} ))} {/* Action Buttons */} ); }