import React, { useState, useRef, useEffect } from 'react'; import { generateLyric, waitForLyricComplete, generateSong, waitForSongComplete } from '../../utils/api'; import { LANGUAGE_MAP } from '../../types/api'; interface BusinessInfo { customer_name: string; region: string; detail_region_info: string; } interface SoundStudioContentProps { onBack: () => void; onNext: (songTaskId: string) => void; businessInfo?: BusinessInfo; imageTaskId: string | null; videoGenerationStatus?: 'idle' | 'generating' | 'complete' | 'error'; videoGenerationProgress?: number; } type GenerationStatus = 'idle' | 'generating_lyric' | 'generating_song' | 'polling' | 'complete' | 'error'; interface SavedGenerationState { taskId: string; songId: string; lyrics: string; status: GenerationStatus; timestamp: number; } const STORAGE_KEY = 'castad_song_generation'; const STORAGE_EXPIRY = 30 * 60 * 1000; const MAX_RETRY_COUNT = 3; const LANGUAGE_FLAGS: Record = { 'ν•œκ΅­μ–΄': 'πŸ‡°πŸ‡·', 'English': 'πŸ‡ΊπŸ‡Έ', 'δΈ­ζ–‡': 'πŸ‡¨πŸ‡³', 'ζ—₯本θͺž': 'πŸ‡―πŸ‡΅', 'ΰΉ„ΰΈ—ΰΈ’': 'πŸ‡ΉπŸ‡­', 'TiαΊΏng Việt': 'πŸ‡»πŸ‡³', }; const SoundStudioContent: React.FC = ({ onBack, onNext, businessInfo, imageTaskId, videoGenerationStatus = 'idle', videoGenerationProgress = 0 }) => { const [selectedType, setSelectedType] = useState('보컬'); const [selectedLang, setSelectedLang] = useState('ν•œκ΅­μ–΄'); const [selectedGenre, setSelectedGenre] = useState('μžλ™ 선택'); const [progress, setProgress] = useState(0); const [isDragging, setIsDragging] = useState(false); const [status, setStatus] = useState('idle'); const [showLyrics, setShowLyrics] = useState(false); const [lyrics, setLyrics] = useState(''); const [audioUrl, setAudioUrl] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [errorMessage, setErrorMessage] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [retryCount, setRetryCount] = useState(0); const [songTaskId, setSongTaskId] = useState(null); const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = useState(false); const progressBarRef = useRef(null); const audioRef = useRef(null); const languageDropdownRef = useRef(null); const saveToStorage = (taskId: string, songId: string, currentLyrics: string, currentStatus: GenerationStatus) => { const data: SavedGenerationState = { taskId, songId, lyrics: currentLyrics, status: currentStatus, timestamp: Date.now(), }; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }; const clearStorage = () => { localStorage.removeItem(STORAGE_KEY); }; const loadFromStorage = (): SavedGenerationState | null => { try { const saved = localStorage.getItem(STORAGE_KEY); if (!saved) return null; const data: SavedGenerationState = JSON.parse(saved); if (Date.now() - data.timestamp > STORAGE_EXPIRY) { clearStorage(); return null; } return data; } catch { clearStorage(); return null; } }; useEffect(() => { const savedState = loadFromStorage(); if (savedState && (savedState.status === 'polling' || savedState.status === 'generating_song')) { if (savedState.lyrics) { setLyrics(savedState.lyrics); setShowLyrics(true); } setStatus('polling'); setStatusMessage('λ…Έλž˜λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€... (μƒˆλ‘œκ³ μΉ¨ ν›„ 볡ꡬ됨)'); resumePolling(savedState.taskId, savedState.songId, savedState.lyrics, 0); } }, []); // Close language dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) { setIsLanguageDropdownOpen(false); } }; if (isLanguageDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isLanguageDropdownOpen]); // Auto-navigate to next page when video generation is complete useEffect(() => { if (videoGenerationStatus === 'complete' && songTaskId) { // Wait a brief moment to show 100% completion before navigating const timer = setTimeout(() => { onNext(songTaskId); }, 500); return () => clearTimeout(timer); } }, [videoGenerationStatus, songTaskId, onNext]); const resumePolling = async (taskId: string, songId: string, currentLyrics: string, currentRetryCount: number = 0) => { try { const statusResponse = await waitForSongComplete( songId, (pollStatus: string) => { if (pollStatus === 'streaming') { setStatusMessage('λ…Έλž˜λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€...'); } else if (pollStatus === 'queued') { setStatusMessage('λ…Έλž˜ 생성 λŒ€κΈ° 쀑...'); } } ); if (!statusResponse.success) { throw new Error(statusResponse.error_message || 'μŒμ•… 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } setAudioUrl(statusResponse.song_url); setSongTaskId(taskId); setStatus('complete'); setStatusMessage(''); setRetryCount(0); clearStorage(); } catch (error) { console.error('Polling failed:', error); if (error instanceof Error && error.message === 'TIMEOUT') { if (currentRetryCount < MAX_RETRY_COUNT) { const newRetryCount = currentRetryCount + 1; setRetryCount(newRetryCount); setStatusMessage(`μ‹œκ°„ 초과둜 μž¬μƒμ„± 쀑... (${newRetryCount}/${MAX_RETRY_COUNT})`); await regenerateSongOnly(currentLyrics, newRetryCount); } else { setStatus('error'); setErrorMessage('μ—¬λŸ¬ 번 μ‹œλ„ν–ˆμ§€λ§Œ μŒμ•… 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.'); setRetryCount(0); clearStorage(); } } else { setStatus('error'); setErrorMessage(error instanceof Error ? error.message : 'μŒμ•… 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); setRetryCount(0); clearStorage(); } } }; const regenerateSongOnly = async (currentLyrics: string, currentRetryCount: number) => { if (!businessInfo || !imageTaskId) return; try { const language = LANGUAGE_MAP[selectedLang] || 'Korean'; const genreMap: Record = { 'μžλ™ 선택': 'pop', 'K-POP': 'kpop', 'λ°œλΌλ“œ': 'ballad', 'Hip-Hop': 'hip-hop', 'R&B': 'rnb', 'EDM': 'edm', 'JAZZ': 'jazz', 'ROCK': 'rock', }; const songResponse = await generateSong(imageTaskId, { genre: genreMap[selectedGenre] || 'pop', language, lyrics: currentLyrics, }); if (!songResponse.success) { throw new Error(songResponse.error_message || 'μŒμ•… 생성 μš”μ²­μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } saveToStorage(songResponse.task_id, songResponse.song_id, currentLyrics, 'polling'); await resumePolling(songResponse.task_id, songResponse.song_id, currentLyrics, currentRetryCount); } catch (error) { console.error('Song regeneration failed:', error); setStatus('error'); setErrorMessage(error instanceof Error ? error.message : 'μŒμ•… μž¬μƒμ„± 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); setRetryCount(0); clearStorage(); } }; const handleMove = (clientX: number) => { if (!progressBarRef.current || !audioRef.current) return; const rect = progressBarRef.current.getBoundingClientRect(); const newProgress = Math.max(0, Math.min(100, ((clientX - rect.left) / rect.width) * 100)); const newTime = (newProgress / 100) * duration; audioRef.current.currentTime = newTime; setProgress(newProgress); setCurrentTime(newTime); }; const onMouseDown = (e: React.MouseEvent) => { if (!audioUrl) return; setIsDragging(true); handleMove(e.clientX); }; useEffect(() => { const onMouseMove = (e: MouseEvent) => { if (isDragging) handleMove(e.clientX); }; const onMouseUp = () => { setIsDragging(false); }; if (isDragging) { window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); } return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); }; }, [isDragging]); useEffect(() => { const audio = audioRef.current; if (!audio) return; const handleTimeUpdate = () => { setCurrentTime(audio.currentTime); if (audio.duration > 0) { setProgress((audio.currentTime / audio.duration) * 100); } }; const handleLoadedMetadata = () => { setDuration(audio.duration); }; const handleEnded = () => { setIsPlaying(false); setProgress(0); setCurrentTime(0); }; audio.addEventListener('timeupdate', handleTimeUpdate); audio.addEventListener('loadedmetadata', handleLoadedMetadata); audio.addEventListener('ended', handleEnded); return () => { audio.removeEventListener('timeupdate', handleTimeUpdate); audio.removeEventListener('loadedmetadata', handleLoadedMetadata); audio.removeEventListener('ended', handleEnded); }; }, [audioUrl]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; const togglePlayPause = () => { if (!audioRef.current) return; if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } setIsPlaying(!isPlaying); }; const handleGenerateMusic = async () => { if (!businessInfo) { setErrorMessage('λΉ„μ¦ˆλ‹ˆμŠ€ 정보가 μ—†μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.'); return; } if (!imageTaskId) { setErrorMessage('이미지 μ—…λ‘œλ“œ 정보가 μ—†μŠ΅λ‹ˆλ‹€. 이전 λ‹¨κ³„λ‘œ λŒμ•„κ°€ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.'); return; } setStatus('generating_lyric'); setErrorMessage(null); setStatusMessage('가사λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€...'); try { const language = LANGUAGE_MAP[selectedLang] || 'Korean'; // 1. 가사 생성 μš”μ²­ const lyricResponse = await generateLyric({ customer_name: businessInfo.customer_name, detail_region_info: businessInfo.detail_region_info, language, region: businessInfo.region, task_id: imageTaskId, }); if (!lyricResponse.success || !lyricResponse.task_id) { throw new Error(lyricResponse.error_message || '가사 생성 μš”μ²­μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } // 2. 가사 생성 μƒνƒœ 폴링 β†’ μ™„λ£Œ μ‹œ 상세 쑰회 setStatusMessage('가사λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€...'); const lyricDetailResponse = await waitForLyricComplete( lyricResponse.task_id, (status: string) => { if (status === 'processing') { setStatusMessage('가사λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€...'); } } ); if (!lyricDetailResponse.lyric_result) { throw new Error('가사λ₯Ό λ°›μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.'); } // "I'm sorry" 체크 if (lyricDetailResponse.lyric_result.includes("I'm sorry")) { throw new Error('가사 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.'); } setLyrics(lyricDetailResponse.lyric_result); setShowLyrics(true); setStatus('generating_song'); setStatusMessage('λ…Έλž˜λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€...'); const genreMap: Record = { 'μžλ™ 선택': 'pop', 'K-POP': 'kpop', 'λ°œλΌλ“œ': 'ballad', 'Hip-Hop': 'hip-hop', 'R&B': 'rnb', 'EDM': 'edm', 'JAZZ': 'jazz', 'ROCK': 'rock', }; const songResponse = await generateSong(imageTaskId, { genre: genreMap[selectedGenre] || 'pop', language, lyrics: lyricDetailResponse.lyric_result, }); if (!songResponse.success) { throw new Error(songResponse.error_message || 'μŒμ•… 생성 μš”μ²­μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); } setStatus('polling'); setStatusMessage('λ…Έλž˜λ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€...'); saveToStorage(songResponse.task_id, songResponse.song_id, lyricDetailResponse.lyric_result, 'polling'); await resumePolling(songResponse.task_id, songResponse.song_id, lyricDetailResponse.lyric_result, 0); } catch (error) { console.error('Music generation failed:', error); setStatus('error'); setErrorMessage(error instanceof Error ? error.message : 'μŒμ•… 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); setRetryCount(0); clearStorage(); } }; const handleRegenerate = async () => { setShowLyrics(false); setAudioUrl(null); setLyrics(''); setProgress(0); setCurrentTime(0); setDuration(0); setIsPlaying(false); setRetryCount(0); clearStorage(); await handleGenerateMusic(); }; const isGenerating = status === 'generating_lyric' || status === 'generating_song' || status === 'polling'; return (
{audioUrl && (