diff --git a/src/api.js b/src/api.js index d617ee9..ec587ad 100644 --- a/src/api.js +++ b/src/api.js @@ -312,6 +312,28 @@ export function removeVocals(payload) { return apiPost('/api/music/vocal-removal', payload); } +// ── 저장된 가사 CRUD ───────────────────────────────────────────────────────── + +// GET /api/music/lyrics/library → { lyrics: [{ id, title, text, prompt, created_at, updated_at }] } +export function getSavedLyrics() { + return apiGet('/api/music/lyrics/library'); +} + +// POST /api/music/lyrics/library body: { title, text, prompt } +export function saveLyrics(data) { + return apiPost('/api/music/lyrics/library', data); +} + +// PUT /api/music/lyrics/library/:id body: { title?, text?, prompt? } +export function updateLyrics(id, data) { + return apiPut(`/api/music/lyrics/library/${id}`, data); +} + +// DELETE /api/music/lyrics/library/:id +export function deleteLyrics(id) { + return apiDelete(`/api/music/lyrics/library/${id}`); +} + // ── 로또 고도화 API ──────────────────────────────────────────────────────────── // GET /api/lotto/stats/performance diff --git a/src/pages/music/MusicStudio.css b/src/pages/music/MusicStudio.css index 98f8233..89df6ed 100644 --- a/src/pages/music/MusicStudio.css +++ b/src/pages/music/MusicStudio.css @@ -2311,6 +2311,10 @@ .ms-lyrics-card__header { padding: 14px 16px 10px; border-bottom: 1px solid var(--ms-line-2); + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px; } .ms-lyrics-card__title { @@ -2340,11 +2344,75 @@ overflow-y: auto; } +.ms-lyrics-card__date { + font-family: var(--ms-ff-mono); + font-size: 9px; + color: var(--ms-dim); + letter-spacing: 0.04em; + margin-left: auto; + flex-shrink: 0; +} + .ms-lyrics-card__actions { display: flex; gap: 6px; padding: 8px 16px 12px; border-top: 1px solid var(--ms-line-2); + flex-wrap: wrap; +} + +/* ── 수정 모드 ── */ +.ms-lyrics-card.is-editing { + border-color: var(--ms-accent); + box-shadow: 0 0 16px rgba(245, 166, 35, 0.08); +} + +.ms-lyrics-card__title-input { + width: 100%; + background: var(--ms-surface2); + border: 1px solid var(--ms-line); + border-radius: 6px; + padding: 6px 10px; + font-family: var(--ms-ff-disp); + font-size: 16px; + letter-spacing: 0.04em; + color: var(--ms-text); + outline: none; + margin-bottom: 4px; +} + +.ms-lyrics-card__title-input:focus { + border-color: var(--ms-accent); +} + +.ms-lyrics-card__text-input { + width: 100%; + background: var(--ms-surface2); + border: none; + border-top: 1px solid var(--ms-line-2); + border-bottom: 1px solid var(--ms-line-2); + padding: 14px 16px; + font-family: var(--ms-ff-mono); + font-size: 12px; + line-height: 1.8; + color: rgba(255,255,255,0.85); + resize: vertical; + min-height: 200px; + outline: none; +} + +.ms-btn--danger-text { + color: #e85c3a !important; + opacity: 0.7; +} + +.ms-btn--danger-text:hover { + opacity: 1; +} + +.ms-btn--accent.ms-btn--sm { + padding: 3px 10px; + font-size: 11px; } /* ═══════════════════════════════════════════════════ diff --git a/src/pages/music/MusicStudio.jsx b/src/pages/music/MusicStudio.jsx index b4fbb88..fb0d61d 100644 --- a/src/pages/music/MusicStudio.jsx +++ b/src/pages/music/MusicStudio.jsx @@ -10,6 +10,10 @@ import { getMusicCredits, extendMusicTrack, removeVocals, + getSavedLyrics, + saveLyrics, + updateLyrics, + deleteLyrics, } from '../../api'; import './MusicStudio.css'; @@ -613,10 +617,24 @@ const Library = ({ tracks, onDelete, onRefresh, onExtend, onVocalRemoval, isGene const LyricsTab = ({ onUseInCreate }) => { const [lyrPrompt, setLyrPrompt] = useState(''); const [lyrLoading, setLyrLoading] = useState(false); - const [lyrResults, setLyrResults] = useState([]); // [{text, title}] const [lyrError, setLyrError] = useState(null); - const [copied, setCopied] = useState(null); // index + const [copied, setCopied] = useState(null); // id + const [saved, setSaved] = useState([]); // DB에 저장된 가사 + const [loadingSaved, setLoadingSaved] = useState(true); + const [editingId, setEditingId] = useState(null); + const [editTitle, setEditTitle] = useState(''); + const [editText, setEditText] = useState(''); + /* ── 저장된 가사 로드 ── */ + useEffect(() => { + setLoadingSaved(true); + getSavedLyrics() + .then((data) => setSaved(data.lyrics ?? [])) + .catch(() => {}) + .finally(() => setLoadingSaved(false)); + }, []); + + /* ── AI 생성 → 즉시 저장 ── */ const handleGenerate = async () => { if (!lyrPrompt.trim() || lyrLoading) return; setLyrLoading(true); @@ -624,7 +642,12 @@ const LyricsTab = ({ onUseInCreate }) => { try { const res = await generateMusicLyrics(lyrPrompt.trim()); if (res?.text) { - setLyrResults((prev) => [{ text: res.text, title: res.title || '', prompt: lyrPrompt.trim() }, ...prev]); + const record = await saveLyrics({ + title: res.title || '', + text: res.text, + prompt: lyrPrompt.trim(), + }); + setSaved((prev) => [record, ...prev]); } else { setLyrError('가사 생성 결과가 없습니다'); } @@ -635,13 +658,42 @@ const LyricsTab = ({ onUseInCreate }) => { } }; - const handleCopy = (text, idx) => { + /* ── 복사 ── */ + const handleCopy = (text, id) => { navigator.clipboard.writeText(text).then(() => { - setCopied(idx); + setCopied(id); setTimeout(() => setCopied(null), 2000); }); }; + /* ── 삭제 ── */ + const handleDelete = async (id) => { + try { + await deleteLyrics(id); + setSaved((prev) => prev.filter((l) => l.id !== id)); + } catch {} + }; + + /* ── 수정 시작 ── */ + const startEdit = (item) => { + setEditingId(item.id); + setEditTitle(item.title); + setEditText(item.text); + }; + + /* ── 수정 저장 ── */ + const handleSaveEdit = async () => { + if (editingId == null) return; + try { + const updated = await updateLyrics(editingId, { title: editTitle, text: editText }); + setSaved((prev) => prev.map((l) => l.id === editingId ? updated : l)); + setEditingId(null); + } catch {} + }; + + /* ── 수정 취소 ── */ + const cancelEdit = () => setEditingId(null); + return (
@@ -686,16 +738,6 @@ const LyricsTab = ({ onUseInCreate }) => { )}
- {lyrResults.length === 0 && !lyrLoading && ( -
- 🎤 -

프롬프트를 입력하고 가사를 생성해보세요

-

- 생성된 가사는 [Verse], [Chorus] 등 섹션 태그가 포함됩니다 -

-
- )} - {lyrLoading && (
@@ -703,29 +745,99 @@ const LyricsTab = ({ onUseInCreate }) => {
)} + {/* 저장된 가사 목록 */} + {loadingSaved && ( +
+
+

저장된 가사를 불러오는 중...

+
+ )} + + {!loadingSaved && saved.length === 0 && !lyrLoading && ( +
+ 🎤 +

저장된 가사가 없습니다

+

+ 프롬프트를 입력하면 AI가 [Verse], [Chorus] 등 섹션이 포함된 가사를 작성합니다 +

+
+ )} +
- {lyrResults.map((item, idx) => ( -
+ {saved.map((item) => ( +
- {item.title &&

{item.title}

} - {item.prompt} + {editingId === item.id ? ( + setEditTitle(e.target.value)} + placeholder="제목" + /> + ) : ( + <> + {item.title &&

{item.title}

} + {item.prompt} + + )} + + {item.created_at ? new Date(item.created_at).toLocaleDateString('ko-KR') : ''} +
-
{item.text}
+ + {editingId === item.id ? ( +