import React, { useState, useEffect, useRef } from 'react'; import { generateVideo, waitForVideoComplete } from '../../utils/api'; interface CompletionContentProps { onBack: () => void; songTaskId: string | null; } type VideoStatus = 'idle' | 'generating' | 'polling' | 'complete' | 'error'; const VIDEO_STORAGE_KEY = 'castad_video_generation'; const VIDEO_STORAGE_EXPIRY = 30 * 60 * 1000; // 30분 interface SavedVideoState { videoTaskId: string; songTaskId: string; status: VideoStatus; videoUrl: string | null; timestamp: number; } const CompletionContent: React.FC = ({ onBack, songTaskId }) => { const [selectedSocials, setSelectedSocials] = useState([]); const [videoStatus, setVideoStatus] = useState('idle'); const [videoUrl, setVideoUrl] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [isPlaying, setIsPlaying] = useState(false); const [progress, setProgress] = useState(0); const videoRef = useRef(null); const hasStartedGeneration = useRef(false); const saveToStorage = (videoTaskId: string, currentSongTaskId: string, status: VideoStatus, url: string | null) => { const data: SavedVideoState = { videoTaskId, songTaskId: currentSongTaskId, status, videoUrl: url, timestamp: Date.now(), }; localStorage.setItem(VIDEO_STORAGE_KEY, JSON.stringify(data)); }; const clearStorage = () => { localStorage.removeItem(VIDEO_STORAGE_KEY); }; const loadFromStorage = (): SavedVideoState | null => { try { const saved = localStorage.getItem(VIDEO_STORAGE_KEY); if (!saved) return null; const data: SavedVideoState = JSON.parse(saved); if (Date.now() - data.timestamp > VIDEO_STORAGE_EXPIRY) { clearStorage(); return null; } return data; } catch { clearStorage(); return null; } }; const startVideoGeneration = async () => { if (!songTaskId || hasStartedGeneration.current) return; hasStartedGeneration.current = true; setVideoStatus('generating'); setStatusMessage('영상 생성을 요청하고 있습니다...'); setErrorMessage(null); try { const videoResponse = await generateVideo(songTaskId); if (!videoResponse.success) { throw new Error(videoResponse.error_message || '영상 생성 요청에 실패했습니다.'); } setVideoStatus('polling'); setStatusMessage('영상을 생성하고 있습니다...'); // video/status API는 creatomate_render_id를 사용 saveToStorage(videoResponse.creatomate_render_id, songTaskId, 'polling', null); await pollVideoStatus(videoResponse.creatomate_render_id, songTaskId); } catch (error) { console.error('Video generation failed:', error); setVideoStatus('error'); setErrorMessage(error instanceof Error ? error.message : '영상 생성 중 오류가 발생했습니다.'); hasStartedGeneration.current = false; clearStorage(); } }; // 상태별 한글 메시지 매핑 const getStatusMessage = (status: string): string => { switch (status) { case 'planned': return '예약됨'; case 'waiting': return '대기 중'; case 'transcribing': return '트랜스크립션 중'; case 'rendering': return '렌더링 중'; case 'succeeded': return '완료'; default: return '처리 중...'; } }; const pollVideoStatus = async (videoTaskId: string, currentSongTaskId: string) => { try { // 영상 생성 상태 폴링 (3분 타임아웃, 3초 간격) const statusResponse = await waitForVideoComplete( videoTaskId, (status: string) => { setStatusMessage(getStatusMessage(status)); } ); // render_data.url에서 영상 URL 가져오기 const videoUrlFromResponse = statusResponse.render_data?.url; if (videoUrlFromResponse) { setVideoUrl(videoUrlFromResponse); setVideoStatus('complete'); setStatusMessage(''); saveToStorage(videoTaskId, currentSongTaskId, 'complete', videoUrlFromResponse); } else { throw new Error('영상 URL을 받지 못했습니다.'); } } catch (error) { console.error('Video polling failed:', error); if (error instanceof Error && error.message === 'TIMEOUT') { setVideoStatus('error'); setErrorMessage('영상 생성 시간이 초과되었습니다. 다시 시도해주세요.'); } else { setVideoStatus('error'); setErrorMessage(error instanceof Error ? error.message : '영상 생성 중 오류가 발생했습니다.'); } hasStartedGeneration.current = false; clearStorage(); } }; // 컴포넌트 마운트 시 저장된 상태 확인 또는 영상 생성 시작 useEffect(() => { const savedState = loadFromStorage(); // 저장된 상태가 있고, 같은 songTaskId인 경우 if (savedState && savedState.songTaskId === songTaskId) { if (savedState.status === 'complete' && savedState.videoUrl) { // 이미 완료된 경우 setVideoUrl(savedState.videoUrl); setVideoStatus('complete'); hasStartedGeneration.current = true; } else if (savedState.status === 'polling') { // 폴링 중이었던 경우 다시 폴링 setVideoStatus('polling'); setStatusMessage('영상을 처리하고 있습니다... (새로고침 후 복구됨)'); hasStartedGeneration.current = true; pollVideoStatus(savedState.videoTaskId, savedState.songTaskId); } } else if (songTaskId && !hasStartedGeneration.current) { // 새로운 영상 생성 시작 startVideoGeneration(); } }, [songTaskId]); const toggleSocial = (id: string) => { setSelectedSocials(prev => prev.includes(id) ? prev.filter(s => s !== id) : [...prev, id] ); }; const togglePlayPause = () => { if (!videoRef.current || !videoUrl) return; if (isPlaying) { videoRef.current.pause(); } else { videoRef.current.play(); } setIsPlaying(!isPlaying); }; const handleTimeUpdate = () => { if (videoRef.current && videoRef.current.duration > 0) { setProgress((videoRef.current.currentTime / videoRef.current.duration) * 100); } }; const handleVideoEnded = () => { setIsPlaying(false); setProgress(0); }; const handleDownload = () => { if (videoUrl) { const link = document.createElement('a'); link.href = videoUrl; link.download = 'castad_video.mp4'; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }; const handleRetry = () => { hasStartedGeneration.current = false; setVideoStatus('idle'); setVideoUrl(null); setErrorMessage(null); clearStorage(); startVideoGeneration(); }; const socials = [ { id: 'Youtube', email: 'o2ocorp@o2o.kr', color: '#ff0000', icon: }, { id: 'Instagram', email: 'o2ocorp@o2o.kr', color: '#e4405f', icon: }, ]; const isLoading = videoStatus === 'generating' || videoStatus === 'polling'; return (
{/* Back Button */}
{/* Header */}

{isLoading ? '영상 생성 중' : videoStatus === 'error' ? '영상 생성 실패' : '콘텐츠 제작 완료'}

{isLoading ? statusMessage || '잠시만 기다려주세요...' : videoStatus === 'error' ? errorMessage || '오류가 발생했습니다.' : '인스타그램 릴스 및 틱톡에 최적화된 고성능 영상을 제작했습니다.'}

{/* Main Content */}
{/* Left: Video Preview */}

이미지 및 영상

{isLoading ? ( /* Loading State */

{statusMessage}

) : videoStatus === 'error' ? ( /* Error State */

{errorMessage}

) : videoUrl ? ( /* Video Player */
{/* Tags - only show when complete */} {videoStatus === 'complete' && (
{['AI 최적화', '색상 보정', '다이나믹 자막', '비트 싱크', 'SEO 메타 태그'].map(tag => ( {tag} ))}
)}
{/* Right: Sharing */}

공유

{socials.map(social => { const isSelected = selectedSocials.includes(social.id); return (
videoStatus === 'complete' && toggleSocial(social.id)} className={`social-card ${isSelected ? 'selected' : ''} ${videoStatus !== 'complete' ? 'disabled' : ''}`} >
{social.icon}
{isSelected && (
)}
{social.id} {social.email}
); })}
); }; export default CompletionContent;