// Suno API 응답 인터페이스 (백엔드 응답용) interface SunoBackendResponse { audioUrl: string; } /** * 가사 정제 로직 (Python의 _sanitize_lyrics 함수를 JavaScript로 포팅). * Suno AI가 가사를 올바르게 해석하도록 특정 패턴을 [Verse 1]과 같은 태그로 변환합니다. * @param {string} lyrics - 원본 가사 텍스트 * @returns {string} - Suno AI에 최적화된 정제된 가사 텍스트 */ const sanitizeLyrics = (lyrics: string): string => { // 섹션 헤더를 감지하는 정규식 (예: Verse, Chorus, Bridge, Hook, Intro, Outro) const sectionPattern = /^(verse|chorus|bridge|hook|intro|outro)\s*(\d+)?\s*:?/i; const sanitizedLines: string[] = []; // 정제된 가사 라인들을 저장할 배열 const lines = lyrics.split('\n'); // 가사를 줄 단위로 분리 for (const rawLine of lines) { const line = rawLine.trim(); // 각 줄의 앞뒤 공백 제거 if (!line) { sanitizedLines.push(""); // 빈 줄은 그대로 추가 continue; } const match = line.match(sectionPattern); // 섹션 패턴 매칭 시도 if (match) { // 섹션 이름 첫 글자 대문자화 const name = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase(); const number = match[2] || ""; // 섹션 번호 (있으면 사용, 없으면 빈 문자열) // [Verse 1] 형태의 태그로 변환하여 추가 const label = `[${name}${number ? ' ' + number : ''}]`; sanitizedLines.push(label); } else { sanitizedLines.push(line); // 패턴에 해당하지 않으면 원본 줄 추가 } } const result = sanitizedLines.join('\n').trim(); // 정제된 줄들을 다시 합치고 최종 공백 제거 if (!result) throw new Error("정제된 가사가 비어있습니다. Suno AI에 전달할 유효한 가사가 필요합니다."); return result; }; /** * Suno AI를 사용하여 음악을 생성하는 함수입니다. * 백엔드 프록시 서버(/api/suno/generate)를 통해 요청을 전송하여 CORS 문제 및 안정성을 개선했습니다. * * @param {string} rawLyrics - 사용자가 입력한 원본 가사 * @param {string} style - 음악 스타일 (예: Pop, Rock, Acoustic 등) * @param {string} title - 노래 제목 * @param {boolean} isFullSong - 전체 길이 곡 생성 여부 (현재는 사용되지 않지만, API에 따라 달라질 수 있음) * @returns {Promise} - 생성된 음악 파일의 URL */ export const generateSunoMusic = async ( rawLyrics: string, style: string, title: string, isFullSong: boolean, // 현재는 사용되지 않음 isInstrumental: boolean = false // 연주곡 여부 ): Promise => { // 1. 가사 정제 (연주곡이 아닐 때만 수행) let sanitizedLyrics = ""; if (!isInstrumental) { sanitizedLyrics = sanitizeLyrics(rawLyrics); } else { // console.log("연주곡 모드: 가사 생성 생략"); // 제거 } // 2. 요청 페이로드 구성 (Suno OpenAPI v1 Spec 준수) const payload = { // Custom Mode에서 instrumental이 false이면 prompt는 가사로 사용됨. // instrumental이 true이면 prompt는 사용되지 않음 (하지만 예제에는 포함되어 있으므로 안전하게 유지). prompt: isInstrumental ? "" : sanitizedLyrics, style: style, // tags -> style 로 복구 title: title.substring(0, 80), // V4/V4_5ALL 기준 80자 제한 안전하게 적용 customMode: true, instrumental: isInstrumental, // make_instrumental -> instrumental 로 복구 model: "V5", // 필수 필드: V5 사용 callBackUrl: "https://api.example.com/callback" // 필수 필드: 더미 URL이라도 보내야 함 }; try { // console.log("백엔드를 통해 Suno 음악 생성 요청 시작..."); // 제거 // 백엔드 엔드포인트 호출 (프록시 사용 X, 로컬 서버 사용) const response = await fetch('/api/suno/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` // 인증 토큰 포함 }, body: JSON.stringify(payload) // JSON 페이로드 전송 }); if (!response.ok) { const errorJson = await response.json().catch(() => ({ error: "Unknown Error" })); console.error("Suno Backend Error:", errorJson); throw new Error(`음악 생성 실패 (서버): ${errorJson.error || response.statusText}`); } const resData: SunoBackendResponse = await response.json(); if (!resData.audioUrl) { throw new Error("서버에서 오디오 URL을 반환하지 않았습니다."); } console.log(`생성 완료! Audio URL: ${resData.audioUrl}`); return resData.audioUrl; } catch (e: any) { console.error("Suno 서비스 오류 발생", e); throw new Error(e.message || "음악 생성 중 알 수 없는 오류가 발생했습니다."); } };