diff --git a/src/pages/music/MusicStudio.css b/src/pages/music/MusicStudio.css index 8ad99ff..d4b743a 100644 --- a/src/pages/music/MusicStudio.css +++ b/src/pages/music/MusicStudio.css @@ -2608,3 +2608,459 @@ background: var(--ms-surface); border: 1px solid var(--ms-line); } .ms-remix-submit { align-self: flex-start; margin-top: 8px; } + +/* ══════════════════════════════════════════ + YouTube Tab — yt-* classes +══════════════════════════════════════════ */ + +.ms-tab--youtube.is-active { + color: #f59e0b; + border-bottom-color: #f59e0b; +} + +.yt-container { + display: flex; + flex-direction: column; + gap: 0; +} + +.yt-subtabs { + display: flex; + border-bottom: 1px solid #1f2937; + background: #0d1117; + padding: 0 16px; +} + +.yt-subtab { + padding: 10px 18px; + font-size: 12px; + color: #6b7280; + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + transition: color 0.15s, border-color 0.15s; + white-space: nowrap; +} + +.yt-subtab:hover { color: #9ca3af; } + +.yt-subtab.is-active { + color: #22c55e; + border-bottom-color: #22c55e; + font-weight: 600; +} + +.yt-content { + display: flex; + flex-direction: column; + gap: 14px; + padding: 16px; +} + +.yt-card { + background: #0d1117; + border: 1px solid #1f2937; + border-radius: 10px; + padding: 14px; +} + +.yt-card--create { border-color: #22c55e33; } +.yt-card--export { border-color: #3b82f633; border-style: dashed; } + +.yt-card__title { + font-size: 12px; + font-weight: 700; + color: #ccc; + margin: 0 0 12px; +} + +.yt-card--create .yt-card__title { color: #86efac; } +.yt-card--export .yt-card__title { color: #93c5fd; } + +.yt-row { + display: flex; + gap: 8px; + margin-bottom: 10px; + align-items: center; +} + +.yt-row--bottom { margin-bottom: 0; margin-top: 8px; } + +.yt-form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-bottom: 0; +} + +.yt-field { display: flex; flex-direction: column; gap: 4px; } +.yt-field__label { font-size: 10px; color: #6b7280; } + +.yt-input { + background: #1f2937; + border: 1px solid #374151; + border-radius: 6px; + padding: 7px 10px; + color: #ccc; + font-size: 12px; + width: 100%; + box-sizing: border-box; +} + +.yt-input:focus { outline: none; border-color: #22c55e; } +.yt-input--sm { padding: 4px 8px; font-size: 11px; } + +.yt-select { + flex: 1; + background: #1f2937; + border: 1px solid #374151; + border-radius: 6px; + padding: 8px 10px; + color: #9ca3af; + font-size: 12px; +} + +.yt-format-toggle { display: flex; gap: 4px; } + +.yt-format-btn { + background: #1f2937; + border: 1px solid #374151; + border-radius: 6px; + padding: 8px 10px; + color: #9ca3af; + font-size: 11px; + cursor: pointer; + white-space: nowrap; +} + +.yt-format-btn.is-active { + background: #1a2e1a; + border-color: #22c55e; + color: #86efac; +} + +.yt-country-label { font-size: 11px; color: #6b7280; margin-bottom: 6px; } + +.yt-country-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; } + +.yt-chip { + background: #1f2937; + border: 1px solid #374151; + border-radius: 4px; + padding: 3px 10px; + color: #6b7280; + font-size: 11px; + cursor: pointer; + transition: all 0.15s; +} + +.yt-chip.is-active { + background: #1e3a2a; + border-color: #22c55e; + color: #86efac; +} + +.yt-create-btn { width: 100%; margin-top: 2px; } + +.yt-project-list { display: flex; flex-direction: column; gap: 8px; } + +.yt-project-card { + background: #1f2937; + border-radius: 8px; + padding: 10px 12px; + display: flex; + align-items: center; + gap: 10px; +} + +.yt-project-card__icon { + width: 40px; + height: 40px; + background: #111827; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + flex-shrink: 0; +} + +.yt-project-card__info { flex: 1; min-width: 0; } + +.yt-project-card__title { + font-size: 12px; + font-weight: 600; + color: #ccc; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.yt-project-card__meta { font-size: 10px; color: #6b7280; margin-top: 2px; } + +.yt-status { + font-size: 10px; + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; +} + +.yt-status--pending { background: #1f2937; color: #9ca3af; } +.yt-status--rendering { background: #1a1500; color: #f59e0b; } +.yt-status--done { background: #0a3d1a; color: #22c55e; } +.yt-status--failed { background: #2d0a0a; color: #f87171; } + +.yt-progress-bar { + height: 3px; + background: #374151; + border-radius: 2px; + margin-top: 6px; + overflow: hidden; +} + +.yt-progress-bar__fill { + height: 100%; + width: 65%; + background: linear-gradient(90deg, #f59e0b, #fbbf24); + border-radius: 2px; + animation: yt-progress-pulse 2s ease-in-out infinite; +} + +@keyframes yt-progress-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +.yt-export-links { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; } + +.yt-meta-preview { background: #111827; border-radius: 6px; padding: 8px; } +.yt-meta-preview__label { font-size: 10px; color: #6b7280; margin-bottom: 4px; } +.yt-meta-preview__content { + font-size: 11px; + color: #9ca3af; + font-family: monospace; + margin: 0; + white-space: pre-wrap; + word-break: break-all; +} + +.yt-empty { + text-align: center; + color: #6b7280; + font-size: 11px; + padding: 8px 0; + margin: 0; +} + +.yt-dash-cards { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; +} + +.yt-dash-card { + background: #0d1117; + border: 1px solid #1f2937; + border-radius: 8px; + padding: 12px; + text-align: center; +} + +.yt-dash-card__label { font-size: 10px; color: #6b7280; margin-bottom: 4px; } +.yt-dash-card__sub { font-size: 9px; color: #6b7280; margin-top: 2px; } + +.yt-dash-card__value { font-size: 18px; font-weight: 700; } +.yt-dash-card__value--green { color: #22c55e; } +.yt-dash-card__value--blue { color: #60a5fa; } +.yt-dash-card__value--amber { color: #f59e0b; } + +.yt-bar-chart { display: flex; flex-direction: column; gap: 8px; } + +.yt-bar-row { display: flex; align-items: center; gap: 8px; } + +.yt-bar-row__label { + width: 80px; + font-size: 11px; + color: #9ca3af; + text-align: right; + flex-shrink: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.yt-bar-row__rank { + width: 24px; + font-size: 11px; + font-weight: 700; + color: #f59e0b; + text-align: center; + flex-shrink: 0; +} + +.yt-bar-row__info { flex: 1; } + +.yt-bar-row__genre-header { + display: flex; + justify-content: space-between; + margin-bottom: 3px; +} + +.yt-bar-row__genre-name { font-size: 12px; color: #ccc; } +.yt-bar-row__flags { font-size: 10px; color: #9ca3af; } + +.yt-bar-row__track { + flex: 1; + height: 6px; + background: #1f2937; + border-radius: 3px; + overflow: hidden; +} + +.yt-bar-row__fill { + height: 100%; + background: linear-gradient(90deg, #22c55e, #4ade80); + border-radius: 3px; + transition: width 0.4s ease; +} + +.yt-bar-row__fill--genre { background: linear-gradient(90deg, #f59e0b, #fbbf24); } + +.yt-bar-row__value { + width: 44px; + font-size: 11px; + color: #22c55e; + text-align: right; + flex-shrink: 0; +} + +.yt-table { display: flex; flex-direction: column; gap: 2px; } + +.yt-table__header { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr 28px; + gap: 4px; + padding: 0 4px 6px; + border-bottom: 1px solid #1f2937; + font-size: 10px; + color: #6b7280; +} + +.yt-table__row { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr 28px; + gap: 4px; + padding: 7px 4px; + border-bottom: 1px solid #111827; + align-items: center; +} + +.yt-table__row--editing { + background: #111827; + border-radius: 6px; + padding: 8px; +} + +.yt-table__row:last-child { border-bottom: none; } + +.yt-table__cell { font-size: 11px; color: #9ca3af; } +.yt-table__cell--mono { font-family: monospace; } +.yt-table__cell--green { color: #22c55e; } +.yt-table__cell--amber { color: #f59e0b; } + +.yt-table__actions { display: flex; gap: 4px; grid-column: span 2; } + +.yt-status-bar { + background: #0d1117; + border: 1px solid #1f2937; + border-radius: 8px; + padding: 10px 14px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.yt-status-bar__left { display: flex; align-items: center; gap: 8px; } + +.yt-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #22c55e; + box-shadow: 0 0 6px #22c55e; + flex-shrink: 0; +} + +.yt-status-bar__text { font-size: 11px; color: #9ca3af; } + +.yt-prompt-list { display: flex; flex-direction: column; gap: 8px; } + +.yt-prompt-card { + background: #1a0d2e; + border-radius: 8px; + padding: 10px 12px; +} + +.yt-prompt-card__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 5px; +} + +.yt-prompt-card__genre { font-size: 11px; font-weight: 700; color: #c084fc; } +.yt-prompt-card__countries { font-size: 10px; color: #6b7280; } + +.yt-prompt-card__text { + display: block; + width: 100%; + text-align: left; + background: #110820; + border: none; + border-radius: 4px; + padding: 6px 8px; + font-size: 11px; + font-family: monospace; + color: #e9d5ff; + line-height: 1.6; + cursor: pointer; + transition: background 0.15s; +} + +.yt-prompt-card__text:hover { background: #1a0d30; } + +.yt-prompt-card__copied { font-size: 10px; color: #22c55e; margin-top: 4px; display: block; } +.yt-prompt-card__reason { font-size: 10px; color: #6b7280; margin-top: 5px; } + +.yt-report-list { display: flex; flex-direction: column; gap: 4px; } + +.yt-report-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + transition: background 0.15s; +} + +.yt-report-row:hover { background: #1f2937; } +.yt-report-row.is-selected { background: #1f2937; } + +.yt-report-row__date { font-size: 11px; color: #ccc; } +.yt-report-row__today { font-size: 10px; color: #22c55e; margin-left: 4px; } +.yt-report-row__meta { font-size: 10px; color: #9ca3af; } +.yt-report-row__action { font-size: 11px; color: #60a5fa; } + +@media (max-width: 600px) { + .yt-dash-cards { grid-template-columns: 1fr 1fr; } + .yt-form-grid { grid-template-columns: 1fr; } + .yt-table__header, + .yt-table__row { grid-template-columns: 2fr 1fr 1fr 28px; } + .yt-table__header span:nth-child(4), + .yt-table__header span:nth-child(5), + .yt-table__row span:nth-child(4), + .yt-table__row span:nth-child(5) { display: none; } +} diff --git a/src/pages/music/MusicStudio.jsx b/src/pages/music/MusicStudio.jsx index da55ed3..e7a89f5 100644 --- a/src/pages/music/MusicStudio.jsx +++ b/src/pages/music/MusicStudio.jsx @@ -27,6 +27,7 @@ import LyricsTab from './components/LyricsTab'; import StemModal from './components/StemModal'; import SyncedLyricsPlayer from './components/SyncedLyricsPlayer'; import RemixTab from './components/RemixTab'; +import YoutubeTab from './components/YoutubeTab'; /* ───────────────────────────────────────────── 데이터 상수 @@ -337,7 +338,7 @@ const TrackResult = ({ track, onDownload, onNew }) => { /* ───────────────────────────────────────────── Library Card ───────────────────────────────────────────── */ -const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, isGenerating }) => { +const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, isGenerating }) => { const [menuOpen, setMenuOpen] = useState(false); const genre = GENRES.find((g) => g.id === track.genre); const totalSec = track.duration_sec ?? null; @@ -425,6 +426,9 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo disabled={isGenerating || !track.lyrics}>📝 Synced Lyrics + )} @@ -447,7 +451,7 @@ const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemo /* ───────────────────────────────────────────── Library Section ───────────────────────────────────────────── */ -const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, isGenerating, loading }) => { +const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, isGenerating, loading }) => { const [playingId, setPlayingId] = useState(null); const handlePlay = (track) => { @@ -501,6 +505,7 @@ const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCove onStemSplit={onStemSplit} onSyncedLyrics={onSyncedLyrics} onVideoGenerate={onVideoGenerate} + onVideoProject={onVideoProject} isGenerating={isGenerating} /> ))} @@ -515,6 +520,7 @@ const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCove export default function MusicStudio() { /* ── 탭 ── */ const [tab, setTab] = useState('create'); + const [initialTrackId, setInitialTrackId] = useState(null); /* ── Provider 상태 ── */ const [providers, setProviders] = useState([]); @@ -1058,6 +1064,11 @@ export default function MusicStudio() { } }; + const handleVideoProject = (track) => { + setInitialTrackId(track.id); + setTab('youtube'); + }; + const handleNewTrack = () => { setTrack(null); setGenProgress(0); @@ -1121,6 +1132,13 @@ export default function MusicStudio() { > 🔄 Remix + {/* ═══ LIBRARY TAB ═══ */} @@ -1138,6 +1156,7 @@ export default function MusicStudio() { onStemSplit={handleStemSplit} onSyncedLyrics={handleSyncedLyrics} onVideoGenerate={handleVideoGenerate} + onVideoProject={handleVideoProject} isGenerating={isGenerating} /> @@ -1166,6 +1185,15 @@ export default function MusicStudio() { /> )} + {/* ═══ YOUTUBE TAB ═══ */} + {tab === 'youtube' && ( + setInitialTrackId(null)} + /> + )} + {/* ═══ CREATE TAB ═══ */} {tab === 'create' && (