/** * 콘텐츠 스튜디오 이미지 생성 (Vercel 서버 함수). POST { channel, format, pillarId } → { ok, imageDataUrl, prompt } * * Gemini 키는 서버 환경변수 GEMINI_API_KEY 에만 둔다 (VITE_ 접두사·vite.config define 금지). * 프롬프트는 브라우저가 아니라 여기서 선택값으로 만든다. 배경은 api/_studio.js 주석. */ import { guard, readSelection } from './_studio.js'; // 이미지·렌더 요청은 외부 API 응답을 기다리므로 실행 시간 한도를 명시한다. export const config = { maxDuration: 60 }; const MODEL = 'gemini-2.5-flash-image'; const PILLAR_DESCRIPTIONS = { safety: 'hospital safety systems, clean surgical rooms, CCTV monitoring, professional medical team, trust and reliability', expertise: 'medical expertise, advanced surgical equipment, professional doctors, medical certifications, precision and skill', results: 'natural beautiful results, before and after transformation, patient satisfaction, subtle elegant outcomes', care: 'patient-centered care, warm consultation, personalized treatment plan, caring medical staff, comfortable clinic interior', }; const CHANNEL_HINTS = { youtube: 'YouTube thumbnail style, bold text overlay area, high contrast, eye-catching', instagram: 'Instagram aesthetic, clean minimalist, lifestyle photography style, warm tones', naver_blog: 'Korean blog header image, informative, medical illustration, clean layout', tiktok: 'TikTok cover image, vertical format, trendy, dynamic, youthful', facebook: 'Facebook ad creative, professional, compelling, clear messaging space', }; function buildPrompt(sel) { return [ 'Generate a premium medical marketing image for a plastic surgery clinic.', `Theme: ${sel.pillarId ? PILLAR_DESCRIPTIONS[sel.pillarId] : ''}`, `Style: ${CHANNEL_HINTS[sel.channel] ?? ''}`, `Aspect ratio: ${sel.aspectRatio}`, 'Color palette: soft purple (#7B2D8E), gold (#E8B931), warm white (#FAF8F5).', 'Premium, luxurious, trustworthy aesthetic.', 'No text or logos in the image.', 'Photorealistic, high quality, professional medical marketing.', ].filter(Boolean).join(' '); } export default async function handler(req, res) { if (guard(req, res, ['POST'])) return; const key = process.env.GEMINI_API_KEY; if (!key) { console.error('[api/gemini-image] GEMINI_API_KEY 가 설정되지 않았습니다.'); return res.status(500).json({ ok: false, error: 'unconfigured' }); } const sel = readSelection(req.body); if (!sel) return res.status(400).json({ ok: false, error: 'bad selection' }); const prompt = buildPrompt(sel); let upstream; try { upstream = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': key }, body: JSON.stringify({ contents: [{ role: 'user', parts: [{ text: prompt }] }], generationConfig: { responseModalities: ['TEXT', 'IMAGE'] }, }), }); } catch (e) { console.error('[api/gemini-image] 연결 실패', e); return res.status(502).json({ ok: false, error: 'upstream unreachable' }); } const data = await upstream.json().catch(() => null); if (!upstream.ok) { // 브라우저에는 상태만 넘기고, Google 의 상세 메시지는 서버 로그에만 남긴다. console.error('[api/gemini-image] 업스트림 오류', upstream.status, JSON.stringify(data?.error ?? {}).slice(0, 300)); const status = upstream.status === 429 ? 429 : upstream.status === 400 ? 400 : 502; return res.status(status).json({ ok: false, error: status === 429 ? 'rate limited' : 'upstream error' }); } const parts = data?.candidates?.[0]?.content?.parts ?? []; const image = parts.find((p) => p.inlineData?.mimeType?.startsWith('image/'))?.inlineData; if (!image) return res.status(502).json({ ok: false, error: 'no image' }); return res.status(200).json({ ok: true, imageDataUrl: `data:${image.mimeType};base64,${image.data}`, prompt }); }