347 lines
9.9 KiB
TypeScript
347 lines
9.9 KiB
TypeScript
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<CrawlingResponse> {
|
|
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<LyricGenerateResponse> {
|
|
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<LyricStatusResponse> {
|
|
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<LyricDetailResponse> {
|
|
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<LyricDetailResponse> {
|
|
const startTime = Date.now();
|
|
|
|
// 재귀적으로 폴링하는 방식으로 변경 (async/await 제대로 동작)
|
|
const poll = async (): Promise<LyricDetailResponse> => {
|
|
// 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<SongGenerateResponse> {
|
|
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<SongStatusResponse> {
|
|
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<SongDownloadResponse> {
|
|
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<SongDownloadResponse> {
|
|
const startTime = Date.now();
|
|
|
|
// 재귀적으로 폴링하는 방식으로 변경 (async/await 제대로 동작)
|
|
const poll = async (): Promise<SongDownloadResponse> => {
|
|
// 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<VideoGenerateResponse> {
|
|
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<VideoStatusResponse> {
|
|
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<VideoDownloadResponse> {
|
|
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<ImageUploadResponse> {
|
|
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<VideoStatusResponse> {
|
|
const startTime = Date.now();
|
|
|
|
// 재귀적으로 폴링하는 방식으로 변경 (async/await 제대로 동작)
|
|
const poll = async (): Promise<VideoStatusResponse> => {
|
|
// 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();
|
|
}
|