← back to hyunwoooim-star__autoshorts-mvp-

Function bodies 248 total

All specs Real LLM only Function bodies
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[ty
Powered 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?: strin
Repobility — 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.data
check 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 } = parseTas
check 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) {
      retur
completeIdempotency 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 });
}
‹ prevpage 4 / 5next ›