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} - Base64 인코딩된 오디오 데이터 URL */ export const generateAdvancedSpeech = async ( text: string, config: TTSConfig ): Promise => { 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} - Base64 Data URL 배열 */ export const generateImageGallery = async ( info: BusinessInfo, count: number ): Promise => { 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} - 생성된 비디오의 원격 URL */ export const generateVideoBackground = async ( posterBase64: string, posterMimeType: string, aspectRatio: AspectRatio = '16:9' ): Promise => { 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} imagesData - Base64 인코딩된 이미지 데이터 배열 ({ mimeType, base64 }) * @returns {Promise>} - 선별된 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} - 생성된 설명 */ export const enrichDescriptionWithReviews = async ( name: string, rawDescription: string, reviews: string[], rating: number ): Promise => { 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} - 생성된 CSS 코드 */ export const extractTextEffectFromImage = async ( imageFile: File ): Promise => { 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 || "텍스트 스타일 분석에 실패했습니다."); } };