469 lines
16 KiB
TypeScript
Executable File
469 lines
16 KiB
TypeScript
Executable File
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import Sidebar from '../../components/Sidebar';
|
|
import AssetManagementContent from './AssetManagementContent';
|
|
import SoundStudioContent from './SoundStudioContent';
|
|
import CompletionContent from './CompletionContent';
|
|
import DashboardContent from './DashboardContent';
|
|
import BusinessSettingsContent from './BusinessSettingsContent';
|
|
import UrlInputContent from './UrlInputContent';
|
|
import ADO2ContentsPage from './ADO2ContentsPage';
|
|
import MyInfoContent from './MyInfoContent';
|
|
import LoadingSection from '../Analysis/LoadingSection';
|
|
import AnalysisResultSection from '../Analysis/AnalysisResultSection';
|
|
import { ImageItem, CrawlingResponse, UserMeResponse } from '../../types/api';
|
|
import { crawlUrl, autocomplete, AutocompleteRequest, getUserMe, clearTokens } from '../../utils/api';
|
|
|
|
const WIZARD_STEP_KEY = 'castad_wizard_step';
|
|
const ACTIVE_ITEM_KEY = 'castad_active_item';
|
|
const SONG_TASK_ID_KEY = 'castad_song_task_id';
|
|
const IMAGE_TASK_ID_KEY = 'castad_image_task_id';
|
|
const ANALYSIS_DATA_KEY = 'castad_analysis_data';
|
|
|
|
// 다른 컴포넌트에서 사용하는 storage key들 (초기화용)
|
|
const SONG_GENERATION_KEY = 'castad_song_generation';
|
|
const VIDEO_GENERATION_KEY = 'castad_video_generation';
|
|
const VIDEO_COMPLETE_KEY = 'castad_video_complete'; // 완료된 영상 정보
|
|
|
|
// 모든 프로젝트 관련 localStorage 초기화
|
|
const clearAllProjectStorage = () => {
|
|
localStorage.removeItem(WIZARD_STEP_KEY);
|
|
localStorage.removeItem(SONG_TASK_ID_KEY);
|
|
localStorage.removeItem(IMAGE_TASK_ID_KEY);
|
|
localStorage.removeItem(SONG_GENERATION_KEY);
|
|
localStorage.removeItem(VIDEO_GENERATION_KEY);
|
|
localStorage.removeItem(VIDEO_COMPLETE_KEY);
|
|
};
|
|
|
|
interface BusinessInfo {
|
|
customer_name: string;
|
|
region: string;
|
|
detail_region_info: string;
|
|
}
|
|
|
|
interface GenerationFlowProps {
|
|
onHome: () => void;
|
|
initialActiveItem?: string;
|
|
initialImageList?: string[];
|
|
businessInfo?: BusinessInfo;
|
|
initialAnalysisData?: CrawlingResponse | null;
|
|
}
|
|
|
|
// 위저드 단계:
|
|
// -2: URL 입력
|
|
// -1: 로딩 (분석 중)
|
|
// 0: 브랜드 분석 결과
|
|
// 1: 에셋 관리 (Asset Management)
|
|
// 2: 사운드 스튜디오 (Sound Studio)
|
|
// 3: 완료 (Completion)
|
|
|
|
const GenerationFlow: React.FC<GenerationFlowProps> = ({
|
|
onHome,
|
|
initialActiveItem = '대시보드',
|
|
initialImageList = [],
|
|
businessInfo,
|
|
initialAnalysisData
|
|
}) => {
|
|
const { t, i18n } = useTranslation();
|
|
// localStorage에서 저장된 상태 복원
|
|
const savedActiveItem = localStorage.getItem(ACTIVE_ITEM_KEY);
|
|
const savedWizardStep = localStorage.getItem(WIZARD_STEP_KEY);
|
|
const savedSongTaskId = localStorage.getItem(SONG_TASK_ID_KEY);
|
|
const savedImageTaskId = localStorage.getItem(IMAGE_TASK_ID_KEY);
|
|
const savedAnalysisData = localStorage.getItem(ANALYSIS_DATA_KEY);
|
|
|
|
// 분석 데이터 파싱
|
|
const parseAnalysisData = (): CrawlingResponse | null => {
|
|
if (initialAnalysisData) return initialAnalysisData;
|
|
if (!savedAnalysisData) return null;
|
|
try {
|
|
return JSON.parse(savedAnalysisData) as CrawlingResponse;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const [activeItem, setActiveItem] = useState(savedActiveItem || initialActiveItem);
|
|
|
|
// 초기 위저드 단계 결정
|
|
const getInitialWizardStep = (): number => {
|
|
// 저장된 단계가 있으면 사용
|
|
if (savedWizardStep !== null) {
|
|
return parseInt(savedWizardStep, 10);
|
|
}
|
|
// 분석 데이터가 있으면 에셋 관리(1)부터, 없으면 URL 입력(-2)부터
|
|
const hasAnalysisData = initialAnalysisData || savedAnalysisData;
|
|
return hasAnalysisData ? 1 : -2;
|
|
};
|
|
|
|
const [wizardStep, setWizardStep] = useState(getInitialWizardStep());
|
|
const [songTaskId, setSongTaskId] = useState<string | null>(savedSongTaskId);
|
|
const [imageTaskId, setImageTaskId] = useState<string | null>(savedImageTaskId);
|
|
const [videoGenerationStatus, setVideoGenerationStatus] = useState<'idle' | 'generating' | 'complete' | 'error'>('idle');
|
|
const [videoGenerationProgress, setVideoGenerationProgress] = useState(0);
|
|
const [analysisData, setAnalysisData] = useState<CrawlingResponse | null>(parseAnalysisData());
|
|
const [analysisError, setAnalysisError] = useState<string | null>(null);
|
|
const [userInfo, setUserInfo] = useState<UserMeResponse | null>(null);
|
|
|
|
// 로그인 직후 사용자 정보 조회
|
|
useEffect(() => {
|
|
const fetchUserInfo = async () => {
|
|
try {
|
|
const data = await getUserMe();
|
|
setUserInfo(data);
|
|
} catch (error) {
|
|
console.error('Failed to fetch user info:', error);
|
|
}
|
|
};
|
|
fetchUserInfo();
|
|
}, []);
|
|
|
|
// 로그아웃 핸들러
|
|
const handleLogout = () => {
|
|
clearTokens();
|
|
localStorage.removeItem('castad_view_mode');
|
|
localStorage.removeItem('castad_analysis_data');
|
|
localStorage.removeItem(WIZARD_STEP_KEY);
|
|
localStorage.removeItem(ACTIVE_ITEM_KEY);
|
|
window.location.href = '/';
|
|
};
|
|
|
|
// 현재 비즈니스 정보 (분석 데이터에서 가져오거나 prop에서 가져옴)
|
|
const currentBusinessInfo = analysisData?.processed_info || businessInfo;
|
|
|
|
// URL 이미지를 ImageItem 형태로 변환하여 초기화
|
|
const getInitialImageList = (): ImageItem[] => {
|
|
if (analysisData?.image_list && analysisData.image_list.length > 0) {
|
|
return analysisData.image_list.map(url => ({ type: 'url', url }));
|
|
}
|
|
return initialImageList.map(url => ({ type: 'url', url }));
|
|
};
|
|
|
|
const [imageList, setImageList] = useState<ImageItem[]>(getInitialImageList());
|
|
|
|
// analysisData 변경 시 imageList 업데이트
|
|
useEffect(() => {
|
|
if (analysisData?.image_list && analysisData.image_list.length > 0) {
|
|
setImageList(analysisData.image_list.map(url => ({ type: 'url', url })));
|
|
}
|
|
}, [analysisData]);
|
|
|
|
const handleRemoveImage = (index: number) => {
|
|
setImageList(prev => {
|
|
const item = prev[index];
|
|
// 파일 이미지인 경우 메모리 해제
|
|
if (item.type === 'file') {
|
|
URL.revokeObjectURL(item.preview);
|
|
}
|
|
return prev.filter((_, i) => i !== index);
|
|
});
|
|
};
|
|
|
|
const handleAddImages = (files: File[]) => {
|
|
const newImages: ImageItem[] = files.map(file => ({
|
|
type: 'file',
|
|
file,
|
|
preview: URL.createObjectURL(file),
|
|
}));
|
|
// 새로 업로드된 이미지를 배열 앞에 추가 (최신 이미지가 상단에 표시)
|
|
setImageList(prev => [...newImages, ...prev]);
|
|
};
|
|
|
|
// 홈 버튼(로고) 클릭 시 모든 상태 초기화 후 홈으로 이동
|
|
const handleHome = () => {
|
|
clearAllProjectStorage();
|
|
localStorage.removeItem(ANALYSIS_DATA_KEY);
|
|
setWizardStep(-2);
|
|
setSongTaskId(null);
|
|
setImageTaskId(null);
|
|
setAnalysisData(null);
|
|
setImageList([]);
|
|
onHome();
|
|
};
|
|
|
|
// 위저드 단계 이동
|
|
const goToWizardStep = (step: number) => {
|
|
setWizardStep(step);
|
|
localStorage.setItem(WIZARD_STEP_KEY, step.toString());
|
|
};
|
|
|
|
// 업체명 자동완성으로 분석 시작
|
|
const handleAutocomplete = async (request: AutocompleteRequest) => {
|
|
goToWizardStep(-1); // 로딩 상태로
|
|
setAnalysisError(null);
|
|
|
|
try {
|
|
const data = await autocomplete(request);
|
|
|
|
// 기본값 보장
|
|
if (data.marketing_analysis) {
|
|
data.marketing_analysis.tags = data.marketing_analysis.tags || [];
|
|
data.marketing_analysis.facilities = data.marketing_analysis.facilities || [];
|
|
data.marketing_analysis.report = data.marketing_analysis.report || '';
|
|
}
|
|
if (data.processed_info) {
|
|
data.processed_info.customer_name = data.processed_info.customer_name || '알 수 없음';
|
|
data.processed_info.region = data.processed_info.region || '';
|
|
data.processed_info.detail_region_info = data.processed_info.detail_region_info || '';
|
|
}
|
|
data.image_list = data.image_list || [];
|
|
|
|
setAnalysisData(data);
|
|
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data));
|
|
goToWizardStep(0); // 브랜드 분석 결과로
|
|
} catch (err) {
|
|
console.error('Autocomplete error:', err);
|
|
setAnalysisError(err instanceof Error ? err.message : t('app.autocompleteError'));
|
|
goToWizardStep(-2); // URL 입력으로 돌아가기
|
|
}
|
|
};
|
|
|
|
// 테스트 데이터로 브랜드 분석 페이지 이동
|
|
const handleTestData = (data: CrawlingResponse) => {
|
|
const tagged = { ...data, _isTestData: true };
|
|
setAnalysisData(tagged);
|
|
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(tagged));
|
|
goToWizardStep(0); // 브랜드 분석 결과로
|
|
};
|
|
|
|
// 언어 변경 시 테스트 데이터 다시 로드
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem(ANALYSIS_DATA_KEY);
|
|
if (!saved) return;
|
|
try {
|
|
const parsed = JSON.parse(saved);
|
|
if (!parsed._isTestData) return;
|
|
const jsonFile = i18n.language === 'en' ? '/example_analysis_en.json' : '/example_analysis.json';
|
|
fetch(jsonFile)
|
|
.then(res => res.json())
|
|
.then((data: CrawlingResponse) => {
|
|
const tagged = { ...data, _isTestData: true };
|
|
setAnalysisData(tagged);
|
|
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(tagged));
|
|
})
|
|
.catch(err => console.error('Failed to reload test data:', err));
|
|
} catch { /* ignore */ }
|
|
}, [i18n.language]);
|
|
|
|
// URL 분석 시작
|
|
const handleStartAnalysis = async (url: string) => {
|
|
if (!url.trim()) return;
|
|
|
|
goToWizardStep(-1); // 로딩 상태로
|
|
setAnalysisError(null);
|
|
|
|
try {
|
|
const data = await crawlUrl(url);
|
|
|
|
// 기본값 보장
|
|
if (data.marketing_analysis) {
|
|
data.marketing_analysis.tags = data.marketing_analysis.tags || [];
|
|
data.marketing_analysis.facilities = data.marketing_analysis.facilities || [];
|
|
data.marketing_analysis.report = data.marketing_analysis.report || '';
|
|
}
|
|
if (data.processed_info) {
|
|
data.processed_info.customer_name = data.processed_info.customer_name || '알 수 없음';
|
|
data.processed_info.region = data.processed_info.region || '';
|
|
data.processed_info.detail_region_info = data.processed_info.detail_region_info || '';
|
|
}
|
|
data.image_list = data.image_list || [];
|
|
|
|
setAnalysisData(data);
|
|
localStorage.setItem(ANALYSIS_DATA_KEY, JSON.stringify(data));
|
|
goToWizardStep(0); // 브랜드 분석 결과로
|
|
} catch (err) {
|
|
console.error('Crawling failed:', err);
|
|
const errorMessage = err instanceof Error ? err.message : t('app.analysisError');
|
|
setAnalysisError(errorMessage);
|
|
goToWizardStep(-2); // URL 입력으로 돌아가기
|
|
}
|
|
};
|
|
|
|
// 브랜드 분석에서 콘텐츠 생성 클릭
|
|
const handleAnalysisGenerate = () => {
|
|
goToWizardStep(1); // 에셋 관리로
|
|
};
|
|
|
|
// 브랜드 분석에서 뒤로가기
|
|
const handleAnalysisBack = () => {
|
|
goToWizardStep(-2); // URL 입력으로
|
|
};
|
|
|
|
// activeItem 변경 시 localStorage에 저장
|
|
useEffect(() => {
|
|
localStorage.setItem(ACTIVE_ITEM_KEY, activeItem);
|
|
}, [activeItem]);
|
|
|
|
// 네비게이션 핸들러 - "새 프로젝트 만들기" 클릭 시 기존 프로젝트 데이터 초기화
|
|
const handleNavigate = (item: string) => {
|
|
if (item === '새 프로젝트 만들기') {
|
|
// 기존 프로젝트 데이터 초기화
|
|
clearAllProjectStorage();
|
|
localStorage.removeItem(ANALYSIS_DATA_KEY);
|
|
setWizardStep(-2);
|
|
setSongTaskId(null);
|
|
setImageTaskId(null);
|
|
setAnalysisData(null);
|
|
setImageList([]);
|
|
setVideoGenerationStatus('idle');
|
|
setVideoGenerationProgress(0);
|
|
setAnalysisError(null);
|
|
}
|
|
setActiveItem(item);
|
|
};
|
|
|
|
// 새 프로젝트 만들기 - 단계별 컨텐츠 렌더링
|
|
const renderWizardContent = () => {
|
|
switch (wizardStep) {
|
|
case -2:
|
|
// URL 입력 단계
|
|
return (
|
|
<UrlInputContent
|
|
onAnalyze={handleStartAnalysis}
|
|
onAutocomplete={handleAutocomplete}
|
|
onTestData={handleTestData}
|
|
error={analysisError}
|
|
/>
|
|
);
|
|
case -1:
|
|
// 로딩 단계
|
|
return <LoadingSection />;
|
|
case 0:
|
|
// 브랜드 분석 결과 단계
|
|
if (!analysisData) {
|
|
goToWizardStep(-2);
|
|
return null;
|
|
}
|
|
return (
|
|
<AnalysisResultSection
|
|
onBack={handleAnalysisBack}
|
|
onGenerate={handleAnalysisGenerate}
|
|
data={analysisData}
|
|
/>
|
|
);
|
|
case 1:
|
|
return (
|
|
<AssetManagementContent
|
|
onNext={(taskId: string) => {
|
|
// Clear video generation state to start fresh
|
|
localStorage.removeItem(VIDEO_GENERATION_KEY);
|
|
localStorage.removeItem(VIDEO_COMPLETE_KEY);
|
|
setVideoGenerationStatus('idle');
|
|
setVideoGenerationProgress(0);
|
|
|
|
setImageTaskId(taskId);
|
|
localStorage.setItem(IMAGE_TASK_ID_KEY, taskId);
|
|
goToWizardStep(2);
|
|
}}
|
|
imageList={imageList}
|
|
onRemoveImage={handleRemoveImage}
|
|
onAddImages={handleAddImages}
|
|
/>
|
|
);
|
|
case 2:
|
|
return (
|
|
<SoundStudioContent
|
|
onBack={() => goToWizardStep(1)}
|
|
onNext={(taskId: string) => {
|
|
setSongTaskId(taskId);
|
|
localStorage.setItem(SONG_TASK_ID_KEY, taskId);
|
|
goToWizardStep(3);
|
|
}}
|
|
businessInfo={currentBusinessInfo}
|
|
imageTaskId={imageTaskId}
|
|
videoGenerationStatus={videoGenerationStatus}
|
|
videoGenerationProgress={videoGenerationProgress}
|
|
/>
|
|
);
|
|
case 3:
|
|
return (
|
|
<CompletionContent
|
|
onBack={() => {
|
|
// 뒤로가기 시 비디오 생성 상태 초기화
|
|
// 새 노래 생성 후 다시 영상 생성할 수 있도록
|
|
localStorage.removeItem(VIDEO_GENERATION_KEY);
|
|
localStorage.removeItem(VIDEO_COMPLETE_KEY);
|
|
setVideoGenerationStatus('idle');
|
|
setVideoGenerationProgress(0);
|
|
goToWizardStep(2);
|
|
}}
|
|
songTaskId={songTaskId}
|
|
onVideoStatusChange={setVideoGenerationStatus}
|
|
onVideoProgressChange={setVideoGenerationProgress}
|
|
/>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const renderContent = () => {
|
|
switch (activeItem) {
|
|
case '대시보드':
|
|
return <DashboardContent />;
|
|
case '비즈니스 설정':
|
|
return <BusinessSettingsContent />;
|
|
case 'ADO2 콘텐츠':
|
|
return (
|
|
<ADO2ContentsPage
|
|
onBack={() => setActiveItem('새 프로젝트 만들기')}
|
|
/>
|
|
);
|
|
case '내 정보':
|
|
return <MyInfoContent />;
|
|
case '새 프로젝트 만들기':
|
|
// 브랜드 분석(0)과 로딩(-1)은 전체 화면으로 표시
|
|
if (wizardStep === 0 || wizardStep === -1) {
|
|
return renderWizardContent();
|
|
}
|
|
return (
|
|
<div className="wizard-page-container">
|
|
{renderWizardContent()}
|
|
</div>
|
|
);
|
|
default:
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center text-gray-500 font-light">
|
|
{t('app.pageComingSoon', { page: activeItem })}
|
|
</div>
|
|
);
|
|
}
|
|
};
|
|
|
|
// 로딩 화면에서만 Sidebar 숨김
|
|
const showSidebar = wizardStep !== -1;
|
|
|
|
// 브랜드 분석(0)일 때는 전체 페이지 스크롤
|
|
const isBrandAnalysis = activeItem === '새 프로젝트 만들기' && wizardStep === 0;
|
|
// 스크롤이 필요한 페이지: 대시보드, 비즈니스 설정, 브랜드 분석(0), ADO2 콘텐츠, 내 정보
|
|
const needsScroll = activeItem === '대시보드' || activeItem === '비즈니스 설정' || activeItem === 'ADO2 콘텐츠' || activeItem === '내 정보' || isBrandAnalysis;
|
|
|
|
// 브랜드 분석일 때는 전체 화면 스크롤
|
|
if (isBrandAnalysis) {
|
|
return (
|
|
<div className="analysis-page-wrapper">
|
|
{showSidebar && (
|
|
<Sidebar activeItem={activeItem} onNavigate={handleNavigate} onHome={handleHome} userInfo={userInfo} onLogout={handleLogout} />
|
|
)}
|
|
<main className="analysis-page-main">
|
|
{renderContent()}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`flex w-full bg-[#002224] text-white ${needsScroll ? 'min-h-[100dvh]' : 'h-[100dvh] overflow-hidden'}`}>
|
|
{showSidebar && (
|
|
<Sidebar activeItem={activeItem} onNavigate={handleNavigate} onHome={handleHome} userInfo={userInfo} onLogout={handleLogout} />
|
|
)}
|
|
<div className={`flex-1 relative pl-0 md:pl-0 ${needsScroll ? 'overflow-y-auto' : 'h-full overflow-hidden'}`}>
|
|
{renderContent()}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default GenerationFlow;
|