'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; type Mode = 'simple' | 'custom'; type SunoClip = { id: string; title?: string; audioUrl?: string; streamAudioUrl?: string; imageUrl?: string; tags?: string; duration?: number; prompt?: string; }; type TaskState = { taskId: string; status: string; errorMessage?: string; clips: SunoClip[]; updatedAt: number; }; const MODELS = [ { id: 'V4', label: 'V4 (기본)', desc: '안정적 고품질' }, { id: 'V4_5', label: 'V4.5', desc: '최신 · 풍부한 디테일' }, { id: 'V3_5', label: 'V3.5', desc: '빠른 생성' }, ]; const TAG_PRESETS = [ 'k-pop', 'lo-fi', 'city pop', 'ballad', 'edm', 'trap', 'rock', 'jazz', 'acoustic', 'cinematic', 'synthwave', 'ambient', ]; const LS_KEY = 'jsm_studio_task_ids_v2'; const isDone = (s: string) => s === 'SUCCESS' || s === 'FIRST_SUCCESS'; const isFailed = (s: string) => s.includes('FAILED') || s === 'SENSITIVE_WORD_ERROR'; export default function StudioPage() { const [mode, setMode] = useState('simple'); const [model, setModel] = useState('V4'); const [prompt, setPrompt] = useState(''); const [title, setTitle] = useState(''); const [lyrics, setLyrics] = useState(''); const [tags, setTags] = useState(''); const [instrumental, setInstrumental] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [tasks, setTasks] = useState([]); const pollRef = useRef(null); const saveToLS = useCallback((ids: string[]) => { if (typeof window === 'undefined') return; try { localStorage.setItem(LS_KEY, JSON.stringify(ids.slice(0, 20))); } catch { /* noop */ } }, []); const fetchOne = useCallback(async (taskId: string) => { try { const res = await fetch(`/api/studio/status?taskId=${encodeURIComponent(taskId)}`); const json = await res.json(); if (!json.ok) return null; const d = json.data?.data ?? json.data; const status: string = d?.status ?? 'PENDING'; const errMsg: string | undefined = d?.errorMessage; const sunoData: SunoClip[] = d?.response?.sunoData ?? []; return { taskId, status, errorMessage: errMsg, clips: sunoData, updatedAt: Date.now() } as TaskState; } catch { return null; } }, []); const refreshAll = useCallback(async (ids: string[]) => { const results = await Promise.all(ids.map((id) => fetchOne(id))); setTasks((prev) => { const map = new Map(prev.map((t) => [t.taskId, t])); for (const r of results) if (r) map.set(r.taskId, r); return Array.from(map.values()).sort((a, b) => b.updatedAt - a.updatedAt); }); }, [fetchOne]); useEffect(() => { if (typeof window === 'undefined') return; try { const raw = localStorage.getItem(LS_KEY); const ids = raw ? (JSON.parse(raw) as string[]) : []; if (ids.length) { setTasks(ids.map((id) => ({ taskId: id, status: 'PENDING', clips: [], updatedAt: Date.now() }))); refreshAll(ids); } } catch { /* noop */ } }, [refreshAll]); useEffect(() => { if (pollRef.current) window.clearInterval(pollRef.current); const pending = tasks.filter((t) => !isDone(t.status) && !isFailed(t.status)); if (pending.length) { pollRef.current = window.setInterval(() => { refreshAll(pending.map((t) => t.taskId)); }, 8000); } return () => { if (pollRef.current) window.clearInterval(pollRef.current); }; }, [tasks, refreshAll]); const onSubmit = async () => { setError(null); if (mode === 'simple' && !prompt.trim()) { setError('프롬프트를 입력해주세요.'); return; } if (mode === 'custom') { if (!title.trim()) { setError('트랙 제목을 입력해주세요.'); return; } if (!tags.trim()) { setError('스타일 태그를 입력해주세요.'); return; } if (!lyrics.trim() && !instrumental) { setError('가사를 입력하거나 Instrumental을 켜주세요.'); return; } } setSubmitting(true); try { const res = await fetch('/api/studio/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode, model, prompt: prompt.trim(), title: title.trim(), lyrics: lyrics.trim(), tags: tags.trim(), make_instrumental: instrumental, }), }); const json = await res.json(); if (!res.ok || !json.ok) { setError(typeof json.error === 'string' ? json.error : '생성 실패'); return; } const taskId: string | undefined = json.data?.data?.taskId ?? json.data?.taskId; if (!taskId) { setError('응답에서 taskId를 찾지 못했습니다.'); return; } setTasks((prev) => { const next: TaskState[] = [ { taskId, status: 'PENDING', clips: [], updatedAt: Date.now() }, ...prev.filter((t) => t.taskId !== taskId), ]; saveToLS(next.map((t) => t.taskId)); return next; }); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setSubmitting(false); } }; const addTag = (t: string) => { const cur = tags.split(',').map((x) => x.trim()).filter(Boolean); if (cur.includes(t)) return; setTags([...cur, t].join(', ')); }; return (
JAENGSEUNG STUDIO

프롬프트 한 줄로 트랙 만들기

Suno 엔진 기반 · Custom 모드로 가사·태그·보컬까지 세밀 제어

⚡ v1 Studio · Live
{/* 좌측: 제어판 */}
{(['simple', 'custom'] as Mode[]).map((m) => ( ))}
{mode === 'simple' ? (