Function bodies 248 total
writeAuditLog function · typescript · L8-L28 (21 LOC)src/lib/audit.ts
export function writeAuditLog(params: {
userId: string;
action: AuditAction;
contentId?: string;
detail?: Record<string, unknown>;
}): void {
prisma.auditLog
.create({
data: {
userId: params.userId,
action: params.action,
contentId: params.contentId ?? null,
detail: params.detail
? (params.detail as Prisma.InputJsonValue)
: Prisma.DbNull,
},
})
.catch((err) => {
console.error("[audit] Failed to write audit log:", err);
});
}getToken function · typescript · L32-L36 (5 LOC)src/lib/buffer.ts
function getToken(): string {
const token = process.env.BUFFER_ACCESS_TOKEN;
if (!token) throw new Error("BUFFER_ACCESS_TOKEN 환경변수가 설정되지 않았습니다");
return token;
}authHeader function · typescript · L39-L43 (5 LOC)src/lib/buffer.ts
function authHeader(token: string): HeadersInit {
return {
Authorization: `Bearer ${token}`,
};
}getBufferProfiles function · typescript · L61-L72 (12 LOC)src/lib/buffer.ts
export async function getBufferProfiles(): Promise<BufferProfile[]> {
const token = getToken();
// Token is sent as an Authorization header — NOT in the URL — to avoid
// leaking it in server logs, proxy logs, or browser history.
const res = await fetch(`${BUFFER_API}/profiles.json`, {
headers: authHeader(token),
});
if (!res.ok) {
throw new Error(`Buffer API 오류: ${res.status} ${res.statusText}`);
}
return res.json();
}createBufferUpdate function · typescript · L78-L134 (57 LOC)src/lib/buffer.ts
export async function createBufferUpdate(params: {
profileIds: string[];
text: string;
scheduledAt?: string; // ISO string
mediaUrl?: string; // 단일 영상 URL (IG feed video — Reels는 아래 주석 참고)
imageUrls?: string[]; // 복수 이미지 URL (캐러셀용)
}): Promise<BufferUpdate> {
const token = getToken();
const body = new URLSearchParams();
// access_token은 Authorization 헤더로 전달하므로 body에 포함하지 않음.
body.append("text", params.text);
body.append("now", params.scheduledAt ? "false" : "true");
for (const id of params.profileIds) {
body.append("profile_ids[]", id);
}
if (params.scheduledAt) {
body.append("scheduled_at", params.scheduledAt);
}
if (params.imageUrls && params.imageUrls.length > 0) {
// 캐러셀 (다중 이미지) — Buffer API: media[picture] 배열
for (const url of params.imageUrls) {
body.append("media[picture][]", url);
}
} else if (params.mediaUrl) {
// Buffer v1 API는 media[video]로 IG 피드 동영상을 예약함.
// 진짜 Reels를 올리려면 Instagram Graph API의 REELS 미buildCardNewsSystemPrompt function · typescript · L4-L92 (89 LOC)src/lib/card-news-prompt-builder.ts
export function buildCardNewsSystemPrompt(niche: NicheTemplate): string {
return `<role>당신은 한국 인스타그램 카드뉴스(캐러셀) 전문 카피라이터입니다. 스와이프를 유도하는 판매형 카드뉴스를 만듭니다. 미리캔버스/망고보드급 퀄리티의 콘텐츠를 생성합니다.</role>
<niche>
업종: ${niche.name} (${niche.category})
기본 톤: ${niche.copy_style.tone_default}
</niche>
<card_news_framework>
카드뉴스는 5~7장 슬라이드로 구성됩니다. 각 슬라이드는 독립적으로 읽히되, 전체가 하나의 판매 퍼널을 형성합니다.
슬라이드 타입별 핵심 목표:
- cover: 표지 — 첫인상이 스와이프 결정. 질문·충격·대담한 주장으로 스크롤을 멈추게 하세요. 제품/서비스명 + 핵심 훅 문장 + 가격(선택).
- why: 이유/장점 — "왜 이걸 사야 하나?"에 답변. 3가지 핵심 이점을 구체적 수치와 함께 제시.
- options: 옵션/가격 — 상품 구성, 가격 비교. 첫 번째 항목이 '추천' 옵션으로 자동 강조됩니다. 명확한 구성과 가격을 제시하세요.
- trust: 신뢰/후기 — 실제 구매자처럼 자연스럽고 구체적인 후기. 별점, 수치, 실명 언급 등 사회적 증거를 활용하세요.
- logistics: 배송/교환 — 배송 정보, 반품 정책. 구매 불안을 해소하는 명확한 정보.
- howto: 주문방법 — 구매 절차를 단계별로. 액션 장벽을 최소화하세요.
- cta: 행동유도 — 최종 CTA. 긴박감(오늘만, 선착순)과 단일 명확한 행동("프로필 링크 클릭" 등).
기본 5장 구성: cover → why → options → trust → cta
풀 7장 구성: cover → why → options → trust → logistics → howto → cta
</card_news_framework>
<writing_rules>buildCardNewsUserPrompt function · typescript · L95-L153 (59 LOC)src/lib/card-news-prompt-builder.ts
export function buildCardNewsUserPrompt(params: {
brandName: string;
tone: string;
topic?: string;
description?: string;
pageTypes: CardPageType[];
}): string {
const slideInstructions = params.pageTypes
.map((type, i) => {
const instructions: Record<CardPageType, string> = {
cover: `슬라이드 ${i + 1} [cover/표지]: 스크롤을 멈추는 훅 문장. 질문·충격·대담한 주장 형식. headline은 20자 이내로 강렬하게.`,
why: `슬라이드 ${i + 1} [why/장점]: items에 수치 포함 혜택 3~4개. "3일 만에", "97%"처럼 구체적 수치 필수.`,
options: `슬라이드 ${i + 1} [options/옵션]: 첫 번째 항목이 추천 옵션. 구성+가격 명확히. price 필드에 대표 가격 입력.`,
trust: `슬라이드 ${i + 1} [trust/신뢰]: 실제 후기처럼 자연스럽고 구체적. 시간·횟수·변화를 포함. "~했어요" 말투 사용.`,
logistics: `슬라이드 ${i + 1} [logistics/배송]: 배송·교환·반품 정보. 당일/익일/무료 등 키워드 포함. 안심 구매 유도.`,
howto: `슬라이드 ${i + 1} [howto/주문방법]: 3~4단계. 동사 시작 (클릭, 문의, 입력, 완료). 최대한 쉽게.`,
cta: `슬라이드 ${i + 1} [cta/행동유도]: subText에 긴박감(오늘만/선착순). headline은 highlight 색으로 강조됨. items에 버튼 텍스트.`,
};
return instructions[tyPowered by Repobility — scan your code at https://repobility.com
getCardNewsTool function · typescript · L156-L217 (62 LOC)src/lib/card-news-prompt-builder.ts
export function getCardNewsTool() {
return {
name: "generate_card_news",
description: "인스타그램 카드뉴스 슬라이드 + 캡션 + 해시태그 생성",
input_schema: {
type: "object" as const,
required: ["pages", "copyText", "hashtags"],
properties: {
pages: {
type: "array" as const,
description: "카드뉴스 슬라이드 배열 (5~7장)",
items: {
type: "object" as const,
required: ["type", "headline", "body"],
properties: {
type: {
type: "string" as const,
enum: ["cover", "why", "options", "trust", "logistics", "howto", "cta"],
description: "슬라이드 타입",
},
headline: {
type: "string" as const,
description: "슬라이드 제목 (최대 20자). cover는 훅 문장, cta는 행동 촉구 문장.",
},
body: {
type: "string" as const,
description: "슬라이드 본문 (최대 80자). 각 슬라이드를 단독으로 봐도 이해 가능해야 함.",
calculateCost function · typescript · L33-L70 (38 LOC)src/lib/claude.ts
export function calculateCost(
model: keyof typeof MODELS,
inputTokens: number,
outputTokens: number,
cacheReadTokens = 0,
cacheWriteTokens = 0
): number {
const pricing = {
GENERATE: {
input: 3.0,
output: 15.0,
cacheRead: 0.3,
cacheWrite: 3.75,
},
// Stage 1 skeleton uses Haiku — same pricing as COMPLIANCE
SKELETON: {
input: 1.0,
output: 5.0,
cacheRead: 0.1,
cacheWrite: 1.25,
},
COMPLIANCE: {
input: 1.0,
output: 5.0,
cacheRead: 0.1,
cacheWrite: 1.25,
},
};
const p = pricing[model];
return (
(inputTokens * p.input +
outputTokens * p.output +
cacheReadTokens * p.cacheRead +
cacheWriteTokens * p.cacheWrite) /
1_000_000
);
}checkCompliance function · typescript · L51-L55 (5 LOC)src/lib/compliance.ts
export async function checkCompliance(
copyText: string,
hashtags: string,
nicheId: string
): Promise<{ result: ComplianceResult; tokenUsage: TokenUsage }> {estimateVoiceCost function · typescript · L21-L23 (3 LOC)src/lib/elevenlabs.ts
export function estimateVoiceCost(characterCount: number): number {
return (characterCount / 1000) * COST_PER_1000_CHARS;
}estimateVoiceDuration function · typescript · L25-L27 (3 LOC)src/lib/elevenlabs.ts
export function estimateVoiceDuration(characterCount: number): number {
return Math.max(1, Math.round(characterCount / CHARS_PER_SECOND));
}ElevenLabsProvider class · typescript · L29-L135 (107 LOC)src/lib/elevenlabs.ts
export class ElevenLabsProvider implements VoiceProvider {
name = "elevenlabs";
private apiKey: string;
private baseUrl = "https://api.elevenlabs.io/v1";
constructor() {
this.apiKey = process.env.ELEVENLABS_API_KEY || "";
}
async synthesize(opts: VoiceSynthesizeOptions): Promise<VoiceSynthesizeResult> {
if (!this.apiKey) {
throw new Error("ELEVENLABS_API_KEY가 설정되지 않았습니다");
}
const modelId = opts.modelId ?? process.env.ELEVENLABS_MODEL ?? "eleven_multilingual_v2";
const stability = opts.stability ?? 0.5;
const similarityBoost = opts.similarityBoost ?? 0.75;
const response = await fetch(
`${this.baseUrl}/text-to-speech/${opts.voiceId}`,
{
method: "POST",
headers: {
"xi-api-key": this.apiKey,
"Content-Type": "application/json",
Accept: "audio/mpeg",
},
body: JSON.stringify({
text: opts.text,
model_id: modelId,
voice_settings: {
constructor method · typescript · L34-L36 (3 LOC)src/lib/elevenlabs.ts
constructor() {
this.apiKey = process.env.ELEVENLABS_API_KEY || "";
}synthesize method · typescript · L38-L99 (62 LOC)src/lib/elevenlabs.ts
async synthesize(opts: VoiceSynthesizeOptions): Promise<VoiceSynthesizeResult> {
if (!this.apiKey) {
throw new Error("ELEVENLABS_API_KEY가 설정되지 않았습니다");
}
const modelId = opts.modelId ?? process.env.ELEVENLABS_MODEL ?? "eleven_multilingual_v2";
const stability = opts.stability ?? 0.5;
const similarityBoost = opts.similarityBoost ?? 0.75;
const response = await fetch(
`${this.baseUrl}/text-to-speech/${opts.voiceId}`,
{
method: "POST",
headers: {
"xi-api-key": this.apiKey,
"Content-Type": "application/json",
Accept: "audio/mpeg",
},
body: JSON.stringify({
text: opts.text,
model_id: modelId,
voice_settings: {
stability,
similarity_boost: similarityBoost,
},
}),
}
);
if (!response.ok) {
let detail = "";
try {
const errJson = (await response.json()) as { detail?: { message?: strinRepobility — same analyzer, your code, free for public repos · /scan/
listVoices method · typescript · L101-L134 (34 LOC)src/lib/elevenlabs.ts
async listVoices(): Promise<VoiceInfo[]> {
if (!this.apiKey) {
throw new Error("ELEVENLABS_API_KEY가 설정되지 않았습니다");
}
const response = await fetch(`${this.baseUrl}/voices`, {
headers: { "xi-api-key": this.apiKey },
});
if (!response.ok) {
throw new Error(`ElevenLabs 보이스 목록 조회 실패: ${response.status}`);
}
const data = (await response.json()) as {
voices: Array<{
voice_id: string;
name: string;
category: string;
labels?: Record<string, string>;
preview_url?: string;
}>;
};
// premade 보이스만 반환 — 클론 보이스 제외 (policy)
return data.voices
.filter((v) => v.category === "premade")
.map((v) => ({
id: v.voice_id,
name: v.name,
category: v.category,
labels: v.labels ?? {},
previewUrl: v.preview_url,
}));
}saveAudioFile function · typescript · L138-L145 (8 LOC)src/lib/elevenlabs.ts
async function saveAudioFile(filename: string, buffer: Buffer): Promise<string> {
const uploadDir = path.join(process.cwd(), "uploads", "audio");
await mkdir(uploadDir, { recursive: true });
const filePath = path.join(uploadDir, filename);
await writeFile(filePath, buffer);
const baseUrl = process.env.NEXTAUTH_URL || "http://localhost:3000";
return `${baseUrl}/api/uploads/audio/${filename}`;
}sanitizeFilename function · typescript · L147-L149 (3 LOC)src/lib/elevenlabs.ts
function sanitizeFilename(str: string): string {
return str.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32);
}estimateCost function · typescript · L19-L21 (3 LOC)src/lib/flux-pro.ts
export function estimateCost(): number {
return COST_PER_IMAGE;
}initFal function · typescript · L23-L28 (6 LOC)src/lib/flux-pro.ts
function initFal() {
if (!process.env.FAL_KEY) {
throw new Error("FAL_KEY environment variable is not set");
}
fal.config({ credentials: process.env.FAL_KEY });
}FluxProProvider class · typescript · L30-L86 (57 LOC)src/lib/flux-pro.ts
export class FluxProProvider implements ImageProvider {
name = "flux-pro-ultra";
async submit(opts: ImageSubmitOptions): Promise<{ taskId: string }> {
initFal();
const input: Record<string, unknown> = {
prompt: opts.prompt,
aspect_ratio: opts.aspectRatio ?? "9:16",
output_format: "jpeg",
safety_tolerance: "2",
};
if (opts.negativePrompt) {
input.negative_prompt = opts.negativePrompt;
}
if (opts.width) input.width = opts.width;
if (opts.height) input.height = opts.height;
const result = await fal.queue.submit(MODEL, { input });
return { taskId: `${MODEL}::${result.request_id}` };
}
async check(taskId: string): Promise<ImageCheckResult> {
initFal();
const { model, requestId } = parseTaskId(taskId);
const status = await fal.queue.status(model, { requestId });
if (status.status === "COMPLETED") {
const result = await fal.queue.result(model, { requestId });
const data = result.datacheck method · typescript · L55-L85 (31 LOC)src/lib/flux-pro.ts
async check(taskId: string): Promise<ImageCheckResult> {
initFal();
const { model, requestId } = parseTaskId(taskId);
const status = await fal.queue.status(model, { requestId });
if (status.status === "COMPLETED") {
const result = await fal.queue.result(model, { requestId });
const data = result.data as Record<string, unknown>;
// Flux output: images array with url field
const images = data.images as Array<{ url: string }> | undefined;
const imageUrl = images?.[0]?.url;
if (!imageUrl) {
return { status: "FAILED", error: "이미지 URL이 응답에 없습니다" };
}
return { status: "READY", imageUrl };
}
if (status.status === "IN_PROGRESS") {
return { status: "PROCESSING" };
}
if (status.status === "IN_QUEUE") {
return { status: "PENDING" };
}
return { status: "FAILED", error: "알 수 없는 상태" };
}getModelFromTaskId function · typescript · L99-L101 (3 LOC)src/lib/flux-pro.ts
export function getModelFromTaskId(taskId: string): string {
return parseTaskId(taskId).model;
}Repobility · severity-and-effort ranking · https://repobility.com
estimateCost function · typescript · L20-L22 (3 LOC)src/lib/flux-pulid.ts
export function estimateCost(): number {
return COST_PER_IMAGE;
}initFal function · typescript · L24-L29 (6 LOC)src/lib/flux-pulid.ts
function initFal() {
if (!process.env.FAL_KEY) {
throw new Error("FAL_KEY environment variable is not set");
}
fal.config({ credentials: process.env.FAL_KEY });
}FluxPulidProvider class · typescript · L31-L93 (63 LOC)src/lib/flux-pulid.ts
export class FluxPulidProvider implements ImageProvider {
name = "flux-pulid";
async submit(opts: ImageSubmitOptions): Promise<{ taskId: string }> {
initFal();
const input: Record<string, unknown> = {
prompt: opts.prompt,
// PuLID uses image dimensions, not aspect_ratio
image_size: opts.aspectRatio === "9:16" ? "portrait_4_3" : "square_hd",
num_inference_steps: 20,
guidance_scale: 3.5,
};
// PuLID: 얼굴 레퍼런스 이미지 (캐릭터 얼굴 고정용)
if (opts.referenceImageUrl) {
input.reference_image = opts.referenceImageUrl;
}
if (opts.negativePrompt) {
input.negative_prompt = opts.negativePrompt;
}
if (opts.width) input.width = opts.width;
if (opts.height) input.height = opts.height;
const result = await fal.queue.submit(MODEL, { input });
return { taskId: `${MODEL}::${result.request_id}` };
}
async check(taskId: string): Promise<ImageCheckResult> {
initFal();
const { model, requestId } = parseTascheck method · typescript · L62-L92 (31 LOC)src/lib/flux-pulid.ts
async check(taskId: string): Promise<ImageCheckResult> {
initFal();
const { model, requestId } = parseTaskId(taskId);
const status = await fal.queue.status(model, { requestId });
if (status.status === "COMPLETED") {
const result = await fal.queue.result(model, { requestId });
const data = result.data as Record<string, unknown>;
// PuLID output: images array with url field
const images = data.images as Array<{ url: string }> | undefined;
const imageUrl = images?.[0]?.url;
if (!imageUrl) {
return { status: "FAILED", error: "이미지 URL이 응답에 없습니다" };
}
return { status: "READY", imageUrl };
}
if (status.status === "IN_PROGRESS") {
return { status: "PROCESSING" };
}
if (status.status === "IN_QUEUE") {
return { status: "PENDING" };
}
return { status: "FAILED", error: "알 수 없는 상태" };
}getModelFromTaskId function · typescript · L106-L108 (3 LOC)src/lib/flux-pulid.ts
export function getModelFromTaskId(taskId: string): string {
return parseTaskId(taskId).model;
}checkIdempotency function · typescript · L19-L84 (66 LOC)src/lib/idempotency.ts
export async function checkIdempotency(
key: string | null | undefined,
endpoint: string,
userId: string
): Promise<IdempotencyCheck> {
if (!key) return { isDuplicate: false };
// Lazy cleanup: 만료된 키 삭제 (비동기, fire-and-forget)
prisma.idempotencyKey
.deleteMany({ where: { expiresAt: { lt: new Date() } } })
.catch(() => {});
// 새 키 삽입 시도 (unique constraint가 중복 방어)
try {
await prisma.idempotencyKey.create({
data: {
key,
endpoint,
userId,
status: "PROCESSING",
expiresAt: new Date(Date.now() + EXPIRY_HOURS * 60 * 60 * 1000),
},
});
return { isDuplicate: false };
} catch {
// Unique constraint violation → 기존 키 조회
const existing = await prisma.idempotencyKey.findUnique({
where: { key },
});
if (!existing) return { isDuplicate: false };
// P0 fix: endpoint + userId 스코프 검증 — 다른 엔드포인트/유저의 키 재사용 방지
if (existing.endpoint !== endpoint || existing.userId !== userId) {
returcompleteIdempotency function · typescript · L89-L104 (16 LOC)src/lib/idempotency.ts
export async function completeIdempotency(
key: string | null | undefined,
status: number,
body: unknown
): Promise<void> {
if (!key) return;
await prisma.idempotencyKey
.update({
where: { key },
data: {
status: "COMPLETED",
response: { status, body } as object,
},
})
.catch(() => {});
}failIdempotency function · typescript · L109-L116 (8 LOC)src/lib/idempotency.ts
export async function failIdempotency(
key: string | null | undefined
): Promise<void> {
if (!key) return;
await prisma.idempotencyKey
.update({ where: { key }, data: { status: "FAILED" } })
.catch(() => {});
}Repobility · code-quality intelligence platform · https://repobility.com
getIdempotencyKey function · typescript · L121-L125 (5 LOC)src/lib/idempotency.ts
export function getIdempotencyKey(
req: Request
): string | null {
return req.headers.get("x-idempotency-key");
}buildCharacterImagePrompt function · typescript · L88-L98 (11 LOC)src/lib/image-prompt-builder.ts
export function buildCharacterImagePrompt(
character: {
basePrompt: string;
hairStyle?: string | null;
facialFeatures?: string | null;
fashionStyle?: string | null;
lightingStyle?: string | null;
},
scene?: string,
niche?: string
): { prompt: string; negativePrompt: string } {getNicheNegatives function · typescript · L144-L160 (17 LOC)src/lib/image-prompt-builder.ts
function getNicheNegatives(niche?: string): string {
const nicheNegativeMap: Record<string, string> = {
cosmetics: "skin blemishes, uneven skin tone, harsh shadows",
cafe: "unnatural food colors, melting artifacts, fake-looking food",
fashion: "face distortion, fabric clipping, unrealistic body proportions",
bakery: "melting artifacts, unnatural food, inedible appearance",
fitness:
"body distortion, impossible body proportions, unrealistic muscles",
"nail-salon": "nail distortion, skin artifacts, blurry nail details",
pet: "animal distortion, unnatural animal poses",
interior: "furniture distortion, impossible geometry",
education: "text distortion, blurry readable text",
"tech-gadget": "screen artifacts, reflections showing crew",
};
return (niche && nicheNegativeMap[niche]) || "";
}buildProductImagePrompt function · typescript · L163-L166 (4 LOC)src/lib/image-prompt-builder.ts
export function buildProductImagePrompt(
productDescription: string,
niche?: string
): { prompt: string; negativePrompt: string } {isImageEnabled function · typescript · L26-L28 (3 LOC)src/lib/image-provider.ts
export function isImageEnabled(): boolean {
return !!process.env.FAL_KEY;
}getImageProvider function · typescript · L30-L39 (10 LOC)src/lib/image-provider.ts
export function getImageProvider(mode: "standard" | "face-lock"): ImageProvider {
if (mode === "face-lock") {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { FluxPulidProvider } = require("./flux-pulid") as typeof import("./flux-pulid");
return new FluxPulidProvider();
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { FluxProProvider } = require("./flux-pro") as typeof import("./flux-pro");
return new FluxProProvider();
}isInstagramConfigured function · typescript · L59-L63 (5 LOC)src/lib/instagram.ts
export function isInstagramConfigured(): boolean {
return Boolean(
process.env.INSTAGRAM_ACCESS_TOKEN && process.env.INSTAGRAM_USER_ID
);
}throwIfIgError function · typescript · L95-L132 (38 LOC)src/lib/instagram.ts
async function throwIfIgError(res: Response): Promise<void> {
if (res.ok) return;
// 레이트 리밋
if (res.status === 429) {
throw new Error(
"Instagram API 레이트 리밋 초과(429). 잠시 후 다시 시도하세요."
);
}
let body: IgErrorResponse | null = null;
try {
body = (await res.json()) as IgErrorResponse;
} catch {
throw new Error(`Instagram API 오류: HTTP ${res.status} ${res.statusText}`);
}
const err = body?.error;
if (!err) {
throw new Error(`Instagram API 오류: HTTP ${res.status}`);
}
// 토큰 만료 (error code 190)
if (err.code === 190) {
throw new Error(
`Instagram 액세스 토큰이 만료되었습니다 (code 190). ` +
`refreshLongLivedToken()을 호출하거나 토큰을 갱신하세요. ` +
`원본 메시지: ${err.message}`
);
}
// 그 외 API 오류
const subcode = err.error_subcode ? ` (subcode ${err.error_subcode})` : "";
throw new Error(
`Instagram API 오류 [code ${err.code ?? "?"}${subcode}]: ${err.message}` +
(err.fbtrace_id ? ` | trace_id=${err.fbtrace_id}` : "")
);
}Powered by Repobility — scan your code at https://repobility.com
isProcessingSubcode function · typescript · L138-L140 (3 LOC)src/lib/instagram.ts
function isProcessingSubcode(subcode?: number): boolean {
return subcode !== undefined && PROCESSING_ERROR_SUBCODES.has(subcode);
}sleep function · typescript · L145-L147 (3 LOC)src/lib/instagram.ts
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}createReelsContainer function · typescript · L162-L167 (6 LOC)src/lib/instagram.ts
export async function createReelsContainer(opts: {
videoUrl: string;
caption: string;
coverUrl?: string;
shareToFeed?: boolean;
}): Promise<{ containerId: string }> {checkContainerStatus function · typescript · L204-L207 (4 LOC)src/lib/instagram.ts
export async function checkContainerStatus(containerId: string): Promise<{
statusCode: ContainerStatusCode;
status?: string;
}> {publishContainer function · typescript · L236-L238 (3 LOC)src/lib/instagram.ts
export async function publishContainer(containerId: string): Promise<{
mediaId: string;
}> {publishReels function · typescript · L276-L282 (7 LOC)src/lib/instagram.ts
export async function publishReels(opts: {
videoUrl: string;
caption: string;
coverUrl?: string;
maxWaitMs?: number;
pollIntervalMs?: number;
}): Promise<{ mediaId: string; containerId: string }> {createCarouselContainer function · typescript · L355-L358 (4 LOC)src/lib/instagram.ts
export async function createCarouselContainer(opts: {
imageUrls: string[];
caption: string;
}): Promise<{ containerId: string }> {publishCarousel function · typescript · L420-L423 (4 LOC)src/lib/instagram.ts
export async function publishCarousel(opts: {
imageUrls: string[];
caption: string;
}): Promise<{ mediaId: string; containerId: string }> {Repobility — same analyzer, your code, free for public repos · /scan/
refreshLongLivedToken function · typescript · L442-L445 (4 LOC)src/lib/instagram.ts
export async function refreshLongLivedToken(): Promise<{
accessToken: string;
expiresIn: number; // 초 단위
}> {estimateCost function · typescript · L22-L26 (5 LOC)src/lib/kling.ts
export function estimateCost(model: string, durationSec = 5): number {
// Pro 모델 기본 $0.224/초
void model; // future: model-specific pricing
return COST_PER_SEC_NO_AUDIO * durationSec;
}initFal function · typescript · L28-L33 (6 LOC)src/lib/kling.ts
function initFal() {
if (!process.env.FAL_KEY) {
throw new Error("FAL_KEY environment variable is not set");
}
fal.config({ credentials: process.env.FAL_KEY });
}