브라우저가 공개 키로 테이블을 직접 읽고 쓰던 구조를 없앤다.
- api/db.js: 정해진 작업만 서비스 키로 실행 (임의 쿼리·열 지정 불가)
- api/_origin.js: 서버 함수 공용 출처 검사, 공개 배포에서는 404
- src/lib/db.ts: 화면용 호출 함수, 반환 형태 { data, error } 유지
- 빌드 큐에 바로 넣던 예전 진단 신청 모달 삭제
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
43 lines
2.3 KiB
JavaScript
43 lines
2.3 KiB
JavaScript
/**
|
|
* 콘텐츠 스튜디오 서버 공용 모듈 (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 };
|
|
}
|