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 (
라이브러리에 트랙이 없습니다
) : (
{JSON.stringify({
title: exportData.title,
duration: fmtDuration(exportData.duration_sec),
mp4_url: exportData.mp4_url,
}, null, 2)}