WorkflowDesigner
캔버스 기반 워크플로우 편집기 컴포넌트
개요
WorkflowDesigner는 트리 구조의 워크플로우를 시각적으로 설계할 수 있는 캔버스 기반 편집기입니다. 노드와 커넥터를 자동 레이아웃하며, 팔레트에서 노드를 추가하고 캔버스에서 편집할 수 있습니다.
주요 특징
- ✅ 자동 레이아웃:
parentId기반 트리 구조를 자동으로 배치 - ✅ 팔레트: 좌측 사이드바에서 노드 유형 선택 및 추가
- ✅ 캔버스 조작: 줌(휠), 패닝(스페이스+드래그, 미들 클릭), 전체 화면
- ✅ 분기 노드: 조건 분기(Yes/No 등)와 라벨/색상 지원
- ✅ 커스텀 렌더링:
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
워크플로우
시작
트리거
흐름
조건
실행
액션
고객 방문
VIP 여부
VIP 혜택 안내
일반 안내
예시 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: 수평 레이아웃
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 생성기 |
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 | 캔버스 빈 영역 클릭 시 (선택 초기화) |
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 | 유효성 오류 여부 |
NodeTypeDefinition
팔레트에 표시되는 노드 유형 정의입니다.
| Property | Type | Default | Description |
|---|---|---|---|
type | string | 필수 | 노드 유형 식별자 |
label | string | 필수 | 노드 유형 라벨 |
icon | ReactNode | - | 노드 아이콘 |
group | string | - | 팔레트 그룹명 |
allowedAfter | string[] | - | 허용된 부모 노드 유형 (비어있으면 제한 없음) |
maxCount | number | - | 최대 배치 가능 수 |
maxChildren | number | 1 | 기본 최대 자식 수 (1=선형, 2+=분기) |
isBranching | boolean | false | 조건 분기 노드 여부 |
branchLabels | string[] | - | 분기 라벨 (예: ["Yes", "No"]) |
branchColors | string[] | - | 분기 색상 (예: ["#22c55e", "#ef4444"]) |
childrenLayout | "vertical" | "horizontal" | string[] | "vertical" | 자식 배치 방향 |
WorkflowNodeAction
노드 hover/선택 시 상단에 표시되는 액션 버튼입니다.
| Property | Type | Description |
|---|---|---|
id | string | 액션 식별자 |
icon | ReactNode | 액션 아이콘 |
label | string | 액션 라벨 (툴팁) |
onClick | (nodeId: string) => void | 클릭 핸들러 |
visible | (node: WorkflowNode) => boolean | 표시 조건 (생략 시 항상 표시) |
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>
)
}캔버스 조작
마우스/키보드
| 조작 | 동작 |
|---|---|
| 마우스 휠 | 줌 인/아웃 (마우스 위치 기준) |
| 스페이스 + 드래그 | 캔버스 패닝 |
| 미들 클릭 + 드래그 | 캔버스 패닝 |
| 노드 클릭 | 노드 선택 |
| 노드 더블클릭 | onNodeDblClick 호출 |
| 캔버스 빈 영역 클릭 | 선택 초기화 |
툴바 버튼
| 버튼 | 동작 |
|---|---|
| 확대 (ZoomIn) | 줌 인 (+0.15 step) |
| 축소 (ZoomOut) | 줌 아웃 (-0.15 step) |
| 이동 (Hand) | 패닝 모드 |
| 선택 (MousePointer2) | 선택 모드 (기본) |
| 화면 맞춤 (Maximize) | 모든 노드가 보이도록 줌/패닝 조정 |
| 전체 화면 (Fullscreen) | 전체 화면 토글 |
노드 추가/삭제 플로우
추가
- 커넥터 중간 또는 리프 노드 하단의 + 버튼 클릭
- 팔레트에서 추가 가능한 노드 유형이 하이라이트됨 (
allowedAfter기반 필터링) - 노드 유형 클릭 →
onNodesChange로 새 노드가 포함된 배열 전달 onNodeAdd콜백도 함께 호출 (기존 호환)
삭제
- 노드 hover/선택 시 상단 삭제 버튼 클릭
onNodesChange가 있으면 내장 로직으로 자식 재연결 처리:- 자식이 1개이고 부모가 있으면 → 부모에 재연결
- 그 외 → 하위 노드 모두 삭제
onNodeDelete콜백도 함께 호출
접근성
권장 사항
- ✅ 각 노드에
title을 설정하여 용도를 명확히 전달 - ✅
readOnly모드에서 + 버튼과 노드 액션 버튼이 자동 숨김 - ✅ 키보드: 스페이스바로 패닝 모드 전환
- ❌ 컨테이너에 충분한 높이(
min-height: 400px)를 지정하지 않으면 콘텐츠가 잘릴 수 있음
관련 컴포넌트
Last updated on