diff --git a/docs/superpowers/plans/2026-05-01-music-youtube-tab-frontend.md b/docs/superpowers/plans/2026-05-01-music-youtube-tab-frontend.md
new file mode 100644
index 0000000..1e1a2c5
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-01-music-youtube-tab-frontend.md
@@ -0,0 +1,1761 @@
+# Music YouTube Tab Frontend Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** MusicStudio ํ์ด์ง์ ๐ฏ YouTube ํญ์ ์ถ๊ฐํ๊ณ , ์์ ์ ์ / ์์ต ์ถ์ / ์์ฅ ํธ๋ ๋ 3๊ฐ ์๋ธํญ์ ๊ตฌํํ๋ค.
+
+**Architecture:** MusicStudio.jsx์ ๊ธฐ์กด ํญ state์ `'youtube'`๋ฅผ ์ถ๊ฐํ๊ณ YoutubeTab ์ปดํฌ๋ํธ๋ฅผ ์กฐ๊ฑด๋ถ ๋ ๋๋งํ๋ค. YoutubeTab์ ์๋ธํญ state๋ฅผ ๊ฐ๊ณ VideoProjectsTab / RevenueTab / TrendsTab์ ๋ ๋๋งํ๋ค. Library ํญ์ LibraryCard์ "YouTube ํ๋ก์ ํธ" ๋ฒํผ์ ์ถ๊ฐํด ํธ๋์ pre-selectํ ์ฑ YouTube ํญ์ผ๋ก ์ด๋ํ ์ ์๋ค.
+
+**Tech Stack:** React 18, Vite, plain fetch API helper (apiGet/apiPost/apiPut/apiDelete), CSS (MusicStudio.css ํ์ฅ)
+
+---
+
+## ํ์ผ ๊ตฌ์กฐ
+
+| ํ์ผ | ๋ณ๊ฒฝ |
+|------|------|
+| `src/api.js` | ๋น๋์ค/์์ต/ํธ๋ ๋ API ํจ์ ์ถ๊ฐ (ํ์ผ ๋์ append) |
+| `src/pages/music/MusicStudio.jsx` | YouTube ํญ ๋ฒํผ, YoutubeTab ๋ ๋๋ง, LibraryCard ๋ฒํผ, initialTrackId state |
+| `src/pages/music/MusicStudio.css` | `.yt-*` CSS ํด๋์ค ์ถ๊ฐ |
+| `src/pages/music/components/YoutubeTab.jsx` | ์ ๊ท โ ์๋ธํญ shell |
+| `src/pages/music/components/VideoProjectsTab.jsx` | ์ ๊ท โ ์์ ์ ์ ์๋ธํญ |
+| `src/pages/music/components/RevenueTab.jsx` | ์ ๊ท โ ์์ต ์ถ์ ์๋ธํญ |
+| `src/pages/music/components/TrendsTab.jsx` | ์ ๊ท โ ์์ฅ ํธ๋ ๋ ์๋ธํญ |
+
+---
+
+## Task 1: Feature ๋ธ๋์น ์์ฑ + API ํจ์ ์ถ๊ฐ
+
+**์์
์์น:** `/Users/jaeohpark/development/web-page/`
+
+**Files:**
+- Modify: `src/api.js` (ํ์ผ ๋์ append)
+
+- [ ] **Step 1: Feature ๋ธ๋์น ์์ฑ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git checkout -b feat/music-youtube-tab
+```
+
+- [ ] **Step 2: `src/api.js` ํ์ผ ๋์ YouTube/Revenue/Trends API ํจ์ ์ถ๊ฐ**
+
+ํ์ฌ ํ์ผ์ 628ํ. ํ์ผ ๋(`triggerLottoCurate` ํจ์ ๋ซ๋ ๋ธ๋ ์ด์ค ๋ค์)์ ์๋๋ฅผ ์ถ๊ฐํ๋ค.
+
+```js
+// โโ Music Lab โ Video Projects โโโโโโโโโโโโโโโโโโโโ
+export const createVideoProject = (data) => apiPost('/api/music/video-project', data);
+export const getVideoProjects = () => apiGet('/api/music/video-projects');
+export const renderVideoProject = (id) => apiPost(`/api/music/video-project/${id}/render`);
+export const exportVideoProject = (id) => apiGet(`/api/music/video-project/${id}/export`);
+export const deleteVideoProject = (id) => apiDelete(`/api/music/video-project/${id}`);
+
+// โโ Music Lab โ Revenue โโโโโโโโโโโโโโโโโโโโโโโโโโโ
+export const getRevenueDashboard = () => apiGet('/api/music/revenue/dashboard');
+export const getRevenueRecords = () => apiGet('/api/music/revenue');
+export const addRevenueRecord = (data) => apiPost('/api/music/revenue', data);
+export const updateRevenueRecord = (id, data) => apiPut(`/api/music/revenue/${id}`, data);
+export const deleteRevenueRecord = (id) => apiDelete(`/api/music/revenue/${id}`);
+
+// โโ Music Lab โ Market Trends โโโโโโโโโโโโโโโโโโโโโ
+export const getLatestTrendReport = () => apiGet('/api/music/market/report/latest');
+export const getTrendReports = () => apiGet('/api/music/market/report');
+export const getMarketSuggestions = () => apiGet('/api/music/market/suggest');
+export const triggerYoutubeResearch = () => apiPost('/api/agent-office/youtube/research', {});
+```
+
+- [ ] **Step 3: ๋ธ๋ผ์ฐ์ ํ์ธ ๋ถํ์ (ํจ์๋ง ์ถ๊ฐ, ํ์
์ค๋ฅ ์์). dev ์๋ฒ ์์ ํ์ธ.**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+npm run dev
+```
+
+Expected: ์ฝ์์ ์๋ฌ ์์ด `http://localhost:5173` (๋๋ ํฌํธ ์ถ๋ ฅ) ๊ธฐ๋.
+
+- [ ] **Step 4: ์ปค๋ฐ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git add src/api.js
+git commit -m "feat(api): video-project / revenue / market-trends API ํจ์ ์ถ๊ฐ"
+```
+
+---
+
+## Task 2: YoutubeTab.jsx โ ์๋ธํญ shell
+
+**์์
์์น:** `/Users/jaeohpark/development/web-page/`
+
+**Files:**
+- Create: `src/pages/music/components/YoutubeTab.jsx`
+
+- [ ] **Step 1: `YoutubeTab.jsx` ์์ฑ**
+
+```jsx
+// src/pages/music/components/YoutubeTab.jsx
+import { useState, useEffect } from 'react';
+import VideoProjectsTab from './VideoProjectsTab';
+import RevenueTab from './RevenueTab';
+import TrendsTab from './TrendsTab';
+
+export default function YoutubeTab({ library, initialTrackId, onClearInitialTrack }) {
+ const [subtab, setSubtab] = useState('video');
+
+ // initialTrackId๊ฐ ๋ค์ด์ค๋ฉด video ์๋ธํญ์ผ๋ก ์ ํ
+ useEffect(() => {
+ if (initialTrackId) setSubtab('video');
+ }, [initialTrackId]);
+
+ return (
+
+
+
+ {subtab === 'video' && (
+
+ )}
+ {subtab === 'revenue' && }
+ {subtab === 'trends' && }
+
+ );
+}
+```
+
+- [ ] **Step 2: ์ปค๋ฐ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git add src/pages/music/components/YoutubeTab.jsx
+git commit -m "feat(youtube-tab): YoutubeTab ์๋ธํญ shell ์ปดํฌ๋ํธ"
+```
+
+---
+
+## Task 3: VideoProjectsTab.jsx โ ์์ ์ ์ ์๋ธํญ
+
+**์์
์์น:** `/Users/jaeohpark/development/web-page/`
+
+**Files:**
+- Create: `src/pages/music/components/VideoProjectsTab.jsx`
+
+> **์ฐธ๊ณ :** `video-projects` API ์๋ต ํ์์ `{ projects: [...] }` ๋๋ ๋ฐฐ์ด ์ง์ ๋ฐํ์ผ ์ ์๋ค. ์์ชฝ ๋ชจ๋ ์ฒ๋ฆฌํ๋ค.
+
+- [ ] **Step 1: `VideoProjectsTab.jsx` ์์ฑ**
+
+```jsx
+// src/pages/music/components/VideoProjectsTab.jsx
+import { useState, useEffect, useRef } from 'react';
+import {
+ createVideoProject, getVideoProjects,
+ renderVideoProject, exportVideoProject, deleteVideoProject,
+} from '../../../api';
+
+const COUNTRY_OPTIONS = ['BR', 'US', 'ID', 'MX', 'KR'];
+const COUNTRY_FLAGS = { BR: '๐ง๐ท', US: '๐บ๐ธ', ID: '๐ฎ๐ฉ', MX: '๐ฒ๐ฝ', KR: '๐ฐ๐ท' };
+
+export default function VideoProjectsTab({ library, initialTrackId, onClearInitialTrack }) {
+ const [projects, setProjects] = useState([]);
+ const [selectedTrackId, setSelectedTrackId] = useState(initialTrackId ?? '');
+ const [format, setFormat] = useState('visualizer');
+ const [countries, setCountries] = useState(['BR']);
+ const [creating, setCreating] = useState(false);
+ const [exportData, setExportData] = useState(null);
+ const [exportingId, setExportingId] = useState(null);
+ const pollRef = useRef(null);
+
+ // initialTrackId prop ๋ฐ์
+ useEffect(() => {
+ if (initialTrackId) {
+ setSelectedTrackId(String(initialTrackId));
+ onClearInitialTrack?.();
+ }
+ }, [initialTrackId]);
+
+ const loadProjects = async () => {
+ try {
+ const data = await getVideoProjects();
+ setProjects(Array.isArray(data) ? data : data.projects ?? []);
+ } catch (e) {
+ console.error('getVideoProjects:', e);
+ }
+ };
+
+ useEffect(() => { loadProjects(); }, []);
+
+ // ๋ ๋๋ง ์ค์ธ ํ๋ก์ ํธ๊ฐ ์์ผ๋ฉด 5์ด๋ง๋ค ํด๋ง
+ useEffect(() => {
+ const hasRendering = projects.some(p => p.status === 'rendering');
+ if (hasRendering && !pollRef.current) {
+ pollRef.current = setInterval(loadProjects, 5000);
+ } else if (!hasRendering && pollRef.current) {
+ clearInterval(pollRef.current);
+ pollRef.current = null;
+ }
+ return () => {
+ if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
+ };
+ }, [projects]);
+
+ const toggleCountry = (c) => {
+ setCountries(prev =>
+ prev.includes(c) ? prev.filter(x => x !== c) : [...prev, c]
+ );
+ };
+
+ const handleCreate = async () => {
+ if (!selectedTrackId || countries.length === 0) return;
+ setCreating(true);
+ try {
+ await createVideoProject({
+ track_id: Number(selectedTrackId),
+ format,
+ target_countries: countries,
+ });
+ await loadProjects();
+ } catch (e) {
+ console.error('createVideoProject:', e);
+ } finally {
+ setCreating(false);
+ }
+ };
+
+ const handleRender = async (id) => {
+ try {
+ await renderVideoProject(id);
+ await loadProjects();
+ } catch (e) {
+ console.error('renderVideoProject:', e);
+ }
+ };
+
+ const handleExport = async (id) => {
+ setExportingId(id);
+ try {
+ const data = await exportVideoProject(id);
+ setExportData({ id, ...data });
+ } catch (e) {
+ console.error('exportVideoProject:', e);
+ } finally {
+ setExportingId(null);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!window.confirm('์ด ํ๋ก์ ํธ๋ฅผ ์ญ์ ํ ๊น์?')) return;
+ try {
+ await deleteVideoProject(id);
+ setProjects(prev => prev.filter(p => p.id !== id));
+ if (exportData?.id === id) setExportData(null);
+ } catch (e) {
+ console.error('deleteVideoProject:', e);
+ }
+ };
+
+ return (
+
+ {/* โ ์ ์์ ๋ง๋ค๊ธฐ */}
+
+
โ ์ ์์ ๋ง๋ค๊ธฐ
+
+
+
+ {['visualizer', 'slideshow'].map(f => (
+
+ ))}
+
+
+
ํ๊ฒ ๊ตญ๊ฐ (๋ณต์ ์ ํ)
+
+ {COUNTRY_OPTIONS.map(c => (
+
+ ))}
+
+
+
+
+ {/* โก ํ๋ก์ ํธ ๋ชฉ๋ก */}
+
+
โก ์์ ํ๋ก์ ํธ
+ {projects.length === 0 ? (
+
ํธ๋์ ์ ํํด ์์์ ๋ง๋ค์ด๋ณด์ธ์
+ ) : (
+
+ {projects.map(p => (
+
+ ))}
+
+ )}
+
+
+ {/* โข ๋ด๋ณด๋ด๊ธฐ ํจํค์ง */}
+ {exportData && (
+
+
โข ๋ด๋ณด๋ด๊ธฐ ํจํค์ง
+
+ {exportData.metadata && (
+
+
metadata.json ๋ฏธ๋ฆฌ๋ณด๊ธฐ
+
+ {JSON.stringify(exportData.metadata, null, 2)}
+
+
+ )}
+
+ )}
+
+ );
+}
+
+function ProjectCard({ project, onRender, onExport, onDelete, isExporting }) {
+ const STATUS_MAP = {
+ pending: { text: '๋๊ธฐ', cls: 'yt-status--pending' },
+ rendering: { text: 'โ ์ฒ๋ฆฌ์ค', cls: 'yt-status--rendering' },
+ done: { text: 'โ ์๋ฃ', cls: 'yt-status--done' },
+ failed: { text: '์คํจ', cls: 'yt-status--failed' },
+ };
+ const s = STATUS_MAP[project.status] ?? { text: project.status, cls: '' };
+
+ return (
+
+
+ {project.status === 'rendering' ? 'โ๏ธ' : project.status === 'done' ? '๐ฌ' : '๐ต'}
+
+
+
+ {project.title ?? `ํ๋ก์ ํธ #${project.id}`}
+
+
+ {project.format} ยท {(project.target_countries ?? []).join(' ')}
+
+ {project.status === 'rendering' && (
+
+ )}
+
+
{s.text}
+ {project.status === 'pending' && (
+
+ )}
+ {project.status === 'done' && (
+
+ )}
+
+
+ );
+}
+```
+
+- [ ] **Step 2: ์ปค๋ฐ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git add src/pages/music/components/VideoProjectsTab.jsx
+git commit -m "feat(youtube-tab): VideoProjectsTab ์์ ์ ์ ์๋ธํญ"
+```
+
+---
+
+## Task 4: RevenueTab.jsx โ ์์ต ์ถ์ ์๋ธํญ
+
+**Files:**
+- Create: `src/pages/music/components/RevenueTab.jsx`
+
+- [ ] **Step 1: `RevenueTab.jsx` ์์ฑ**
+
+```jsx
+// src/pages/music/components/RevenueTab.jsx
+import { useState, useEffect } from 'react';
+import {
+ getRevenueDashboard, getRevenueRecords,
+ addRevenueRecord, updateRevenueRecord, deleteRevenueRecord,
+} from '../../../api';
+
+const COUNTRIES = ['BR', 'US', 'ID', 'MX', 'KR'];
+const currentMonth = () => new Date().toISOString().slice(0, 7);
+
+export default function RevenueTab() {
+ const [dashboard, setDashboard] = useState(null);
+ const [records, setRecords] = useState([]);
+ const [form, setForm] = useState({
+ yt_video_id: '', record_month: currentMonth(),
+ revenue_usd: '', views: '', country: 'BR',
+ });
+ const [saving, setSaving] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [editForm, setEditForm] = useState({});
+
+ const loadAll = async () => {
+ const [dash, recs] = await Promise.all([
+ getRevenueDashboard().catch(() => null),
+ getRevenueRecords().catch(() => []),
+ ]);
+ setDashboard(dash);
+ setRecords(Array.isArray(recs) ? recs : recs.records ?? []);
+ };
+
+ useEffect(() => { loadAll(); }, []);
+
+ const handleAdd = async () => {
+ if (!form.yt_video_id || !form.revenue_usd || !form.views) return;
+ setSaving(true);
+ try {
+ await addRevenueRecord({
+ yt_video_id: form.yt_video_id,
+ record_month: form.record_month,
+ revenue_usd: parseFloat(form.revenue_usd),
+ views: parseInt(form.views, 10),
+ country: form.country,
+ });
+ setForm({ yt_video_id: '', record_month: currentMonth(), revenue_usd: '', views: '', country: 'BR' });
+ await loadAll();
+ } catch (e) {
+ console.error('addRevenueRecord:', e);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handleEditSave = async () => {
+ try {
+ await updateRevenueRecord(editingId, {
+ revenue_usd: parseFloat(editForm.revenue_usd),
+ views: parseInt(editForm.views, 10),
+ });
+ setEditingId(null);
+ await loadAll();
+ } catch (e) {
+ console.error('updateRevenueRecord:', e);
+ }
+ };
+
+ const handleDelete = async (id) => {
+ if (!window.confirm('์ด ๊ธฐ๋ก์ ์ญ์ ํ ๊น์?')) return;
+ try {
+ await deleteRevenueRecord(id);
+ await loadAll();
+ } catch (e) {
+ console.error('deleteRevenueRecord:', e);
+ }
+ };
+
+ // ์์๋ณ RPM ์์ 5๊ฐ (bar chart ์ฉ)
+ const chartData = records
+ .filter(r => r.views > 0)
+ .map(r => ({
+ label: r.yt_video_id,
+ rpm: (r.revenue_usd / r.views) * 1000,
+ }))
+ .sort((a, b) => b.rpm - a.rpm)
+ .slice(0, 5);
+ const maxRpm = chartData.length > 0 ? Math.max(...chartData.map(d => d.rpm)) : 1;
+
+ return (
+
+ {/* ๋์๋ณด๋ ์นด๋ 3๊ฐ */}
+
+
+
์ด ์์ต
+
+ ${dashboard?.total_revenue_usd?.toFixed(2) ?? 'โ'}
+
+
๋์
+
+
+
์ด ์กฐํ์
+
+ {dashboard?.total_views != null
+ ? (dashboard.total_views >= 1000
+ ? `${(dashboard.total_views / 1000).toFixed(1)}K`
+ : String(dashboard.total_views))
+ : 'โ'}
+
+
๋์
+
+
+
ํ๊ท RPM
+
+ ${dashboard?.avg_rpm?.toFixed(2) ?? 'โ'}
+
+
๊ฐ์คํ๊ท
+
+
+
+ {/* ์์๋ณ RPM ๋ฐ ์ฐจํธ */}
+ {chartData.length > 0 && (
+
+
์์๋ณ RPM ๋น๊ต
+
+ {chartData.map((d, i) => (
+
+
+ {d.label.slice(0, 11)}
+
+
+
${d.rpm.toFixed(2)}
+
+ ))}
+
+
+ )}
+
+ {/* ์์ต ๊ธฐ๋ก ์ถ๊ฐ ํผ */}
+
+
+ ์์ต ๊ธฐ๋ก ์ถ๊ฐ
+
+
+
+
+
+
+
+ {/* ์์ต ๊ธฐ๋ก ํ
์ด๋ธ */}
+
+
์์ต ๊ธฐ๋ก
+ {records.length === 0 ? (
+
์์ต ๊ธฐ๋ก์ด ์์ต๋๋ค. ์ ํผ์ผ๋ก ์ถ๊ฐํด๋ณด์ธ์.
+ ) : (
+
+
+ ์์ ID
+ ์
+ ์์ต
+ ์กฐํ์
+ RPM
+
+
+ {records.map(rec => (
+ editingId === rec.id ? (
+
+
{rec.yt_video_id.slice(0, 11)}
+
{rec.record_month}
+
setEditForm(f => ({ ...f, revenue_usd: e.target.value }))}
+ />
+
setEditForm(f => ({ ...f, views: e.target.value }))}
+ />
+
โ
+
+
+
+
+
+ ) : (
+
{
+ setEditingId(rec.id);
+ setEditForm({ revenue_usd: rec.revenue_usd, views: rec.views });
+ }}
+ style={{ cursor: 'pointer' }}
+ >
+ {rec.yt_video_id.slice(0, 11)}
+ {rec.record_month}
+ ${rec.revenue_usd?.toFixed(2)}
+ {rec.views?.toLocaleString()}
+
+ {rec.views > 0
+ ? `$${((rec.revenue_usd / rec.views) * 1000).toFixed(2)}`
+ : 'โ'}
+
+
+
+ )
+ ))}
+
+ )}
+
+
+ );
+}
+```
+
+- [ ] **Step 2: ์ปค๋ฐ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git add src/pages/music/components/RevenueTab.jsx
+git commit -m "feat(youtube-tab): RevenueTab ์์ต ์ถ์ ์๋ธํญ"
+```
+
+---
+
+## Task 5: TrendsTab.jsx โ ์์ฅ ํธ๋ ๋ ์๋ธํญ
+
+**Files:**
+- Create: `src/pages/music/components/TrendsTab.jsx`
+
+- [ ] **Step 1: `TrendsTab.jsx` ์์ฑ**
+
+```jsx
+// src/pages/music/components/TrendsTab.jsx
+import { useState, useEffect } from 'react';
+import {
+ getLatestTrendReport, getTrendReports,
+ getMarketSuggestions, triggerYoutubeResearch,
+} from '../../../api';
+
+const FLAG = { BR: '๐ง๐ท', US: '๐บ๐ธ', ID: '๐ฎ๐ฉ', MX: '๐ฒ๐ฝ', KR: '๐ฐ๐ท' };
+
+export default function TrendsTab() {
+ const [latestReport, setLatestReport] = useState(null);
+ const [reports, setReports] = useState([]);
+ const [suggestions, setSuggestions] = useState([]);
+ const [selectedReport, setSelectedReport] = useState(null);
+ const [researching, setResearching] = useState(false);
+ const [copiedIdx, setCopiedIdx] = useState(null);
+
+ const loadAll = async () => {
+ const [latest, rpts, sugg] = await Promise.all([
+ getLatestTrendReport().catch(() => null),
+ getTrendReports().catch(() => []),
+ getMarketSuggestions().catch(() => []),
+ ]);
+ setLatestReport(latest);
+ setReports(Array.isArray(rpts) ? rpts : rpts.reports ?? []);
+ setSuggestions(Array.isArray(sugg) ? sugg : sugg.suggestions ?? []);
+ };
+
+ useEffect(() => { loadAll(); }, []);
+
+ const handleResearch = async () => {
+ setResearching(true);
+ try {
+ await triggerYoutubeResearch();
+ } catch (e) {
+ console.error('triggerYoutubeResearch:', e);
+ } finally {
+ setResearching(false);
+ }
+ };
+
+ const handleCopy = (text, idx) => {
+ navigator.clipboard.writeText(text).then(() => {
+ setCopiedIdx(idx);
+ setTimeout(() => setCopiedIdx(null), 2000);
+ });
+ };
+
+ // ์ ํ๋ ๋ฆฌํฌํธ๊ฐ ์์ผ๋ฉด ๊ทธ๊ฒ, ์์ผ๋ฉด ์ต์ ๋ฆฌํฌํธ์ ์ฅ๋ฅด ํ์
+ const displayReport = selectedReport ?? latestReport;
+ const topGenres = displayReport?.top_genres?.slice(0, 5) ?? [];
+ const maxScore = topGenres.length > 0 ? Math.max(...topGenres.map(g => g.score)) : 1;
+
+ return (
+
+ {/* ์์ง ์ํ ๋ฐ */}
+
+
+
+
+ ๋ง์ง๋ง ์์ง: {latestReport?.report_date ?? '์์'}
+ {latestReport && ` ยท ${latestReport.top_genres?.length ?? 0}๊ฐ ์ฅ๋ฅด`}
+
+
+
+
+
+ {/* ์ธ๊ธฐ ์ฅ๋ฅด Top 5 */}
+
+
๐ฅ ์ค๋์ ์ธ๊ธฐ ์ฅ๋ฅด Top 5
+ {topGenres.length === 0 ? (
+
+ ํธ๋ ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค. ์๋ ์์ง์ ์คํํ๊ฑฐ๋ agent-office๊ฐ ๋ด์ผ 09:00์ ์๋ ์์งํฉ๋๋ค.
+
+ ) : (
+
+ {topGenres.map((g, i) => (
+
+
#{i + 1}
+
+
+ {g.genre}
+
+ {(g.countries ?? []).map(c => FLAG[c] ?? c).join(' ')}
+
+
+
+
+
{g.score}
+
+ ))}
+
+ )}
+
+
+ {/* Suno ํ๋กฌํํธ ์ถ์ฒ */}
+ {suggestions.length > 0 && (
+
+
โจ AI ์ถ์ฒ Suno ํ๋กฌํํธ
+
+ {suggestions.map((s, i) => (
+
+
+ {s.genre}
+
+ {(s.target_countries ?? []).map(c => FLAG[c] ?? c).join(' ')}
+
+
+
+ {copiedIdx === i && (
+
โ ๋ณต์ฌ๋จ
+ )}
+ {s.reason && (
+
{s.reason}
+ )}
+
+ ))}
+
+
+ )}
+
+ {/* ํธ๋ ๋ ๋ฆฌํฌํธ ์ด๋ ฅ */}
+
+
๐ ํธ๋ ๋ ๋ฆฌํฌํธ ์ด๋ ฅ
+ {reports.length === 0 ? (
+
๋ฆฌํฌํธ ์ด๋ ฅ์ด ์์ต๋๋ค
+ ) : (
+
+ {reports.map(r => (
+
setSelectedReport(
+ selectedReport?.report_date === r.report_date ? null : r
+ )}
+ >
+
+ {r.report_date}
+ {r.report_date === latestReport?.report_date && (
+ โ ์ค๋
+ )}
+
+
+ {r.top_genres?.length ?? 0}๊ฐ ์ฅ๋ฅด ยท {r.recommended_styles?.length ?? 0}๊ฐ ์ถ์ฒ
+
+ ๋ณด๊ธฐ โ
+
+ ))}
+
+ )}
+
+
+ );
+}
+```
+
+- [ ] **Step 2: ์ปค๋ฐ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git add src/pages/music/components/TrendsTab.jsx
+git commit -m "feat(youtube-tab): TrendsTab ์์ฅ ํธ๋ ๋ ์๋ธํญ"
+```
+
+---
+
+## Task 6: MusicStudio.jsx ์ฐ๊ฒฐ + CSS + Library ๋ฒํผ
+
+**Files:**
+- Modify: `src/pages/music/MusicStudio.jsx`
+- Modify: `src/pages/music/MusicStudio.css`
+
+### 6-A: MusicStudio.jsx import ์ถ๊ฐ
+
+- [ ] **Step 1: ํ์ผ ์๋จ import ๋ธ๋ก์ YoutubeTab import ์ถ๊ฐ**
+
+`MusicStudio.jsx` ํ์ผ ์๋จ์์ ๊ธฐ์กด import ๋ธ๋ก์ ์ฐพ๋๋ค (RemixTab import ๊ทผ์ฒ). ๊ทธ ์๋์ ์ถ๊ฐ:
+
+```jsx
+import YoutubeTab from './components/YoutubeTab';
+```
+
+๊ธฐ์กด import ๋ธ๋ก ์์ (3~10ํ ๊ทผ์ฒ):
+```jsx
+import LyricsTab from './components/LyricsTab';
+import RemixTab from './components/RemixTab';
+// ์ด ์๋์ ์ถ๊ฐ:
+import YoutubeTab from './components/YoutubeTab';
+```
+
+### 6-B: MusicStudio ํจ์ ๋ด ์ํ ์ถ๊ฐ
+
+- [ ] **Step 2: `tab` state ์ ์ธ ์๋์ `initialTrackId` state ์ถ๊ฐ**
+
+ํ์ฌ 517ํ:
+```jsx
+const [tab, setTab] = useState('create');
+```
+
+์ด ๋ฐ๋ก ์๋์ ์ถ๊ฐ:
+```jsx
+const [initialTrackId, setInitialTrackId] = useState(null);
+```
+
+### 6-C: LibraryCard์ YouTube ํ๋ก์ ํธ ๋ฒํผ ์ถ๊ฐ
+
+- [ ] **Step 3: `LibraryCard` ์ปดํฌ๋ํธ props์ `onVideoProject` ์ถ๊ฐ**
+
+ํ์ฌ 340ํ:
+```jsx
+const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, isGenerating }) => {
+```
+
+์ด๋ฅผ ์๋๋ก ๊ต์ฒด:
+```jsx
+const LibraryCard = ({ track, onDelete, onPlay, isPlaying, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, isGenerating }) => {
+```
+
+- [ ] **Step 4: `hasSunoId` ์กฐ๊ฑด ๋ธ๋ก์ `โขโขโข` ๋๋กญ๋ค์ด ์์ YouTube ํ๋ก์ ํธ ๋ฒํผ ์ถ๊ฐ**
+
+ํ์ฌ 426~427ํ (`๐ฌ Music Video` ๋ฒํผ ๋ค์):
+```jsx
+
+```
+
+์ด ๋ฒํผ **์๋**์ ์ถ๊ฐ:
+```jsx
+
+```
+
+### 6-D: Library ์ปดํฌ๋ํธ์ onVideoProject prop ์ ๋ฌ
+
+- [ ] **Step 5: `Library` ์ปดํฌ๋ํธ props์ `onVideoProject` ์ถ๊ฐ**
+
+ํ์ฌ 450ํ:
+```jsx
+const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, isGenerating, loading }) => {
+```
+
+์ด๋ฅผ ์๋๋ก ๊ต์ฒด:
+```jsx
+const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, onCoverArt, onWavConvert, onStemSplit, onSyncedLyrics, onVideoGenerate, onVideoProject, isGenerating, loading }) => {
+```
+
+- [ ] **Step 6: `Library` ๋ด๋ถ์ `LibraryCard` ๋ ๋๋ง์ `onVideoProject` prop ์ถ๊ฐ**
+
+ํ์ฌ 491~515ํ์ `` ๋ ๋๋ง ๋ธ๋ก์์, ๊ธฐ์กด `onVideoGenerate={onVideoGenerate}` ์๋์:
+```jsx
+onVideoProject={onVideoProject}
+```
+
+์ถ๊ฐํ๋ค.
+
+### 6-E: MusicStudio ํจ์ ๋ด ํธ๋ค๋ฌ ์ถ๊ฐ
+
+- [ ] **Step 7: `handleVideoGenerate` ํธ๋ค๋ฌ ๊ทผ์ฒ์ `handleVideoProject` ํธ๋ค๋ฌ ์ถ๊ฐ**
+
+`handleVideoGenerate` ํจ์๋ฅผ ์ฐพ์ (ํ์ผ ๋ด `onVideoGenerate` ์ฝ๋ฐฑ ์ ์ ์์น) ๊ทธ ๋ฐ๋ก ์๋์ ์ถ๊ฐ:
+
+```jsx
+const handleVideoProject = (track) => {
+ setInitialTrackId(track.id);
+ setTab('youtube');
+};
+```
+
+### 6-F: Library ๋ ๋๋ง ๋ธ๋ก์ prop ์ฐ๊ฒฐ
+
+- [ ] **Step 8: `tab === 'library'` ๋ธ๋ก์ `` ์ปดํฌ๋ํธ์ `onVideoProject` prop ์ถ๊ฐ**
+
+ํ์ฌ 1129~1143ํ์ `` ์ปดํฌ๋ํธ์์, ๊ธฐ์กด `onVideoGenerate={handleVideoGenerate}` ์๋์:
+```jsx
+onVideoProject={handleVideoProject}
+```
+
+์ถ๊ฐํ๋ค.
+
+### 6-G: YouTube ํญ ๋ฒํผ ์ถ๊ฐ
+
+- [ ] **Step 9: ํญ nav์ YouTube ํญ ๋ฒํผ ์ถ๊ฐ**
+
+ํ์ฌ 1117~1123ํ (Remix ํญ ๋ฒํผ):
+```jsx
+
+```
+
+์ด ๋ฒํผ **๋ค์**์ ์ถ๊ฐ:
+```jsx
+
+```
+
+### 6-H: YouTube ํญ ์ฝํ
์ธ ๋ ๋๋ง ์ถ๊ฐ
+
+- [ ] **Step 10: Remix ํญ ๋ ๋ ๋ธ๋ก ๋ค์์ YouTube ํญ ๋ ๋ ๋ธ๋ก ์ถ๊ฐ**
+
+ํ์ฌ 1151~1167ํ (Remix ํญ ๋ ๋):
+```jsx
+{/* โโโ REMIX TAB โโโ */}
+{tab === 'remix' && (
+
+)}
+```
+
+์ด ๋ธ๋ก **๋ค์**์ ์ถ๊ฐ:
+```jsx
+{/* โโโ YOUTUBE TAB โโโ */}
+{tab === 'youtube' && (
+ setInitialTrackId(null)}
+ />
+)}
+```
+
+### 6-I: CSS ์ถ๊ฐ
+
+- [ ] **Step 11: `MusicStudio.css` ํ์ผ ๋์ YouTube ํญ ์คํ์ผ ์ถ๊ฐ**
+
+```css
+/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ YouTube Tab โ yt-* classes
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+/* YouTube ํญ ๋ฒํผ ๊ฐ์กฐ (amber) */
+.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; }
+}
+```
+
+- [ ] **Step 12: ์ปค๋ฐ**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git add src/pages/music/MusicStudio.jsx src/pages/music/MusicStudio.css
+git commit -m "feat(youtube-tab): MusicStudio YouTube ํญ ์ฐ๊ฒฐ + CSS + Library ๋ฒํผ"
+```
+
+---
+
+## Task 7: ๋ธ๋ผ์ฐ์ ํตํฉ ๊ฒ์ฆ
+
+**์์
์์น:** `/Users/jaeohpark/development/web-page/`
+
+- [ ] **Step 1: dev ์๋ฒ ์์ (์ด๋ฏธ ์คํ ์ค์ด๋ฉด ์คํต)**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+npm run dev
+```
+
+- [ ] **Step 2: ๋ธ๋ผ์ฐ์ ์์ `http://localhost:5173` (๋๋ ์ถ๋ ฅ๋ ํฌํธ) ์ด๊ธฐ**
+
+- [ ] **Step 3: Music ํ์ด์ง โ YouTube ํญ ๋ฒํผ ํด๋ฆญ ํ์ธ**
+
+Expected:
+- ํญ ๋ฐ์ `๐ฏ YouTube` ๋ฒํผ์ด ๋ณด์
+- ํด๋ฆญ ์ 3๊ฐ ์๋ธํญ(`๐ฌ ์์ ์ ์ / ๐ฐ ์์ต ์ถ์ / ๐ ์์ฅ ํธ๋ ๋`) ํ์
+- ์ฝ์ ์๋ฌ ์์
+
+- [ ] **Step 4: ์์ ์ ์ ์๋ธํญ ํ์ธ**
+
+- ํธ๋ ๋๋กญ๋ค์ด์ Library ํธ๋ ๋ชฉ๋ก ํ์ (Library์ ํธ๋์ด ์๋ ๊ฒฝ์ฐ)
+- ๊ตญ๊ฐ ์นฉ BR/US/ID/MX/KR ํด๋ฆญ ํ ๊ธ ๋์
+- ๋น์ฃผ์ผ๋ผ์ด์ /์ฌ๋ผ์ด๋์ผ ํ ๊ธ ๋์
+- "ํ๋ก์ ํธ ์์ฑ" ๋ฒํผ ํด๋ฆญ โ ํธ๋ ๋ฏธ์ ํ ์ ๋นํ์ฑํ ํ์ธ
+
+- [ ] **Step 5: ์์ต ์ถ์ ์๋ธํญ ํ์ธ**
+
+- ๋์๋ณด๋ ์นด๋ 3๊ฐ ํ์ (๋ฐ์ดํฐ ์์ผ๋ฉด `โ` ํ์)
+- ์์ต ์ถ๊ฐ ํผ์ YouTube ID / ์ / ์์ต / ์กฐํ์ / ๊ตญ๊ฐ ์
๋ ฅ ํ ์ ์ฅ
+- ์ ์ฅ ํ ํ
์ด๋ธ์ ๋ ์ฝ๋ ํ์, ๋์๋ณด๋ ์์น ๊ฐฑ์
+
+- [ ] **Step 6: ์์ฅ ํธ๋ ๋ ์๋ธํญ ํ์ธ**
+
+- ์์ง ์ํ ๋ฐ ํ์ (๋ง์ง๋ง ์์ง ์ผ์)
+- ์ฅ๋ฅด Top 5 ๋ฐ ์ฐจํธ ํ์ (๋ฐ์ดํฐ ์๋ ๊ฒฝ์ฐ)
+- Suno ํ๋กฌํํธ ํด๋ฆญ โ ํด๋ฆฝ๋ณด๋ ๋ณต์ฌ + "โ ๋ณต์ฌ๋จ" ๋ฉ์์ง
+- ๋ฆฌํฌํธ ์ด๋ ฅ ํด๋ฆญ โ ํด๋น ๋ ์ง ๋ฐ์ดํฐ๋ก Top 5 ๊ฐฑ์
+
+- [ ] **Step 7: Library ํญ โ ํธ๋ ์นด๋ `โขโขโข` โ `๐ฏ YouTube ํ๋ก์ ํธ` ๋ฒํผ ํ์ธ**
+
+Expected: ํด๋ฆญ ์ YouTube ํญ์ผ๋ก ์ด๋, ํด๋น ํธ๋์ด ๋๋กญ๋ค์ด์ pre-select๋จ
+
+- [ ] **Step 8: ์ต์ข
์ปค๋ฐ ๋ฐ PR ์ค๋น**
+
+```bash
+cd /Users/jaeohpark/development/web-page
+git log --oneline feat/music-youtube-tab ^main
+```
+
+Expected: Task 1~6์์ ๋ง๋ ์ปค๋ฐ 6๊ฐ ํ์.
+
+```bash
+git push -u origin feat/music-youtube-tab
+```