문서 모델과 편집
v2 문서, 원자적 명령, undo/redo, 브랜드와 고객별 자료의 변경 규칙을 설명합니다.
reopt design업데이트
1. DeckDocument v2 구조
문서는 version=2와 id·revision, width·height, title을 포함합니다. 슬라이드 순서는 slideOrder에, 실제 슬라이드와 노드는 ID 기반 맵에 보관합니다. 화면 확대율은 문서 좌표와 분리되어 저장됩니다.
- slides / slideOrder
- 슬라이드 배경·노트·전환·애니메이션과 루트 자식 ID, 표시 순서.
- nodes
- text·shape·path·image·group·chart·table·media와 확장 JSON·스냅샷을 가진 custom 장면 노드.
- masters
- 여러 슬라이드가 참조하는 마스터 슬라이드.
- assets
- 미디어·벡터·글꼴 원본과 접근 URL, 크기·임베드 정보.
- brand
- 색상·폰트·텍스트 스타일·간격·차트·모션 규칙.
- derivation
- 고객별 자료의 원본 ID·revision·비교 기준 스냅샷.
| 항목 | 동작과 사용 기준 |
|---|---|
| slides / slideOrder | 슬라이드 배경·노트·전환·애니메이션과 루트 자식 ID, 표시 순서. |
| nodes | text·shape·path·image·group·chart·table·media와 확장 JSON·스냅샷을 가진 custom 장면 노드. |
| masters | 여러 슬라이드가 참조하는 마스터 슬라이드. |
| assets | 미디어·벡터·글꼴 원본과 접근 URL, 크기·임베드 정보. |
| brand | 색상·폰트·텍스트 스타일·간격·차트·모션 규칙. |
| derivation | 고객별 자료의 원본 ID·revision·비교 기준 스냅샷. |
커스텀 요소: 차트와 데이터 표
extensions에 DeckExtension을 등록하면 커스텀 요소 메뉴가 나타납니다. optChartsExtension()은 opt-charts의 데이터 차트 31종과 카드형 시각화 4종을 제공합니다. 종류 검색, 표 데이터 편집, 행렬·계층·그래프 JSON 설정과 유형별 데이터 검증을 지원합니다. 기존 막대·선·영역 차트는 호환되며 고급 설정으로 전환할 수 있습니다. 적용하면 HTML 표·범례까지 포함한 PNG와 원본 데이터를 함께 저장하며, PDF·PowerPoint에는 정적 이미지로 내보냅니다.
optDataGridExtension()은 실제 DataGrid에서 셀 편집, 행 포함·강조, 열 숨김을 지원합니다. 엑셀의 탭 구분 데이터를 첫 행 제목과 함께 붙여넣고 열별 숫자·퍼센트·통화 서식을 지정하세요. 원본은 최대 500행·24열이며 현재 선택한 페이지만 슬라이드에 삽입됩니다. 긴 내용은 맞춤 축소되므로 적은 행·열을 선택하면 읽기 좋습니다. 전체 원본은 보존되며 다른 페이지는 요소를 복제한 뒤 선택할 수 있습니다. PDF·PPTX에서는 정적 이미지로 표시됩니다.
JSON과 .deck은 원본 데이터를 유지합니다. 같은 ID·버전의 확장이 있으면 데이터·스타일 편집으로 다시 열 수 있습니다. 확장이 없어도 캔버스·발표·출력은 저장된 스냅샷을 표시합니다. 발표는 정적이며 PDF·PPTX의 커스텀 요소는 이미지입니다. 직접 확장을 만들려면 createData·parseData·Preview·Inspector·snapshot을 구현하세요. 버전 마이그레이션은 호스트가 처리합니다.
"use client";
// Install @reopt-ai/opt-charts and @reopt-ai/opt-datagrid alongside opt-deck.
import { DeckEditor } from "@reopt-ai/opt-deck";
import { optChartsExtension } from "@reopt-ai/opt-deck/opt-charts";
import { optDataGridExtension } from "@reopt-ai/opt-deck/opt-datagrid";
import "@reopt-ai/opt-deck/styles.css";
const extensions = [optChartsExtension(), optDataGridExtension()];
export function ChartPresentation() {
return <DeckEditor extensions={extensions} locale="ko" />;
}2. 원자적 트랜잭션
여러 변경을 하나의 SceneTransaction에 넣습니다. baseRevision이 현재 문서와 다르거나 노드 참조·계층이 유효하지 않으면 전체 적용이 실패합니다. applySceneTransaction은 입력 문서를 변경하지 않고 검증된 다음 문서를 반환합니다.
이 함수 자체는 undo 기록을 저장하지 않습니다. history가 필요하면 DeckStore.dispatch 또는 useDeck.dispatch를 사용합니다. label은 사용자가 이해할 수 있는 작업 이름으로 지정합니다.
import {
createDeckDocument,
createNode,
applySceneTransaction,
} from "@reopt-ai/opt-deck/server";
const document = createDeckDocument({ design: "product", title: "Launch" });
const slideId = document.slideOrder[0];
const node = createNode(document, "text", "launch-heading");
const next = applySceneTransaction(document, {
baseRevision: document.revision,
label: "Add heading",
operations: [
{ type: "insert-node", parentId: slideId, node },
{ type: "update-node", id: node.id, patch: { x: 120, y: 100 } },
],
});
// next is a new validated document; document is unchanged.
export { next };3. useDeck과 undo/redo
useDeck은 호스트가 직접 명령 UI를 만들 때 사용할 독립 store입니다. getSnapshot은 최신 문서를, document는 React 렌더용 스냅샷을 제공합니다. undo/redo 후에도 revision은 증가하므로 revision을 과거 내용의 고정 식별자로 사용하지 않습니다.
DeckEditor는 자체 store를 관리합니다. 아래 예제처럼 useDeck으로 제어하는 뷰어와 명령 UI를 만들거나, 기본 편집기의 value/onChange를 사용하는 방식 중 history 소유자를 정하세요. 외부 value 교체는 내부 history를 초기화합니다.
"use client";
import { useDeck } from "@reopt-ai/opt-deck";
import { DeckViewer } from "@reopt-ai/opt-deck/viewer";
export function HostControlledPresentation() {
const deck = useDeck({ historyLimit: 100 });
function rename() {
const current = deck.getSnapshot();
deck.dispatch({
baseRevision: current.revision,
label: "Rename slide",
operations: [{
type: "update-slide",
id: current.slideOrder[0],
patch: { name: "Customer introduction" },
}],
});
}
return (
<div>
<button onClick={rename}>Rename first slide</button>
<button onClick={deck.undo} disabled={!deck.canUndo}>Undo</button>
<DeckViewer document={deck.document} />
</div>
);
}4. 조작과 단축키
캔버스에서 요소를 선택한 뒤 이동·리사이즈·회전하고, Shift로 다중 선택합니다. 텍스트 더블클릭 또는 선택 후 Enter로 직접 편집합니다. 텍스트 입력 중에는 캔버스 단축키 대신 입력 동작을 우선합니다. 캔버스에 포커스를 두고 Shift+F10이나 메뉴 키로 작업 메뉴를 열 수 있습니다. 선택창과 메뉴는 방향키·Enter·Escape로 조작합니다. 숫자는 Enter 또는 포커스 이동으로 한 번에 확정하고, Escape로 입력 중인 변경을 취소합니다. 숫자 입력의 Enter는 호스트 폼을 제출하지 않습니다.
- ⌘ / Ctrl + Z · Shift + Z
- 실행 취소 / 다시 실행.
- ⌘ / Ctrl + D · G · Shift + G
- 복제 / 그룹 / 그룹 해제.
- ⌘ / Ctrl + C · X · V
- 선택 요소 복사 / 잘라내기 / 붙여넣기.
- ⌘ / Ctrl + K
- 명령 검색.
- V · H · T · R · P
- 선택 / 손 / 텍스트 / 도형 / 벡터 도구.
- Arrow · Shift + Arrow
- 선택 요소 1 / 10 문서 단위 이동.
- Space + drag · 1
- 캔버스 이동 / 화면에 맞추기.
| 항목 | 동작과 사용 기준 |
|---|---|
| ⌘ / Ctrl + Z · Shift + Z | 실행 취소 / 다시 실행. |
| ⌘ / Ctrl + D · G · Shift + G | 복제 / 그룹 / 그룹 해제. |
| ⌘ / Ctrl + C · X · V | 선택 요소 복사 / 잘라내기 / 붙여넣기. |
| ⌘ / Ctrl + K | 명령 검색. |
| V · H · T · R · P | 선택 / 손 / 텍스트 / 도형 / 벡터 도구. |
| Arrow · Shift + Arrow | 선택 요소 1 / 10 문서 단위 이동. |
| Space + drag · 1 | 캔버스 이동 / 화면에 맞추기. |
5. 브랜드·마스터·재사용 구성요소
브랜드 색과 글꼴을 변경하면 연결된 스타일의 요소에 반영됩니다. 마스터는 슬라이드 공통 요소를 관리하고, 재사용 구성요소는 선택한 그룹을 원본과 인스턴스로 관리합니다. 인스턴스의 개별 변경값을 유지하면서 원본 갱신을 반영하거나 연결을 해제할 수 있습니다.
글꼴 관리에서 업로드한 파일의 family·weight·style과 임베드 허용 범위를 확인하세요. 폰트 미설치와 수신 앱의 글꼴 대체는 편집 화면과 파일 출력의 차이를 만들 수 있습니다.
6. 고객별 자료 비교와 병합
createDeckVariant는 원본과 비교 기준을 보존한 별도 문서를 만듭니다. compareVariantSource로 추가·삭제·수정과 충돌 여부를 확인하고, 사용자가 선택한 변경만 mergeVariantSource에 전달합니다.
충돌이 없는 변경 집합도 부모·자식 참조가 함께 필요할 수 있습니다. 구조적으로 불완전한 선택은 원자적으로 실패하므로 오류를 표시하고 선택 집합을 다시 조정하세요. 결과 저장과 원본 조회는 호스트 책임입니다.
import {
createDeckDocument, createDeckVariant,
compareVariantSource, mergeVariantSource, applySceneTransaction,
} from "@reopt-ai/opt-deck/server";
const source = createDeckDocument({ title: "Company introduction" });
const customer = createDeckVariant(source, "customer-a", "Customer A");
const updatedSource = applySceneTransaction(source, {
baseRevision: source.revision,
label: "Update source notes",
operations: [{ type: "update-slide", id: source.slideOrder[0],
patch: { notes: "Updated presenter guidance" } }],
});
const changes = compareVariantSource(customer, updatedSource);
// Present changes to the user before choosing the accepted set.
const accepted = changes.filter((change) => !change.conflict);
const merged = mergeVariantSource(customer, updatedSource, accepted);
export { changes, merged };