feat(youtube-tab): MusicStudio YouTube 탭 연결 + CSS + Library 버튼
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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</button>
|
||||
<button type="button" onClick={() => { onVideoGenerate(track); setMenuOpen(false); }}
|
||||
disabled={isGenerating}>🎬 Music Video</button>
|
||||
<button type="button" onClick={() => { onVideoProject(track); setMenuOpen(false); }}>
|
||||
🎯 YouTube 프로젝트
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -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() {
|
||||
>
|
||||
<span className="ms-tab__icon">🔄</span> Remix
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`ms-tab ms-tab--youtube ${tab === 'youtube' ? 'is-active' : ''}`}
|
||||
onClick={() => setTab('youtube')}
|
||||
>
|
||||
<span className="ms-tab__icon">🎯</span> YouTube
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
{/* ═══ LIBRARY TAB ═══ */}
|
||||
@@ -1138,6 +1156,7 @@ export default function MusicStudio() {
|
||||
onStemSplit={handleStemSplit}
|
||||
onSyncedLyrics={handleSyncedLyrics}
|
||||
onVideoGenerate={handleVideoGenerate}
|
||||
onVideoProject={handleVideoProject}
|
||||
isGenerating={isGenerating}
|
||||
/>
|
||||
</PullToRefresh>
|
||||
@@ -1166,6 +1185,15 @@ export default function MusicStudio() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ═══ YOUTUBE TAB ═══ */}
|
||||
{tab === 'youtube' && (
|
||||
<YoutubeTab
|
||||
library={library}
|
||||
initialTrackId={initialTrackId}
|
||||
onClearInitialTrack={() => setInitialTrackId(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ═══ CREATE TAB ═══ */}
|
||||
{tab === 'create' && (
|
||||
<div className="ms-layout">
|
||||
|
||||
Reference in New Issue
Block a user