reopt designreopt design
DocsExploreToolsPricingBuilder
시작하기
시작하기
Playground
핵심 개념
핵심 개념
Editor
Editor 개요
블록 타입
스트리밍
Markdown 변환
구축·운영
Skills
앱 조합 가이드
AI 연동
Authoring 플레이북
커스텀 블록
릴리즈 노트
Oopt-editor
reopt designreopt design

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

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

© 2026 reopt-ai. All rights reserved.

구축·운영
  1. 문서
  2. /
  3. 구축·운영
  4. /
  5. 커스텀 블록

커스텀 블록

BlockDefinition 작성, defineCatalog 확장, 커스텀 렌더러 구현 가이드

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

시작하기Playground핵심 개념개요블록 타입스트리밍Markdown 변환Skills앱 조합 가이드AI 연동Authoring 플레이북커스텀 블록릴리즈 노트

1. 현재 BlockDefinition 구조

커스텀 블록은 schema나 editableFields를 쓰지 않습니다. 현재 public type은 attrsSchema로 attribute 스키마를 선언하고, rich text 블록은 contentKind="rich-text"와 editableContent() helper를 사용합니다.

tsx
import type { BlockDefinition } from "@reopt-ai/opt-editor";

const definition: BlockDefinition = {
  type: "custom",
  attrsSchema: {
    variant: { type: "string", default: "info" },
  },
  contentKind: "rich-text",
  canHaveChildren: false,
  component: CustomBlock,
  prompt: "Describe when AI should use this block.",
};

2. rich-text 블록

문장형 콘텐츠는 attrs에 텍스트를 넣지 않고, EditorElement.content의 canonical inline segment에 저장합니다. edit mode에서는 editableContent()가 contentEditable 렌더링을 만들고, stream/diff mode에서는 MarkedText로 같은 content를 렌더링합니다.

tsx
import {
  MarkedText,
  type BlockDefinition,
  type BlockRenderProps,
} from "@reopt-ai/opt-editor";

type AlertAttrs = {
  severity?: string;
};

function AlertBlock({
  attrs,
  content,
  editableContent,
}: BlockRenderProps<AlertAttrs>) {
  return (
    <aside data-alert-severity={attrs.severity ?? "info"}>
      {editableContent?.({
        as: "div",
        placeholder: "Alert message",
      }) ?? <MarkedText content={content} />}
    </aside>
  );
}

export const alertBlock: BlockDefinition<AlertAttrs> = {
  type: "alert",
  attrsSchema: {
    severity: { type: "string", default: "info" },
  },
  contentKind: "rich-text",
  component: AlertBlock,
  prompt: 'An alert box. severity is "info", "warning", or "error".',
};

3. string attr 블록

code, image, embed처럼 텍스트가 attribute 자체인 블록은 editableStringAttr("field")를 사용합니다. 이 경우 값은 EditorElement.attrs에 저장됩니다.

tsx
import type {
  BlockDefinition,
  BlockRenderProps,
} from "@reopt-ai/opt-editor";

type BannerAttrs = {
  title?: string;
  tone?: string;
};

function BannerBlock({
  attrs,
  mode,
  editableStringAttr,
}: BlockRenderProps<BannerAttrs>) {
  return (
    <section data-banner-tone={attrs.tone ?? "neutral"}>
      {mode === "edit"
        ? editableStringAttr?.("title", {
            as: "h3",
            placeholder: "Banner title",
          })
        : <h3>{attrs.title}</h3>}
    </section>
  );
}

export const bannerBlock: BlockDefinition<BannerAttrs> = {
  type: "banner",
  attrsSchema: {
    title: { type: "string", default: "" },
    tone: { type: "string", default: "neutral" },
  },
  component: BannerBlock,
  prompt: "A short title banner with a visual tone.",
};

4. 카탈로그 확장

defineCatalog()는 block definition map을 받습니다. 기본 블록을 유지하려면 defaultCatalog.blocks를 펼친 뒤 새 definition을 추가합니다. 이렇게 등록한 블록은 catalog.prompt(), catalog.jsonSchema(),catalog.validateAttrs()에 반영됩니다.

tsx
import { defineCatalog, defaultCatalog } from "@reopt-ai/opt-editor";
import { alertBlock } from "./alert-block";
import { bannerBlock } from "./banner-block";

export const catalog = defineCatalog({
  ...defaultCatalog.blocks,
  alert: alertBlock,
  banner: bannerBlock,
});

const systemPrompt = catalog.prompt();
const jsonSchema = catalog.jsonSchema();

5. split / merge

rich text 블록에서 Enter/Backspace 동작이 필요하면 operations.split과 operations.merge를 정의합니다. EditorElement는 props가 아니라attrs와 content를 갖습니다.

tsx
import type {
  BlockDefinition,
  EditorElement,
  InlineContentSegment,
} from "@reopt-ai/opt-editor";

function splitContent(
  content: InlineContentSegment[] = [],
  offset: number,
): [InlineContentSegment[], InlineContentSegment[]] {
  const text = content.map((segment) => segment.text).join("");
  return [
    [{ text: text.slice(0, offset), marks: [] }],
    [{ text: text.slice(offset), marks: [] }],
  ];
}

export const alertBlock: BlockDefinition<AlertAttrs> = {
  type: "alert",
  attrsSchema: {
    severity: { type: "string", default: "info" },
  },
  contentKind: "rich-text",
  component: AlertBlock,
  operations: {
    split(element: EditorElement, offset: number) {
      const [left, right] = splitContent(element.content, offset);
      return [
        { ...element, content: left },
        {
          id: `${element.id}-split`,
          type: "paragraph",
          attrs: {},
          content: right,
        },
      ];
    },
    merge(a: EditorElement, b: EditorElement) {
      return {
        ...a,
        content: [...(a.content ?? []), ...(b.content ?? [])],
      };
    },
  },
};
PreviousAuthoring 플레이북생성, 스트리밍, 검수, 직접 편집, 배포 직전 검증까지 이어지는 운영형 authoring 워크플로우구축·운영
Authoring 플레이북 페이지로 이동
Next릴리즈 노트opt-editor 버전별 변경 사항과 하이라이트를 패키지 기준으로 정리합니다.구축·운영