import { CrawlingResponse, LyricGenerateRequest, LyricGenerateResponse, LyricStatusResponse, LyricDetailResponse, SongGenerateRequest, SongGenerateResponse, SongStatusResponse, SongDownloadResponse, VideoGenerateResponse, VideoStatusResponse, VideoDownloadResponse, ImageUrlItem, ImageUploadResponse, } from '../types/api'; const API_URL = import.meta.env.VITE_API_URL || 'http://40.82.133.44'; // 크롤링 타임아웃: 5분 const CRAWL_TIMEOUT = 5 * 60 * 1000; export async function crawlUrl(url: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CRAWL_TIMEOUT); try { const response = await fetch(`${API_URL}/crawling`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url }), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error('크롤링 요청 시간이 초과되었습니다. 다시 시도해주세요.'); } throw error; } } // 가사 생성 API export async function generateLyric(request: LyricGenerateRequest): Promise { const response = await fetch(`${API_URL}/lyric/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(request), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 가사 상태 조회 API export async function getLyricStatus(taskId: string): Promise { const response = await fetch(`${API_URL}/lyric/status/${taskId}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 가사 상세 조회 API export async function getLyricDetail(taskId: string): Promise { const response = await fetch(`${API_URL}/lyric/${taskId}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 가사 생성 완료까지 폴링 (2분 타임아웃, 1초 간격) const LYRIC_POLL_TIMEOUT = 2 * 60 * 1000; // 2분 const LYRIC_POLL_INTERVAL = 1000; // 1초 export async function waitForLyricComplete( taskId: string, onStatusChange?: (status: string) => void ): Promise { const startTime = Date.now(); // 재귀적으로 폴링하는 방식으로 변경 (async/await 제대로 동작) const poll = async (): Promise => { // 2분 타임아웃 체크 if (Date.now() - startTime > LYRIC_POLL_TIMEOUT) { throw new Error('TIMEOUT'); } try { const statusResponse = await getLyricStatus(taskId); onStatusChange?.(statusResponse.status); if (statusResponse.status === 'completed') { // 완료되면 상세 조회로 가사 가져오기 const detailResponse = await getLyricDetail(taskId); return detailResponse; } else if (statusResponse.status === 'failed') { throw new Error(statusResponse.error_message || '가사 생성에 실패했습니다.'); } // processing은 대기 후 재시도 await new Promise(resolve => setTimeout(resolve, LYRIC_POLL_INTERVAL)); return poll(); } catch (error) { throw error; } }; return poll(); } // 노래 생성 API (task_id는 URL 경로에 포함) export async function generateSong(taskId: string, request: SongGenerateRequest): Promise { const response = await fetch(`${API_URL}/song/generate/${taskId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(request), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 노래 상태 확인 API (suno_task_id 사용) export async function getSongStatus(sunoTaskId: string): Promise { const response = await fetch(`${API_URL}/song/status/${sunoTaskId}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 노래 다운로드 API export async function downloadSong(taskId: string): Promise { const response = await fetch(`${API_URL}/song/download/${taskId}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 노래 생성 완료까지 폴링 (2분 타임아웃) const POLL_TIMEOUT = 2 * 60 * 1000; // 2분 const POLL_INTERVAL = 5000; // 5초 export async function waitForSongComplete( taskId: string, sunoTaskId: string, onStatusChange?: (status: string) => void ): Promise { const startTime = Date.now(); // 재귀적으로 폴링하는 방식으로 변경 (async/await 제대로 동작) const poll = async (): Promise => { // 2분 타임아웃 체크 if (Date.now() - startTime > POLL_TIMEOUT) { throw new Error('TIMEOUT'); } try { // 상태 확인은 suno_task_id 사용 const statusResponse = await getSongStatus(sunoTaskId); onStatusChange?.(statusResponse.status); // status가 "SUCCESS" (대문자)인 경우 완료 if (statusResponse.status === 'SUCCESS' && statusResponse.success) { // 다운로드는 task_id 사용 const downloadResponse = await downloadSong(taskId); return downloadResponse; } else if (statusResponse.status === 'FAILED' || statusResponse.status === 'failed') { throw new Error('Song generation failed'); } // PENDING, PROCESSING 등은 대기 후 재시도 await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL)); return poll(); } catch (error) { throw error; } }; return poll(); } // 영상 생성 API export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise { const response = await fetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 영상 상태 확인 API export async function getVideoStatus(taskId: string): Promise { const response = await fetch(`${API_URL}/video/status/${taskId}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 영상 다운로드(결과 조회) API export async function downloadVideo(taskId: string): Promise { const response = await fetch(`${API_URL}/video/download/${taskId}`, { method: 'GET', }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // 이미지 업로드 API (multipart/form-data) // 타임아웃: 5분 (많은 이미지 업로드 시 시간이 오래 걸릴 수 있음) const IMAGE_UPLOAD_TIMEOUT = 5 * 60 * 1000; export async function uploadImages( imageUrls: ImageUrlItem[], files: File[] ): Promise { const formData = new FormData(); // URL 이미지들을 images_json으로 전달 if (imageUrls.length > 0) { formData.append('images_json', JSON.stringify(imageUrls)); } // 파일들을 files로 전달 files.forEach((file) => { formData.append('files', file); }); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), IMAGE_UPLOAD_TIMEOUT); try { const response = await fetch(`${API_URL}/image/upload/blob`, { method: 'POST', body: formData, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error('이미지 업로드 시간이 초과되었습니다. 다시 시도해주세요.'); } throw error; } } // 영상 생성 완료까지 폴링 (10분 타임아웃, 3초 간격) const VIDEO_POLL_TIMEOUT = 10 * 60 * 1000; // 10분 const VIDEO_POLL_INTERVAL = 3000; // 3초 export async function waitForVideoComplete( taskId: string, onStatusChange?: (status: string) => void ): Promise { const startTime = Date.now(); // 재귀적으로 폴링하는 방식으로 변경 (async/await 제대로 동작) const poll = async (): Promise => { // 10분 타임아웃 체크 if (Date.now() - startTime > VIDEO_POLL_TIMEOUT) { throw new Error('TIMEOUT'); } try { const statusResponse = await getVideoStatus(taskId); // render_data.status를 전달 (planned, waiting, transcribing, rendering, succeeded, failed) const renderStatus = statusResponse.render_data?.status; onStatusChange?.(renderStatus || statusResponse.status); // render_data.status가 "succeeded"일 때만 완료 if (renderStatus === 'succeeded') { return statusResponse; } else if (renderStatus === 'failed' || statusResponse.status === 'FAILED' || statusResponse.status === 'failed') { throw new Error(statusResponse.error_message || 'Video generation failed'); } // pending, rendering 등은 대기 후 재시도 await new Promise(resolve => setTimeout(resolve, VIDEO_POLL_INTERVAL)); return poll(); } catch (error) { throw error; } }; return poll(); }