327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
import { BusinessInfo, TTSConfig, AspectRatio, Language } from '../types';
|
|
import { decodeBase64, decodeAudioData, bufferToWaveBlob } from './audioUtils';
|
|
|
|
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif'];
|
|
|
|
// Helper to convert File object to base64 Data URL (MIME type 포함)
|
|
export const fileToBase64 = (file: File): Promise<{ base64: string; mimeType: string }> => {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.readAsDataURL(file);
|
|
reader.onload = () => {
|
|
const result = reader.result as string;
|
|
const [header, base64] = result.split(',');
|
|
const mimeType = header.split(':')[1].split(';')[0];
|
|
resolve({ base64, mimeType });
|
|
};
|
|
reader.onerror = error => reject(error);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 비즈니스 정보 검색 (Gemini 2.5 Flash + Google Maps Tool) - 백엔드 프록시 이용
|
|
* @param {string} query - 검색어 (업체명 또는 주소)
|
|
* @returns {Promise<{name: string, description: string, mapLink?: string}>}
|
|
*/
|
|
export const searchBusinessInfo = async (
|
|
query: string
|
|
): Promise<{ name: string; description: string; mapLink?: string }> => {
|
|
try {
|
|
const response = await fetch('/api/gemini/search-business', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}` // 인증 토큰 포함
|
|
},
|
|
body: JSON.stringify({ query })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (e: any) {
|
|
console.error("Search Business (Frontend) Failed:", e);
|
|
throw new Error(e.message || "업체 정보 검색에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 창의적 콘텐츠 생성 (광고 카피 & 가사/스크립트) - 백엔드 프록시 이용
|
|
* @param {BusinessInfo} info - 비즈니스 정보 객체
|
|
* @returns {Promise<{adCopy: string[], lyrics: string}>}
|
|
*/
|
|
export const generateCreativeContent = async (
|
|
info: BusinessInfo
|
|
): Promise<{ adCopy: string[]; lyrics: string }> => {
|
|
try {
|
|
// 이미지 File 객체를 Base64로 변환하여 백엔드로 전달
|
|
const imagesForBackend = await Promise.all(
|
|
info.images.map(async (file) => await fileToBase64(file))
|
|
);
|
|
|
|
const payload = { ...info, images: imagesForBackend };
|
|
|
|
const response = await fetch('/api/gemini/creative-content', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (e: any) {
|
|
console.error("Generate Creative Content (Frontend) Failed:", e);
|
|
throw new Error(e.message || "창의적 콘텐츠 생성에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
// 사용자 설정(성별/톤)을 Gemini 미리 정의된 목소리 이름으로 매핑 (이 함수는 백엔드에서 사용)
|
|
// const getVoiceName = (config: TTSConfig): string => { ... };
|
|
|
|
/**
|
|
* 고급 음성 합성 (TTS) - 백엔드 프록시 이용
|
|
* @param {string} text - 음성으로 변환할 텍스트
|
|
* @param {TTSConfig} config - TTS 설정
|
|
* @returns {Promise<string>} - Base64 인코딩된 오디오 데이터 URL
|
|
*/
|
|
export const generateAdvancedSpeech = async (
|
|
text: string,
|
|
config: TTSConfig
|
|
): Promise<string> => {
|
|
try {
|
|
const response = await fetch('/api/gemini/speech', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ text, config })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
// Base64 오디오 데이터를 Blob URL로 변환하여 반환
|
|
const audioContext = new ((window as any).AudioContext || (window as any).webkitAudioContext)({ sampleRate: 24000 });
|
|
const audioBytes = decodeBase64(data.base64Audio);
|
|
const audioBuffer = await decodeAudioData(audioBytes, audioContext, 24000, 1);
|
|
const wavBlob = bufferToWaveBlob(audioBuffer, audioBuffer.length);
|
|
return URL.createObjectURL(wavBlob);
|
|
|
|
} catch (e: any) {
|
|
console.error("Generate Advanced Speech (Frontend) Failed:", e);
|
|
throw new Error(e.message || "성우 음성 생성에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 광고 포스터 생성 - 백엔드 프록시 이용
|
|
* @param {BusinessInfo} info - 비즈니스 정보 객체
|
|
* @returns {Promise<{ blobUrl: string; base64: string; mimeType: string }>}
|
|
*/
|
|
export const generateAdPoster = async (
|
|
info: BusinessInfo
|
|
): Promise<{ blobUrl: string; base64: string; mimeType: string }> => {
|
|
try {
|
|
const imagesForBackend = await Promise.all(
|
|
info.images.map(async (file) => await fileToBase64(file))
|
|
);
|
|
const payload = { ...info, images: imagesForBackend };
|
|
|
|
const response = await fetch('/api/gemini/ad-poster', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ info: payload })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
// Base64를 Blob URL로 변환
|
|
const byteCharacters = atob(data.base64);
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
}
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
const blob = new Blob([byteArray], { type: data.mimeType });
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
return { blobUrl, base64: data.base64, mimeType: data.mimeType };
|
|
} catch (e: any) {
|
|
console.error("Generate Ad Poster (Frontend) Failed:", e);
|
|
throw new Error(e.message || "광고 포스터 생성에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 다수의 비즈니스 관련 이미지 생성 (갤러리/슬라이드쇼용) - 백엔드 프록시 이용
|
|
* @param {BusinessInfo} info - 비즈니스 정보 객체
|
|
* @param {number} count - 생성할 이미지 개수
|
|
* @returns {Promise<string[]>} - Base64 Data URL 배열
|
|
*/
|
|
export const generateImageGallery = async (
|
|
info: BusinessInfo,
|
|
count: number
|
|
): Promise<string[]> => {
|
|
try {
|
|
const response = await fetch('/api/gemini/image-gallery', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ info, count })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
return data.images;
|
|
} catch (e: any) {
|
|
console.error("Generate Image Gallery (Frontend) Failed:", e);
|
|
throw new Error(e.message || "이미지 갤러리 생성에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 비디오 배경 생성 - 백엔드 프록시 이용
|
|
* @param {string} posterBase64 - Base64 인코딩된 포스터 이미지 데이터
|
|
* @param {string} posterMimeType - 포스터 이미지 MIME 타입
|
|
* @param {AspectRatio} aspectRatio - 비디오 화면 비율
|
|
* @returns {Promise<string>} - 생성된 비디오의 원격 URL
|
|
*/
|
|
export const generateVideoBackground = async (
|
|
posterBase64: string,
|
|
posterMimeType: string,
|
|
aspectRatio: AspectRatio = '16:9'
|
|
): Promise<string> => {
|
|
try {
|
|
const response = await fetch('/api/gemini/video-background', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ posterBase64, posterMimeType, aspectRatio })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
// 서버에서 받은 비디오 URL에 API 키를 붙이지 않고 그대로 반환
|
|
return data.videoUrl;
|
|
} catch (e: any) {
|
|
console.error("Generate Video Background (Frontend) Failed:", e);
|
|
throw new Error(e.message || "비디오 배경 생성에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* AI 이미지 검수 (Gemini Vision) - 백엔드 프록시 이용
|
|
* @param {Array<object>} imagesData - Base64 인코딩된 이미지 데이터 배열 ({ mimeType, base64 })
|
|
* @returns {Promise<Array<object>>} - 선별된 Base64 이미지 데이터 배열
|
|
*/
|
|
export const filterBestImages = async (
|
|
imagesData: { base64: string; mimeType: string }[]
|
|
): Promise<{ base64: string; mimeType: string }[]> => {
|
|
try {
|
|
const response = await fetch('/api/gemini/filter-images', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ imagesData })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
return data.filteredImages;
|
|
} catch (e: any) {
|
|
console.error("Filter Best Images (Frontend) Failed:", e);
|
|
throw new Error(e.message || "이미지 검수에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 리뷰 기반 마케팅 설명 생성 - 백엔드 프록시 이용
|
|
* @param {string} name - 업체명
|
|
* @param {string} rawDescription - 기본 설명
|
|
* @param {string[]} reviews - 고객 리뷰 배열
|
|
* @param {number} rating - 평균 별점
|
|
* @returns {Promise<string>} - 생성된 설명
|
|
*/
|
|
export const enrichDescriptionWithReviews = async (
|
|
name: string,
|
|
rawDescription: string,
|
|
reviews: string[],
|
|
rating: number
|
|
): Promise<string> => {
|
|
try {
|
|
const response = await fetch('/api/gemini/enrich-description', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ name, rawDescription, reviews, rating })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
return data.enrichedDescription;
|
|
} catch (e: any) {
|
|
console.error("Enrich Description (Frontend) Failed:", e);
|
|
throw new Error(e.message || "마케팅 설명 생성에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 이미지에서 텍스트 스타일(CSS) 추출 - 백엔드 프록시 이용
|
|
* @param {File} imageFile - 이미지 File 객체
|
|
* @returns {Promise<string>} - 생성된 CSS 코드
|
|
*/
|
|
export const extractTextEffectFromImage = async (
|
|
imageFile: File
|
|
): Promise<string> => {
|
|
try {
|
|
const imageForBackend = await fileToBase64(imageFile);
|
|
const response = await fetch('/api/gemini/text-effect', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
},
|
|
body: JSON.stringify({ imageFile: imageForBackend })
|
|
});
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || `Server error: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
return data.cssCode;
|
|
} catch (e: any) {
|
|
console.error("Extract Text Effect (Frontend) Failed:", e);
|
|
throw new Error(e.message || "텍스트 스타일 분석에 실패했습니다.");
|
|
}
|
|
}; |