저장·자산·공유 연동
호스트가 자동 저장, 충돌 감지, 복구, 자산 업로드와 읽기 전용 공유를 구현하는 기준입니다.
reopt design업데이트
1. 패키지와 앱의 책임
onChange는 검증된 다음 문서를 전달하며 서버 저장을 수행하지 않습니다. status는 호스트가 전달하는 표시 문구입니다. 실제 저장 응답을 받기 전에는 저장 완료로 표시하지 마세요.
이 저장소의 Builder 어댑터는 EAV 저장·스냅샷·복구·공유를 구현하지만 npm 패키지에 백엔드가 포함되는 것은 아닙니다. 소비 앱은 데이터베이스나 파일 저장소를 자유롭게 선택할 수 있습니다.
2. 읽기와 마이그레이션
외부 파일과 서버 응답은 unknown으로 받고 파싱합니다. 이전 v1 자료를 허용하는 경로는 migrateDeckDocument를, 현재 v2 계약만 받는 API는 parseDeckDocument를 사용합니다.
열기에 실패했을 때 기존 문서나 로컬 복구본을 새 빈 문서로 덮어쓰지 마세요. 권한 오류, 파싱 오류, 자산 접근 실패를 구분해 재시도·다른 파일 선택·복구 선택을 제공합니다.
import {
migrateDeckDocument, parseDeckDocument,
} from "@reopt-ai/opt-deck/server";
export function openImportedJson(text: string) {
const input: unknown = JSON.parse(text);
// Accept legacy v1 and current v2. Throws on invalid content.
return migrateDeckDocument(input);
}
export function openCurrentServerRecord(payload: unknown) {
// Current v2 only. Use this at a host API boundary.
return parseDeckDocument(payload);
}3. 자동 저장과 서버 버전
document.revision은 편집 이력용 카운터입니다. 서버 동시성 검사는 별도의 serverVersion과 compare-and-swap으로 처리하세요. 저장 요청은 직렬화하고, 저장 중 새 편집이 들어오면 최신 스냅샷을 다음 요청으로 보냅니다.
409 충돌은 자동 덮어쓰기 대신 최신 서버 자료와 로컬 초안의 조정을 요구해야 합니다. 아래 함수는 저장 요청 한 번의 계약만 보여 줍니다. debounce·직렬 큐·재시도·계정 전환 처리는 소비 앱에서 구성합니다.
import type { DeckDocument } from "@reopt-ai/opt-deck/server";
export type StoredDeck = { document: DeckDocument; serverVersion: number };
// Host-defined endpoint and payload; not a built-in opt-deck API.
export async function saveSnapshot(
id: string, document: DeckDocument, serverVersion: number,
signal: AbortSignal,
): Promise<number> {
const response = await fetch("/api/presentations/" + encodeURIComponent(id), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ document, expectedVersion: serverVersion }),
signal,
});
if (response.status === 409) throw new Error("Reload or reconcile the conflicting version");
if (!response.ok) throw new Error("Save failed; retain the local draft");
const result: unknown = await response.json();
if (!result || typeof result !== "object" || !("serverVersion" in result)
|| !Number.isSafeInteger(result.serverVersion)) {
throw new Error("Invalid save response");
}
return result.serverVersion as number;
}4. 자산 업로드 계약
onAssetUpload는 File과 AbortSignal을 받고 DeckAsset을 반환합니다. 필수 필드는 id·type·name·src·mimeType·bytes·embeddable입니다. type은 image/font/video/audio/vector이며, 글꼴에는 family·weight·style·embedding 정보도 사용됩니다.
문서의 src는 편집·발표·출력 시 다시 접근할 수 있어야 합니다. 만료된 서명 URL을 영구 자산 주소처럼 저장하지 마세요. 업로드 취소와 문서 전환 뒤에는 완료 응답을 이전 문서에 적용하지 않습니다. 콜백이 없으면 로컬 자산을 문서에 포함합니다.
import type { DeckEditorProps } from "@reopt-ai/opt-deck";
import { assetSchema } from "@reopt-ai/opt-deck/server";
export const uploadAsset: NonNullable<DeckEditorProps["onAssetUpload"]> =
async (file, signal) => {
const body = new FormData();
body.set("file", file);
// The host validates ownership, media type and file size.
const response = await fetch("/api/presentation-assets", {
method: "POST", body, signal,
});
if (!response.ok) throw new Error("Asset upload failed");
return assetSchema.parse(await response.json());
};5. 복구와 사용자 전환
복구 키에는 계정·프로젝트·문서 ID를 포함하고 원본 serverVersion을 함께 기록합니다. 복구본이 서버보다 오래되었거나 서로 수정된 경우 사용자 선택 전에 자동 저장을 시작하지 않습니다.
로그아웃·계정 변경·다른 프로젝트 열기에서는 요청을 취소하고 늦은 응답을 무시하세요. 문서별 key로 편집기를 다시 마운트하면 선택·카메라·history가 이전 문서에서 넘어오는 것을 방지할 수 있습니다.