WorkflowDesigner
캔버스 기반 워크플로우 편집기 컴포넌트
개요
WorkflowDesigner는 트리 구조의 워크플로우를 시각적으로 설계할 수 있는 캔버스 기반 편집기입니다. 노드와 커넥터를 자동 레이아웃하며, 팔레트에서 노드를 추가하고 캔버스에서 편집할 수 있습니다.
주요 특징
- ✅ 자동 레이아웃:
parentId기반 트리 구조를 자동으로 배치 - ✅ 팔레트: 좌측 사이드바에서 노드 유형 선택 및 추가
- ✅ 캔버스 조작: 위/아래 이동(휠), 패닝(드래그·스페이스+드래그·미들 클릭), 줌(툴바), 전체 화면
- ✅ 분기 노드: 조건 분기(Yes/No 등)와 라벨/색상 지원, 빈 분기 레인에 + 버튼 자동 표시 (
childrenLayout: "branch") - ✅ 종속 노드: 특정 노드 추가 시 자동으로 따라 붙는 종속(attached) 노드 지원
- ✅ 사이드 패널:
sidePanelprop으로 우측 패널 렌더링 (전체 화면에서도 표시) - ✅ 커스텀 렌더링:
renderNode로 노드 내부 커스터마이징 - ✅ 읽기 전용:
readOnly모드 지원 - ✅ Controlled/Uncontrolled: 선택 상태, 노드 배열 모두 제어 가능
아키텍처
| 영역 | 설명 |
|---|---|
| Palette | 좌측 사이드바. nodeTypes에 정의된 노드 유형을 그룹별로 표시 |
| Header | 캔버스 상단. title/description 또는 커스텀 header 렌더링 |
| Canvas | 중앙 캔버스. 노드, 커넥터, + 버튼을 렌더링 |
| Toolbar | 좌측 하단. 줌, 패닝/선택 모드, 화면 맞춤, 전체 화면 버튼 |
기본 사용
Preview
Node Status
노드는 status 속성으로 시각적 상태를 표현합니다.
Preview
Status 설명
| Status | 설명 | 시각적 표현 |
|---|---|---|
| default | 기본 상태 | 기본 테두리 |
| selected | 선택됨 | Primary 테두리 + 그림자 |
| completed | 완료됨 | 녹색 테두리 |
| error | 오류 | 빨간 테두리 + 그림자 |
| disabled | 비활성 | 50% 투명도, 클릭 불가 |
| locked | 잠김 | 70% 투명도, 잠금 아이콘 |
사용 예시
예시 1: 분기 워크플로우
조건 노드로 Yes/No 분기를 만들 수 있습니다.
Preview
예시 2: onNodesChange를 활용한 편집 모드
onNodesChange를 전달하면 내장 추가/삭제 로직이 활성화됩니다.
import { WorkflowDesigner } from "@vortex/ui-icignal"
import type { WorkflowNode, NodeTypeDefinition } from "@vortex/ui-icignal"
import { useState } from "react"
function WorkflowEditor() {
const [nodes, setNodes] = useState<WorkflowNode[]>([
{ id: "1", type: "trigger", title: "시작", parentId: null },
])
const nodeTypes: NodeTypeDefinition[] = [
{ type: "trigger", label: "트리거", maxCount: 1, maxChildren: 1 },
{ type: "action", label: "액션", maxChildren: 1 },
]
return (
<div style={{ height: 600 }}>
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={setNodes}
onNodeSelect={(id) => console.log("선택:", id)}
/>
</div>
)
}예시 3: 커스텀 노드 렌더링
renderNode prop으로 노드 내부를 자유롭게 커스터마이징할 수 있습니다.
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={setNodes}
renderNode={({ node, isSelected }) => (
<div className="p-3">
<div className="font-semibold">{node.title}</div>
{node.description && (
<div className="text-xs text-muted-foreground mt-1">
{node.description}
</div>
)}
{isSelected && (
<div className="text-xs text-primary mt-2">선택됨</div>
)}
</div>
)}
/>예시 4: 커스텀 헤더
header prop으로 캔버스 상단에 커스텀 UI를 배치하거나, title/description으로 간단히 텍스트를 표시합니다.
// 간단한 텍스트 헤더
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
title="캠페인 워크플로우"
description="고객 여정을 설계하세요"
/>
// 커스텀 헤더
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
header={
<div className="flex items-center justify-between">
<h3>캠페인 A</h3>
<button>저장</button>
</div>
}
/>예시 5: 팔레트 3상태 (isAllowed + isRecommended)
NodeTypeDefinition.isAllowed로 추가 가능 여부(disabled)를, isRecommended로 하이라이트 여부를 각각 제어할 수 있습니다.
import { WorkflowDesigner } from "@vortex/ui-icignal"
import type { WorkflowNode, NodeTypeDefinition, CanAddToContext } from "@vortex/ui-icignal"
import { useState } from "react"
function WorkflowEditorWithPaletteStates() {
const [nodes, setNodes] = useState<WorkflowNode[]>([
{ id: "1", type: "start", title: "시작", parentId: null },
])
const nodeTypes: NodeTypeDefinition[] = [
{ type: "start", label: "시작", maxCount: 1, maxChildren: 1 },
// 정적 목록: start, target 뒤에만 추가 가능
{ type: "target", label: "대상자", isAllowed: ["start", "target"] },
{
type: "benefit",
label: "혜택",
// 동적 판단: target/condition 뒤 + 중복 방지
isAllowed: ({ parentNode, siblings }: CanAddToContext) => {
if (!["target", "condition"].includes(parentNode.type)) return false
return !siblings.some((s) => s.type === "benefit")
},
// 추천: target 뒤에서만 하이라이트
isRecommended: ({ parentNode }: CanAddToContext) =>
parentNode.type === "target",
},
{ type: "message", label: "메시지", isAllowed: ["target", "benefit"] },
]
return (
<div style={{ height: 600 }}>
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={setNodes}
/>
</div>
)
}팔레트 3상태
| 상태 | 조건 | 시각 효과 | 클릭 |
|---|---|---|---|
| Highlighted | isAllowed 통과 + isRecommended true | Primary 배경색 | ✅ |
| Normal | isAllowed 통과 + isRecommended false | 기본 (opacity 1) | ✅ |
| Disabled | isAllowed 불통과 또는 maxCount 도달 | opacity 0.35 | ❌ |
isRecommended를 하나도 정의하지 않으면 모든 available 항목이 자동으로 highlighted됩니다 (기존 동작 호환). 하나라도 정의하면isRecommended미정의 노드는 normal(비추천) 상태로 표시됩니다.
예시 6: 수평 레이아웃
layoutDirection="horizontal"로 왼→오른쪽 방향 트리를 구성합니다.
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
layoutDirection="horizontal"
onNodesChange={setNodes}
/>API Reference
WorkflowDesignerProps
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | WorkflowNode[] | 필수 | 노드 목록 |
nodeTypes | NodeTypeDefinition[] | 필수 | 노드 유형 정의 (팔레트에 표시) |
connectors | WorkflowConnector[] | 자동 생성 | 커넥터 목록 (생략 시 parentId에서 자동 생성) |
selectedNodeId | string | null | - | 현재 선택된 노드 ID (controlled) |
focusedNodeId | string | null | null | 포커스 하이라이트 노드 ID |
invalidNodeIds | string[] | [] | 유효성 오류 노드 ID 목록 |
renderNode | (props: WorkflowNodeRenderProps) => ReactNode | - | 커스텀 노드 렌더러 |
nodeActions | WorkflowNodeAction[] | 기본(삭제, 설정) | 노드 hover/선택 시 표시되는 액션 버튼 |
paletteTitle | string | "워크플로우 구성" | 팔레트 타이틀 |
title | string | - | 캔버스 상단 타이틀 |
description | string | - | 캔버스 상단 설명 |
header | ReactNode | - | 커스텀 헤더 (설정 시 title/description보다 우선) |
readOnly | boolean | false | 읽기 전용 모드 |
defaultScale | number | 1 | 초기 줌 레벨 |
minScale | number | 0.25 | 최소 줌 레벨 |
maxScale | number | 2 | 최대 줌 레벨 |
nodeWidth | number | 200 | 노드 기본 너비 (px) |
nodeGapX | number | 60 | 노드 간 수평 간격 (px) |
nodeGapY | number | 60 | 노드 간 수직 간격 (px) |
layoutDirection | "vertical" | "horizontal" | "vertical" | 레이아웃 방향 |
generateNodeId | () => string | crypto.randomUUID | 노드 ID 생성기 |
canShowAddButton | (parentNodeId: string) => boolean | - | 커넥터 + 버튼 표시 여부 외부 제어 (미제공 시 자동 판단) |
sidePanel | ReactNode | - | 우측 사이드 패널 (전체 화면 시에도 함께 표시됨) |
className | string | - | 루트 요소 CSS 클래스 |
Events
| Event | Type | Description |
|---|---|---|
onNodeSelect | (nodeId: string | null) => void | 노드 선택 시 |
onNodeDblClick | (nodeId: string) => void | 노드 더블클릭 시 |
onNodeAdd | (nodeType: string, parentNodeId: string, insertBeforeNodeId?: string) => void | 노드 추가 시 |
onNodeDelete | (nodeId: string) => void | 노드 삭제 시 |
onNodeSettings | (nodeId: string) => void | 노드 설정 버튼 클릭 시 |
onNodesChange | (nodes: WorkflowNode[]) => void | 내장 추가/삭제 후 노드 배열 변경 시 |
onChange | (nodes: WorkflowNode[], connectors: WorkflowConnector[]) => void | 데이터 변경 시 |
onCanvasClick | () => void | 캔버스 빈 영역 클릭 시 (선택 초기화) |
CanAddToContext
NodeTypeDefinition.isAllowed (함수형) 및 isRecommended 콜백에 전달되는 컨텍스트입니다.
| Property | Type | Description |
|---|---|---|
parentNode | WorkflowNode | + 버튼이 클릭된 부모 노드 |
siblings | WorkflowNode[] | 부모의 현재 직속 자식 노드 목록 |
direction | "vertical" | "horizontal" | null | 추가 방향 힌트 |
insertBeforeNodeId | string | null | 삽입 대상 노드 ID (기존 자식 앞에 끼워넣기 시) |
branchIndex | number | null | 분기 슬롯 인덱스 (branch 부모의 특정 레인) |
allNodes | WorkflowNode[] | 전체 노드 배열 |
WorkflowNode
노드 데이터 구조입니다.
| Property | Type | Default | Description |
|---|---|---|---|
id | string | 필수 | 노드 고유 식별자 |
type | string | 필수 | 노드 유형 (NodeTypeDefinition.type과 매칭) |
title | string | 필수 | 노드 타이틀 |
description | string | - | 노드 설명 (요약 정보) |
caption | string | - | 우상단 캡션 (날짜, 상태 텍스트 등) |
status | WorkflowNodeStatus | "default" | 노드 상태 |
icon | ReactNode | - | 노드 아이콘 |
badge | string | - | 상태 뱃지 텍스트 |
parentId | string | null | - | 부모 노드 ID (null = 루트 노드) |
branchIndex | number | 0 | 부모 노드의 분기 인덱스 (0-based) |
maxChildren | number | - | 이 노드의 최대 자식 수 (개별 오버라이드) |
data | Record<string, unknown> | - | 사용자 정의 데이터 |
locked | boolean | false | 잠금 상태 |
invalid | boolean | false | 유효성 오류 여부 |
attachedToId | string | - | 종속 대상 노드 ID (종속 노드일 때 설정) |
NodeTypeDefinition
팔레트에 표시되는 노드 유형 정의입니다.
| Property | Type | Default | Description |
|---|---|---|---|
type | string | 필수 | 노드 유형 식별자 |
label | string | 필수 | 노드 유형 라벨 |
icon | ReactNode | - | 노드 아이콘 |
group | string | - | 팔레트 그룹명 |
isAllowed | string[] | (ctx: CanAddToContext) => boolean | - | 추가 가능 여부 (disabled 결정). 배열: 허용 부모 타입, 함수: 동적 판단 |
isRecommended | (ctx: CanAddToContext) => boolean | - | 추천 여부 (하이라이트 결정). 미제공 시 normal 상태 (전체 미정의면 모두 highlighted) |
maxCount | number | - | 최대 배치 가능 수 |
maxChildren | number | 1 | 기본 최대 자식 수 (1=선형, 2+=분기) |
isBranching | boolean | false | 조건 분기 노드 여부 |
branchLabels | string[] | - | 분기 라벨 (예: ["Yes", "No"]) |
branchColors | string[] | - | 분기 색상 (예: ["#22c55e", "#ef4444"]) |
childrenLayout | "vertical" | "horizontal" | "branch" | string[] | "vertical" | 자식 배치 방향 ("branch": 분기 레인별 배치 + 빈 레인 placeholder) |
attachedNodes | AttachedNodeDefinition[] | - | 노드 추가 시 자동 생성되는 종속 노드 정의 |
hidden | boolean | false | true이면 팔레트에서 숨김 (종속 노드 전용) |
allowedAfter | string[] | - | ⚠️ deprecated — isAllowed를 사용하세요 |
canAddTo | (ctx: CanAddToContext) => boolean | - | ⚠️ deprecated — isAllowed를 사용하세요 |
AttachedNodeDefinition
종속 노드 자동 생성 시 사용되는 정의입니다. NodeTypeDefinition.attachedNodes에서 사용됩니다.
| Property | Type | Description |
|---|---|---|
type | string | 종속 노드의 유형 (nodeTypes의 type과 매칭) |
title | string | 종속 노드의 기본 타이틀 |
WorkflowNodeAction
노드 hover/선택 시 표시되는 액션 버튼입니다. position으로 상단/하단 배치를 지정할 수 있습니다.
| Property | Type | Default | Description |
|---|---|---|---|
id | string | 필수 | 액션 식별자 |
icon | ReactNode | 필수 | 액션 아이콘 |
label | string | 필수 | 액션 라벨 (툴팁) |
onClick | (nodeId: string) => void | 필수 | 클릭 핸들러 |
visible | (node: WorkflowNode) => boolean | - | 표시 조건 (생략 시 항상 표시) |
position | "top" | "bottom" | "top" | 렌더링 위치 (노드 상단 또는 하단) |
WorkflowNodeRenderProps
renderNode 콜백에 전달되는 props입니다.
| Property | Type | Description |
|---|---|---|
node | WorkflowNode | 노드 데이터 |
isSelected | boolean | 선택 상태 |
isFocused | boolean | 포커스 상태 |
isHighlighted | boolean | 하이라이트 상태 |
WorkflowConnector
노드 간 연결선 데이터입니다. 생략 시 parentId 관계에서 자동 생성됩니다.
| Property | Type | Description |
|---|---|---|
id | string | 커넥터 고유 식별자 |
sourceId | string | 시작 노드 ID |
targetId | string | 끝 노드 ID |
label | string | 커넥터 라벨 |
color | string | 커넥터 색상 |
branchIndex | number | 분기 인덱스 |
기본 사용법
import { WorkflowDesigner } from "@vortex/ui-icignal"
import type { WorkflowNode, NodeTypeDefinition } from "@vortex/ui-icignal"
import { useState } from "react"
const nodeTypes: NodeTypeDefinition[] = [
{ type: "trigger", label: "트리거", maxCount: 1, maxChildren: 1 },
{ type: "action", label: "액션", maxChildren: 1 },
{ type: "condition", label: "조건", maxChildren: 2, isBranching: true,
branchLabels: ["Yes", "No"], branchColors: ["#22c55e", "#ef4444"] },
]
function MyWorkflow() {
const [nodes, setNodes] = useState<WorkflowNode[]>([
{ id: "1", type: "trigger", title: "시작", parentId: null },
])
const [selected, setSelected] = useState<string | null>(null)
return (
<div style={{ height: 600 }}>
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
selectedNodeId={selected}
onNodeSelect={setSelected}
onNodesChange={setNodes}
onNodeDelete={(id) => console.log("삭제:", id)}
onNodeSettings={(id) => console.log("설정:", id)}
/>
</div>
)
}노드 액션 버튼 위치 지정
WorkflowNodeAction.position을 사용하면 각 액션 버튼을 노드 상단 또는 하단에 배치할 수 있습니다.
"top"(기본값): 노드 위에 표시 (기존 삭제 버튼 위치)"bottom": 노드 아래에 표시
import type { WorkflowNodeAction } from "@vortex/ui-icignal"
import { Trash2, Settings, Play } from "lucide-react"
const nodeActions: WorkflowNodeAction[] = [
{
id: "delete",
icon: <Trash2 size={14} />,
label: "삭제",
position: "top", // 기존과 동일하게 상단
onClick: (nodeId) => handleDelete(nodeId),
visible: (node) => !node.locked && !!node.parentId,
},
{
id: "settings",
icon: <Settings size={14} />,
label: "설정",
position: "bottom", // 노드 하단에 배치
onClick: (nodeId) => handleSettings(nodeId),
},
{
id: "run",
icon: <Play size={14} />,
label: "실행",
position: "bottom",
onClick: (nodeId) => handleRun(nodeId),
visible: (node) => node.type === "start", // 시작 노드에서만 표시
},
]
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
nodeActions={nodeActions}
onNodesChange={setNodes}
/>시작 노드(루트 노드)도 일반 노드와 동일하게 액션 버튼을 지원합니다.
visible콜백에서node.type이나node.parentId를 확인하여 노드별로 액션을 제어하세요.
커넥터 + 버튼 동적 Show/Hide
커넥터의 + 버튼은 추가 가능한 노드 유형이 없으면 자동으로 숨겨집니다. 추가로 canShowAddButton prop을 사용해 외부에서 직접 제어할 수도 있습니다.
자동 숨김 (기본 동작)
- 부모 노드의
maxChildren에 도달한 경우 isAllowed규칙으로 추가 가능한 nodeType이 하나도 없는 경우
→ 해당 부모 노드의 + 버튼이 자동으로 숨겨집니다.
외부 제어 (canShowAddButton)
canShowAddButton prop을 제공하면 자동 계산을 대체하여 외부에서 + 버튼 표시 여부를 결정합니다.
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={setNodes}
canShowAddButton={(parentNodeId) => {
// 특정 노드 이후에는 + 버튼을 숨김
const node = nodes.find((n) => n.id === parentNodeId)
if (node?.type === "end") return false
return true
}}
/>
canShowAddButton이 제공되면 내장 자동 숨김 로직을 완전히 대체합니다. 제공하지 않으면isAllowed/maxChildren기반 자동 판단이 적용됩니다.
캔버스 조작
마우스/키보드
| 조작 | 동작 |
|---|---|
| 마우스 휠 | 캔버스 위/아래 이동 (가로 스크롤은 좌우 이동) |
| 드래그 | 선택 모드에서 노드 선택, 패닝 모드에서 캔버스 이동 |
| 스페이스 + 드래그 | 캔버스 패닝 |
| 미들 클릭 + 드래그 | 캔버스 패닝 |
| 노드 클릭 | 노드 선택 |
| 노드 더블클릭 | onNodeDblClick 호출 |
| 캔버스 빈 영역 클릭 | 선택 초기화 |
툴바 버튼
| 버튼 | 동작 |
|---|---|
| 확대 (ZoomIn) | 줌 인 (+0.15 step) |
| 축소 (ZoomOut) | 줌 아웃 (-0.15 step) |
| 이동 (Hand) | 패닝 모드 |
| 선택 (MousePointer2) | 선택 모드 (기본) |
| 화면 맞춤 (Focus) | 모든 노드가 보이도록 줌/패닝 조정 |
| 전체 화면 (Maximize/Minimize) | 전체 화면 토글 (ESC 키로 해제). 전체 화면 모드에서는 Minimize 아이콘으로 전환 |
노드 추가/삭제 플로우
추가
- 커넥터 중간 또는 리프 노드 하단의 + 버튼 클릭
- 팔레트에서 추가 가능한 노드 유형이 하이라이트됨 (
isAllowed기반 필터링 +isRecommended로 강조 구분) - 노드 유형 클릭 →
onNodesChange로 새 노드가 포함된 배열 전달 onNodeAdd콜백도 함께 호출 (기존 호환)
삭제
- 노드 hover/선택 시 상단 삭제 버튼 클릭
onNodesChange가 있으면 내장 로직으로 해당 노드 + 모든 하위 노드를 일괄 삭제- 종속 노드(
attachedToId)도 함께 삭제됨 - 종속 노드는 단독 삭제 불가 (소유 노드 삭제 시에만 제거)
- 종속 노드(
onNodeDelete콜백도 함께 호출
전체 화면 (Fullscreen)
전체 화면 모드는 createPortal을 사용하여 document.body에 렌더링됩니다.
- 진입: 툴바의 전체 화면 버튼 클릭
- 해제: ESC 키 또는 전체 화면 버튼 재클릭
- z-index:
50(모달/토스트와 같은 Portal 레벨이며, DOM 순서로 모달이 위에 표시됨) - sidePanel: 전체 화면에서도
sidePanelprop이 정상 표시됨
분기 노드 (Branch Nodes)
childrenLayout: "branch"를 설정하면 분기 노드의 각 레인별로 자식이 배치되고, 자식이 없는 빈 레인에는 자동으로 + 버튼(placeholder)이 표시됩니다. 각 + 버튼에는 branchLabels와 branchColors가 함께 적용되어 Yes/No 같은 분기 경로를 한 번에 시각화할 수 있습니다.
정의
const nodeTypes: NodeTypeDefinition[] = [
{
type: "condition",
label: "조건",
maxChildren: 2,
isBranching: true,
branchLabels: ["Yes", "No"],
branchColors: ["#22c55e", "#ef4444"],
childrenLayout: "branch", // 핵심: 레인별 배치 + 빈 레인 placeholder
},
]동작
- 레인 개수:
branchLabels.length→maxChildren→ 기본2순으로 결정됨 - 빈 레인 placeholder: 자식이 없는 레인마다 라벨/색상이 적용된 + 버튼이 자동 표시됨
- + 버튼 클릭: 해당 레인의
branchIndex로 새 자식이 추가됨 (자동 위치 이동 없음) - 일부만 채워진 경우: 채워진 레인은 기존 분기 커넥터로, 빈 레인에는 placeholder가 표시됨
- 모든 레인이 채워지면 placeholder는 숨겨짐
vs. isBranching: true 단독
| 설정 | 레인 공간 예약 | 빈 레인 + 버튼 | 분기 색상/라벨 |
|---|---|---|---|
isBranching: true | ❌ | ❌ (우측 + 만) | ✅ |
childrenLayout: "branch" | ✅ | ✅ | ✅ |
기존
isBranching: true설정은 호환성을 위해 유지되며, 새 분기 UX(빈 레인 + 버튼)를 원하면childrenLayout: "branch"를 함께 지정하세요.
종속 노드 (Attached Nodes)
특정 노드 유형을 추가할 때 자동으로 함께 생성되는 종속 노드를 정의할 수 있습니다.
정의
const nodeTypes: NodeTypeDefinition[] = [
{
type: "message",
label: "메시지",
maxChildren: 1,
attachedNodes: [
{ type: "schedule", title: "발송 스케줄" },
],
},
{
type: "schedule",
label: "발송 스케줄",
maxChildren: 1,
hidden: true, // 팔레트에서 숨김
},
]동작
- “메시지” 노드 추가 시 → “발송 스케줄” 종속 노드가 자동 생성됨
- 종속 노드는
attachedToId로 소유 노드에 연결됨 - 종속 노드는 팔레트에서 숨겨지고(
hidden: true), 단독 삭제 불가 - 소유 노드 삭제 시 종속 노드도 함께 삭제됨
- 종속 노드와 다음 노드 사이의 + 버튼은 숨겨짐
사이드 패널 (sidePanel)
sidePanel prop을 사용하여 워크플로우 디자이너 내부에 우측 패널을 렌더링할 수 있습니다. 전체 화면 모드에서도 Portal 내부에 포함되어 정상 표시됩니다.
<WorkflowDesigner
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={setNodes}
sidePanel={
selectedNode ? (
<div className="absolute inset-y-0 right-0 w-80 border-l bg-background shadow-lg z-10">
<h3>{selectedNode.title} 설정</h3>
{/* 설정 폼 */}
</div>
) : undefined
}
/>접근성
권장 사항
- ✅ 각 노드에
title을 설정하여 용도를 명확히 전달 - ✅
readOnly모드에서 + 버튼과 노드 액션 버튼이 자동 숨김 - ✅ 키보드: 스페이스바로 패닝 모드 전환
- ❌ 컨테이너에 충분한 높이(
min-height: 400px)를 지정하지 않으면 콘텐츠가 잘릴 수 있음