feat(web-ui): YouTube 6 서브탭 + Library 영상 파이프라인 트리거

This commit is contained in:
2026-05-07 17:31:37 +09:00
parent 9ffd7889e7
commit 522b7695aa
2 changed files with 49 additions and 35 deletions

View File

@@ -338,7 +338,7 @@ const TrackResult = ({ track, onDownload, onNew }) => {
/* ───────────────────────────────────────────── /* ─────────────────────────────────────────────
Library Card Library Card
───────────────────────────────────────────── */ ───────────────────────────────────────────── */
const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, isGenerating }) => { const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, onVideoPipeline, isGenerating }) => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const genre = GENRES.find((g) => g.id === track.genre); const genre = GENRES.find((g) => g.id === track.genre);
const totalSec = track.duration_sec ?? null; const totalSec = track.duration_sec ?? null;
@@ -429,6 +429,9 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo
<button type="button" onClick={() => { onVideoProject(track); setMenuOpen(false); }}> <button type="button" onClick={() => { onVideoProject(track); setMenuOpen(false); }}>
🎯 YouTube 프로젝트 🎯 YouTube 프로젝트
</button> </button>
<button type="button" onClick={() => { onVideoPipeline(track); setMenuOpen(false); }}>
🎬 영상 파이프라인
</button>
</div> </div>
)} )}
</div> </div>
@@ -439,6 +442,10 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo
<a href={track.audio_url} download className="ms-btn ms-btn--ghost ms-btn--sm"> <a href={track.audio_url} download className="ms-btn ms-btn--ghost ms-btn--sm">
Download Download
</a> </a>
<button type="button" className="ms-btn ms-btn--ghost ms-btn--sm"
onClick={() => onVideoPipeline(track)}>
🎬 영상 파이프라인
</button>
</div> </div>
)} )}
<p className="ms-lib-card__date"> <p className="ms-lib-card__date">
@@ -451,7 +458,7 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo
/* ───────────────────────────────────────────── /* ─────────────────────────────────────────────
Library Section Library Section
───────────────────────────────────────────── */ ───────────────────────────────────────────── */
const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, isGenerating, loading }) => { const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, onVideoPipeline, isGenerating, loading }) => {
const [playingId, setPlayingId] = useState(null); const [playingId, setPlayingId] = useState(null);
const handlePlay = (track) => { const handlePlay = (track) => {
@@ -506,6 +513,7 @@ const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCove
onSyncedLyrics={onSyncedLyrics} onSyncedLyrics={onSyncedLyrics}
onVideoGenerate={onVideoGenerate} onVideoGenerate={onVideoGenerate}
onVideoProject={onVideoProject} onVideoProject={onVideoProject}
onVideoPipeline={onVideoPipeline}
isGenerating={isGenerating} isGenerating={isGenerating}
/> />
))} ))}
@@ -521,6 +529,7 @@ export default function MusicStudio() {
/* ── 탭 ── */ /* ── 탭 ── */
const [tab, setTab] = useState('create'); const [tab, setTab] = useState('create');
const [initialTrackId, setInitialTrackId] = useState(null); const [initialTrackId, setInitialTrackId] = useState(null);
const [openPipelineFor, setOpenPipelineFor] = useState(null);
/* ── Provider 상태 ── */ /* ── Provider 상태 ── */
const [providers, setProviders] = useState([]); const [providers, setProviders] = useState([]);
@@ -1070,6 +1079,11 @@ export default function MusicStudio() {
setTab('youtube'); setTab('youtube');
}; };
const handleVideoPipeline = (track) => {
setOpenPipelineFor(track.id);
setTab('youtube');
};
const handleNewTrack = () => { const handleNewTrack = () => {
setTrack(null); setTrack(null);
setGenProgress(0); setGenProgress(0);
@@ -1159,6 +1173,7 @@ export default function MusicStudio() {
onSyncedLyrics={handleSyncedLyrics} onSyncedLyrics={handleSyncedLyrics}
onVideoGenerate={handleVideoGenerate} onVideoGenerate={handleVideoGenerate}
onVideoProject={handleVideoProject} onVideoProject={handleVideoProject}
onVideoPipeline={handleVideoPipeline}
isGenerating={isGenerating} isGenerating={isGenerating}
/> />
</PullToRefresh> </PullToRefresh>
@@ -1193,6 +1208,7 @@ export default function MusicStudio() {
library={library} library={library}
initialTrackId={initialTrackId} initialTrackId={initialTrackId}
onClearInitialTrack={() => setInitialTrackId(null)} onClearInitialTrack={() => setInitialTrackId(null)}
openPipelineFor={openPipelineFor}
/> />
)} )}

View File

@@ -3,48 +3,45 @@ import VideoProjectsTab from './VideoProjectsTab';
import RevenueTab from './RevenueTab'; import RevenueTab from './RevenueTab';
import TrendsTab from './TrendsTab'; import TrendsTab from './TrendsTab';
import CompileTab from './CompileTab'; import CompileTab from './CompileTab';
import PipelineTab from './PipelineTab';
import SetupTab from './SetupTab';
export default function YoutubeTab({ library, initialTrackId, onClearInitialTrack }) { export default function YoutubeTab({ library, initialTrackId, onClearInitialTrack, openPipelineFor }) {
const [subtab, setSubtab] = useState('video'); const [subtab, setSubtab] = useState('pipeline');
// initialTrackId가 들어오면 video 서브탭으로 전환
useEffect(() => { useEffect(() => {
if (initialTrackId) setSubtab('video'); if (initialTrackId) setSubtab('video');
}, [initialTrackId]); }, [initialTrackId]);
useEffect(() => {
if (openPipelineFor) setSubtab('pipeline');
}, [openPipelineFor]);
const tabs = [
['pipeline', '🚀 진행'],
['video', '🎬 영상 제작'],
['compile', '🎵 컴파일'],
['trends', '📊 시장 트렌드'],
['revenue', '💰 수익 추적'],
['setup', '⚙️ 구성'],
];
return ( return (
<div className="yt-container"> <div className="yt-container">
<nav className="yt-subtabs"> <nav className="yt-subtabs">
<button {tabs.map(([key, label]) => (
type="button" <button
className={`yt-subtab ${subtab === 'video' ? 'is-active' : ''}`} key={key}
onClick={() => setSubtab('video')} type="button"
> className={`yt-subtab ${subtab === key ? 'is-active' : ''}`}
🎬 영상 제작 onClick={() => setSubtab(key)}
</button> >
<button {label}
type="button" </button>
className={`yt-subtab ${subtab === 'revenue' ? 'is-active' : ''}`} ))}
onClick={() => setSubtab('revenue')}
>
💰 수익 추적
</button>
<button
type="button"
className={`yt-subtab ${subtab === 'trends' ? 'is-active' : ''}`}
onClick={() => setSubtab('trends')}
>
📊 시장 트렌드
</button>
<button
type="button"
className={`yt-subtab ${subtab === 'compile' ? 'is-active' : ''}`}
onClick={() => setSubtab('compile')}
>
🎵 컴파일
</button>
</nav> </nav>
{subtab === 'pipeline' && <PipelineTab library={library} initialTrackId={openPipelineFor} />}
{subtab === 'video' && ( {subtab === 'video' && (
<VideoProjectsTab <VideoProjectsTab
library={library} library={library}
@@ -52,9 +49,10 @@ export default function YoutubeTab({ library, initialTrackId, onClearInitialTrac
onClearInitialTrack={onClearInitialTrack} onClearInitialTrack={onClearInitialTrack}
/> />
)} )}
{subtab === 'revenue' && <RevenueTab />}
{subtab === 'trends' && <TrendsTab />}
{subtab === 'compile' && <CompileTab library={library} />} {subtab === 'compile' && <CompileTab library={library} />}
{subtab === 'trends' && <TrendsTab />}
{subtab === 'revenue' && <RevenueTab />}
{subtab === 'setup' && <SetupTab />}
</div> </div>
); );
} }