/** * 콘텐츠 스튜디오 서버 공용 모듈 (Vercel 서버 함수 전용, '_' 로 시작해 주소로 열리지 않는다). * * 왜 서버에 두는가 * Gemini·Creatomate 키는 브라우저로 내려가면 공개다. 2026-10-07 공개 번들에서 Gemini 키가 발견됐고, * 새 Gemini 키는 서비스 계정 바인딩 키라 웹사이트(리퍼러) 제한을 걸 수 없다. 그래서 호출을 서버로 옮긴다. * 브라우저는 채널·포맷·주제 선택값만 보내고, 프롬프트와 영상 구성은 여기서 만든다. * 임의의 프롬프트·렌더를 대신 실행해 주는 통로가 되지 않도록 선택값은 아래 목록에 있는 것만 받는다. * * 채널·포맷 목록은 src/types/studio.ts 의 CHANNEL_OPTIONS 와 같아야 한다 (서버 함수는 src 의 TS 를 불러올 수 없다). */ import { safeParse } from './_origin.js'; export { guard } from './_origin.js'; export const CHANNELS = { youtube: { label: 'YouTube', formats: { shorts: ['Shorts', '9:16'], long_form: ['Long-form', '16:9'] } }, instagram: { label: 'Instagram', formats: { reels: ['Reels', '9:16'], carousel: ['Carousel', '1:1'], feed_image: ['Feed Image', '1:1'], stories: ['Stories', '9:16'] }, }, naver_blog: { label: 'Naver Blog', formats: { seo_post: ['SEO Post', '16:9'], faq_post: ['FAQ Post', '16:9'] } }, tiktok: { label: 'TikTok', formats: { short_video: ['Short Video', '9:16'] } }, facebook: { label: 'Facebook', formats: { ad_creative: ['Ad Creative', '1:1'], retarget_content: ['Retarget Content', '16:9'] } }, }; export const PILLARS = ['safety', 'expertise', 'results', 'care']; /** * 브라우저가 보낸 선택값을 목록과 대조한다. 목록 밖이면 null. * @returns {{ channel: string, channelLabel: string, format: string, formatLabel: string, aspectRatio: string, pillarId: string | null } | null} */ export function readSelection(body) { const b = typeof body === 'string' ? safeParse(body) : body ?? {}; const ch = CHANNELS[b?.channel]; const fmt = ch?.formats[b?.format]; if (!ch || !fmt) return null; const pillarId = b?.pillarId == null ? null : String(b.pillarId); if (pillarId !== null && !PILLARS.includes(pillarId)) return null; return { channel: b.channel, channelLabel: ch.label, format: b.format, formatLabel: fmt[0], aspectRatio: fmt[1], pillarId }; }