Appearance
상태 관리
Zustand 기반 전역 상태 설계입니다.
스토어 구조
useAppStore는 6개 slice를 합성합니다:
typescript
useAppStore = create(
persist(
devtools((...a) => ({
...createCanvasSlice(...a), // tool, zoom, position
...createHistorySlice(...a), // undo/redo
...createSelectionSlice(...a), // selectedIds
...createEditorSlice(...a), // sidebar, labels
...createNodesSlice(...a), // nodes, nodeOrder
...createWorkspaceSlice(...a), // workspaceId, nodesLoaded, 저장 상태
})),
{ name: 'canvas-app-v1', version: 2, partialize },
),
);Slice 상세
CanvasSlice
| 상태 | 타입 | 설명 |
|---|---|---|
tool | 'move' | NodeTool | 현재 활성 도구 |
zoom | number | 줌 레벨 |
position | { x, y } | 팬 오프셋 |
canvasSize | { width, height } | 캔버스 DOM 크기 |
HistorySlice
실행 취소/다시 실행용 스택입니다. 현재는 tool(활성 도구) 변경만 스냅샷으로 저장합니다.
- 스냅샷 타입:
{ tool }(historySlice.ts) pushHistory()유틸이 있으나, 아직 어떤 액션에서도 호출되지 않아 undo/redo 커맨드는 사실상 비활성 상태입니다- 노드 추가·수정·삭제 히스토리는 향후
pushHistory연동 또는 별도 스냅샷 설계가 필요합니다
SelectionSlice
| 상태 | 설명 |
|---|---|
selectedIds | 선택된 노드 ID 배열. selectAll()은 ['__all__'] sentinel을 사용 |
deleteSelectionRequest | 삭제 요청 카운터. useCanvasSelection이 증가를 감지해 Fabric 객체를 제거 |
EditorSlice
| 상태 | 기본값 | 설명 |
|---|---|---|
isPropertiesSidebarOpen | true | 속성 패널 표시 |
isVisibleNodeLabels | true | 노드 표시 이름(라벨) 오버레이 표시 |
NodesSlice
| 상태 | 설명 |
|---|---|
nodes | Record<id, CanvasNodeState> |
nodeOrder | 렌더링/z-index 순서 |
WorkspaceSlice
| 상태 | 설명 |
|---|---|
workspaceId | 현재 편집 중인 워크스페이스 ID |
nodesLoaded | 서버에서 노드 문서 로드 완료 여부 |
isWorkspaceLoading | 문서 로드 중 |
isNodesSaving | 서버 저장 중 |
documentError | unauthorized / forbidden / not_found 등 |
커맨드 시스템
모든 사용자 동작은 Command로 선언됩니다:
typescript
interface Command {
id: string;
label: string;
shortcut?: string;
group?: string;
isActive?: (store) => boolean;
isDisabled?: (store) => boolean;
execute: (store) => void;
}실행:
typescript
executeCommand('history.undo', store);커맨드 그룹:
| group | 예시 |
|---|---|
tools | move, text |
history | undo, redo |
selections | selectAll, delete |
views | zoomIn, zoomOut, zoomToFit |
editor | togglePropertiesSidebar |
Persist (localStorage)
stores/persistence/appStorage.ts:
typescript
export const PERSIST_GROUPS = {
preferences: ['isPropertiesSidebarOpen', 'isVisibleNodeLabels'],
};- preferences — UI 설정만 localStorage에 저장
nodes,nodeOrder는 persist 대상이 아님 — 서버Node테이블에 저장
partializeAppState가 persist 대상만 추출합니다.
버전 관리
typescript
export const APP_STORAGE_KEY = 'canvas-app-v1';
export const APP_STORAGE_VERSION = 2;스키마 변경 시 version을 올리고 migrate 함수를 추가하는 것을 권장합니다.
서버 동기화
features/workspace/hooks/useWorkspaceDocument.ts:
- 로드:
GET /workspaces/:id/nodes→replaceDocument(nodes, nodeOrder)→nodesLoaded = true - 저장:
nodes/nodeOrder변경 시 500ms debounce 후PUT /workspaces/:id/nodes - 초기 로드 직후 1회 저장은
skipNextSaveRef로 스킵
개별 노드 PATCH가 아니라 문서 전체 스냅샷을 PUT합니다.
Fabric ↔ Store 동기화
- Hydration:
nodesLoaded후useCanvasHydration→hydrateCanvasFromStore로 store → Fabric 일괄 복원 - Sync:
useCanvasNodes가 실시간 양방향 처리 — 캔버스 조작 시 Fabric read →setNode/updateNode, store 변경 시 Fabric write - Selection:
useCanvasSelection이 Fabric selection ↔selectedIds동기화 및 삭제 처리 - Placement:
createState+createFabricObject로 store + Fabric 동시 추가 - Sync guard:
canvasSync.ts의syncingFromCanvasRef/isCanvasInteracting()으로 루프·덮어쓰기 방지
노드 타입별 변환은 NodeDefinition의 stateFromFabricObject / applyStateToFabricObject가 담당합니다. 메서드 역할·흐름은 노드 시스템을 참고하세요.
Devtools
Redux DevTools Extension으로 app-store 이름으로 inspect 가능합니다.