import { useState, useEffect, useRef, useCallback } from 'react'; import { createCompileJob, getCompileJobs, deleteCompileJob, exportCompileJob, createPipeline, startPipeline, } from '../../../api'; const fmtDuration = (sec) => { if (!sec) return ''; const m = Math.floor(sec / 60); const s = Math.round(sec % 60); return `${m}분 ${s}초`; }; export default function CompileTab({ library, onSwitchToPipeline }) { const [jobs, setJobs] = useState([]); const [selected, setSelected] = useState([]); // [{id, title, audio_url}] in order const [crossfade, setCrossfade] = useState(3); const [title, setTitle] = useState(''); const [creating, setCreating] = useState(false); const [exportData, setExportData] = useState(null); // {mp4_url, duration_sec, title} const [exportingId, setExportingId] = useState(null); const pollRef = useRef(null); const loadJobs = useCallback(async () => { const res = await getCompileJobs().catch(() => ({ jobs: [] })); setJobs(Array.isArray(res.jobs) ? res.jobs : []); }, []); useEffect(() => { loadJobs(); }, [loadJobs]); // Poll while any job is rendering useEffect(() => { const hasRendering = jobs.some(j => j.status === 'rendering'); if (hasRendering && !pollRef.current) { pollRef.current = setInterval(loadJobs, 5000); } else if (!hasRendering && pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } return () => { clearInterval(pollRef.current); pollRef.current = null; }; }, [jobs, loadJobs]); const toggleTrack = (track) => { setSelected(prev => { const exists = prev.find(t => t.id === track.id); if (exists) return prev.filter(t => t.id !== track.id); return [...prev, { id: track.id, title: track.title, audio_url: track.audio_url }]; }); }; const moveUp = (idx) => setSelected(prev => { const a = [...prev]; [a[idx-1], a[idx]] = [a[idx], a[idx-1]]; return a; }); const moveDown = (idx) => setSelected(prev => { const a = [...prev]; [a[idx], a[idx+1]] = [a[idx+1], a[idx]]; return a; }); const remove = (idx) => setSelected(prev => prev.filter((_, i) => i !== idx)); const handleCreate = async () => { if (selected.length < 2 || creating) return; setCreating(true); try { await createCompileJob({ title: title.trim() || `컴파일 ${new Date().toLocaleDateString('ko-KR')}`, track_ids: selected.map(t => t.id), crossfade_sec: crossfade, }); setSelected([]); setTitle(''); await loadJobs(); } catch (e) { console.error('createCompileJob:', e); } finally { setCreating(false); } }; const handleExport = async (jobId) => { setExportingId(jobId); try { const data = await exportCompileJob(jobId); setExportData(data); } catch (e) { console.error('exportCompileJob:', e); } finally { setExportingId(null); } }; const handleDelete = async (jobId) => { if (!window.confirm('컴파일 영상을 삭제할까요?')) return; await deleteCompileJob(jobId).catch(() => {}); setJobs(prev => prev.filter(j => j.id !== jobId)); if (exportData && exportData.id === jobId) setExportData(null); }; const handleVideoFromCompile = async (jobId) => { if (!window.confirm('이 mix로 영상 파이프라인을 시작할까요?')) return; try { const p = await createPipeline({ compile_job_id: jobId }); await startPipeline(p.id); if (onSwitchToPipeline) { onSwitchToPipeline(p.id); } } catch (e) { alert(`파이프라인 시작 실패: ${e.message || e}`); } }; const totalMin = selected.length > 0 ? Math.round(selected.reduce((acc, t) => { const match = library.find(l => l.id === t.id); return acc + (match?.duration_sec ?? 180); }, 0) / 60) : 0; return (
{/* 트랙 선택 패널 */}

🎵 트랙 선택 (2개 이상)

{library.length === 0 ? (

라이브러리에 트랙이 없습니다

) : (
{library.map(t => { const isSelected = !!selected.find(s => s.id === t.id); return (
toggleTrack(t)} > {isSelected ? '✓' : ''} {t.title} {t.duration_sec && ( {fmtDuration(t.duration_sec)} )}
); })}
)}
{/* 순서 조정 + 설정 */} {selected.length > 0 && (

📋 선택된 트랙 순서 ({selected.length}개 {totalMin > 0 ? ` · 약 ${totalMin}분` : ''})

{selected.map((t, i) => (
{i + 1} {t.title}
))}
{/* 설정 */}
setCrossfade(Number(e.target.value))} className="yt-compile-slider" />
setTitle(e.target.value)} maxLength={80} />
)} {/* 컴파일 작업 목록 */} {jobs.length > 0 && (

컴파일 작업

{jobs.map(j => (
🎵
{j.title || `컴파일 #${j.id}`}
{j.track_ids?.length ?? 0}곡 {j.duration_sec ? ` · ${fmtDuration(j.duration_sec)}` : ''} {' · '}크로스페이드 {j.crossfade_sec}초
{j.status === 'rendering' && ( <> 처리중
)} {j.status === 'done' && ( <> ✓ 완료 )} {j.status === 'failed' && ( 실패 )} {j.status === 'pending' && ( 대기 )}
))}
)} {/* 내보내기 패키지 */} {exportData && (

↓ 내보내기

↓ MP4 다운로드
파일 정보
{JSON.stringify({
    title:    exportData.title,
    duration: fmtDuration(exportData.duration_sec),
    mp4_url:  exportData.mp4_url,
}, null, 2)}
                        
)}
); }