reopt designreopt design
DocsExploreToolsPricingBuilder
시작하기
개요
시작하기
핵심 개념
핵심 개념
Surface
Surface 카탈로그
구축·운영
서버 계약
Production readiness
Obrandapp-ui
reopt designreopt design

AI 시대를 위한 디자인 시스템

  • 문서
  • 가격
  • 릴리즈 노트
  • GitHub
  • 서비스 약관
  • 개인정보처리방침

© 2026 reopt-ai. All rights reserved.

구축·운영
  1. 문서
  2. /
  3. 구축·운영
  4. /
  5. 서버 계약

서버 계약

clientSecret이 필요한 BrandApp 기능은 Surface에 직접 넣지 않습니다. server route가 SDK를 호출하고 Surface는 endpoint만 받습니다.

reopt design · 업데이트 2026년 6월 26일

개요시작하기핵심 개념Surface 카탈로그서버 계약Production readiness

1. Contract matrix

SurfaceEndpointsSecret boundary
ReoptAiChatapiEndpoint POST, optional modelsEndpoint GET, agentsEndpoint GET, creditsEndpoint GETgetBrandappProvider/getBrandappSDK stays behind server route handlers
ReoptAiImageStudioapiEndpoint POST, optional image modelsEndpoint GETsdk.ai.generateImage and typed errors stay on server
ReoptRecordTablerecordsEndpoint GETsdk.eav.records(...).list() stays on server

2. Lazy server clients

Next.js build와 static evaluation은 route module을 runtime env 없이 평가할 수 있습니다. BrandApp SDK와 AI provider는 module scope에서 만들지 말고, server-only helper의 lazy getter 안에서 초기화합니다.

ts
// lib/brandapp-server.ts
import { createReoptSDK } from "@reopt-ai/brandapp-sdk";
import { createBrandappProvider } from "@reopt-ai/brandapp-sdk/ai-provider";

function requireEnv(name: string) {
  const value = process.env[name];
  if (!value) throw new Error(name + " is required");
  return value;
}

function getBrandappConfig() {
  return {
    clientId: requireEnv("BRANDAPP_CLIENT_ID"),
    clientSecret: requireEnv("BRANDAPP_CLIENT_SECRET"),
    brandappId: requireEnv("BRANDAPP_ID"),
  };
}

let sdk: ReturnType<typeof createReoptSDK> | null = null;

export function getBrandappSDK() {
  sdk ??= createReoptSDK(getBrandappConfig());
  return sdk;
}

let provider: ReturnType<typeof createBrandappProvider> | null = null;

export function getBrandappProvider() {
  provider ??= createBrandappProvider(getBrandappConfig());
  return provider;
}

3. AI chat route

ReoptAiChat은 opt-chat UI와 transport를 제공합니다. 실제 BrandApp AI provider, model fallback, typed error copy는 route handler에서 결정합니다.

ts
// app/api/chat/route.ts
import { APICallError } from "@ai-sdk/provider";
import {
  isCreditLimitError,
  isModelAccessError,
} from "@reopt-ai/brandapp-sdk";
import { convertToModelMessages, streamText } from "ai";

import { getBrandappProvider } from "@/lib/brandapp-server";

export async function POST(req: Request) {
  const { messages, model, agentId } = await req.json();
  const brandapp = getBrandappProvider();

  const result = streamText({
    model: brandapp(model ?? "anthropic/claude-haiku-4.5"),
    messages: convertToModelMessages(messages),
    // agentId는 BrandApp agent routing 정책에 맞게 route에서 해석합니다.
  });

  return result.toUIMessageStreamResponse({
    onError: (error) => {
      const cause = APICallError.isInstance(error) ? error.cause : error;
      if (isCreditLimitError(cause)) return "AI 크레딧을 모두 사용했습니다.";
      if (isModelAccessError(cause)) {
        return "현재 플랜에서 사용할 수 없는 모델입니다.";
      }
      return cause instanceof Error ? cause.message : "생성 중 오류가 발생했습니다.";
    },
  });
}
ts
// app/api/brandapp-models/route.ts
import { getBrandappSDK } from "@/lib/brandapp-server";

export async function GET() {
  const sdk = getBrandappSDK();
  const models = await sdk.ai.models();

  return Response.json(
    models
      .filter((model) => model.modality === "chat")
      .sort((a, b) => Number(b.isDefault) - Number(a.isDefault))
      .map((model) => ({
        id: model.id,
        label: model.displayName,
        description: model.description,
        provider: model.provider,
        ...(model.isDefault ? { badge: "기본" } : {}),
      })),
  );
}

4. Image generation route

ReoptAiImageStudio는 typed error code를 사용자 친화 메시지로 매핑할 수 있습니다. 서버 route가 SDK error를 { error: { code, message } } shape로 내려주면 됩니다.

ts
// app/api/brandapp-image/route.ts
import { isReoptSDKError } from "@reopt-ai/brandapp-sdk";

import { getBrandappSDK } from "@/lib/brandapp-server";

export async function POST(req: Request) {
  const sdk = getBrandappSDK();
  const body = await req.json();

  try {
    const result = await sdk.ai.generateImage(body);
    return Response.json(result);
  } catch (error) {
    if (isReoptSDKError(error)) {
      return Response.json(
        { error: { code: error.code, message: error.message } },
        { status: error.status },
      );
    }
    throw error;
  }
}

5. EAV record proxy

ReoptRecordTable은 array, data, records 응답을 모두 행 배열로 정규화합니다. 그래도 권장 응답은 EAV list page 그대로입니다.

ts
// app/api/brandapp-records/route.ts
import { getBrandappSDK } from "@/lib/brandapp-server";

export async function GET(req: Request) {
  const sdk = getBrandappSDK();
  const { searchParams } = new URL(req.url);
  const entityId = searchParams.get("entityId");
  if (!entityId) return Response.json({ data: [] });

  const page = await sdk.eav.records(entityId).list({
    page: Number(searchParams.get("page") ?? 1),
    pageSize: Number(searchParams.get("pageSize") ?? 50),
  });

  return Response.json(page);
}

6. 401 session event

보호된 API가 401을 반환하면 SessionExpiredDialog가 알아들을 수 있는 window event를 dispatch합니다. Surface 내부에서 fetch helper를 강제하지 않으므로 consumer app의 API wrapper에 넣습니다.

ts
// lib/protected-fetch.ts
import { SESSION_EXPIRED_EVENT } from "@/components/session-expired-dialog";

export async function protectedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (response.status === 401 && typeof window !== "undefined") {
    window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT));
  }
  return response;
}
PreviousSurface 카탈로그brandapp-ui 인증, AI, EAV, 운영 콘솔 Surface 선택 기준과 설치 명령Surface
Surface 카탈로그 페이지로 이동
NextProduction readinessbrandapp-ui registry, auth state matrix, secret boundary, consumer smoke, docs 검증 체크리스트구축·운영