Skip to Content
ComponentsIcignalWorkflowDesigner

WorkflowDesigner

캔버스 기반 워크플로우 편집기 컴포넌트


개요

WorkflowDesigner는 트리 구조의 워크플로우를 시각적으로 설계할 수 있는 캔버스 기반 편집기입니다. 노드와 커넥터를 자동 레이아웃하며, 팔레트에서 노드를 추가하고 캔버스에서 편집할 수 있습니다.

주요 특징

  • 자동 레이아웃: parentId 기반 트리 구조를 자동으로 배치
  • 팔레트: 좌측 사이드바에서 노드 유형 선택 및 추가
  • 캔버스 조작: 위/아래 이동(휠), 패닝(드래그·스페이스+드래그·미들 클릭), 줌(툴바), 전체 화면
  • 분기 노드: 조건 분기(Yes/No 등)와 라벨/색상 지원, 빈 분기 레인에 + 버튼 자동 표시 (childrenLayout: "branch")
  • 종속 노드: 특정 노드 추가 시 자동으로 따라 붙는 종속(attached) 노드 지원
  • 사이드 패널: sidePanel prop으로 우측 패널 렌더링 (전체 화면에서도 표시)
  • 커스텀 렌더링: renderNode로 노드 내부 커스터마이징
  • 읽기 전용: readOnly 모드 지원
  • Controlled/Uncontrolled: 선택 상태, 노드 배열 모두 제어 가능

아키텍처

영역설명
Palette좌측 사이드바. nodeTypes에 정의된 노드 유형을 그룹별로 표시
Header캔버스 상단. title/description 또는 커스텀 header 렌더링
Canvas중앙 캔버스. 노드, 커넥터, + 버튼을 렌더링
Toolbar좌측 하단. 줌, 패닝/선택 모드, 화면 맞춤, 전체 화면 버튼

기본 사용

워크플로우
시작
실행
흐름
시작
이메일 발송
알림 전송

Node Status

노드는 status 속성으로 시각적 상태를 표현합니다.

워크플로우 구성
완료됨
오류 발생
오류
비활성
잠김

Status 설명

Status설명시각적 표현
default기본 상태기본 테두리
selected선택됨Primary 테두리 + 그림자
completed완료됨녹색 테두리
error오류빨간 테두리 + 그림자
disabled비활성50% 투명도, 클릭 불가
locked잠김70% 투명도, 잠금 아이콘

사용 예시

예시 1: 분기 워크플로우

조건 노드로 Yes/No 분기를 만들 수 있습니다.

워크플로우
시작
흐름
실행
YesNo
고객 방문
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: 팔레트 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상태

상태조건시각 효과클릭
HighlightedisAllowed 통과 + isRecommended truePrimary 배경색
NormalisAllowed 통과 + isRecommended false기본 (opacity 1)
DisabledisAllowed 불통과 또는 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

PropTypeDefaultDescription
nodesWorkflowNode[]필수노드 목록
nodeTypesNodeTypeDefinition[]필수노드 유형 정의 (팔레트에 표시)
connectorsWorkflowConnector[]자동 생성커넥터 목록 (생략 시 parentId에서 자동 생성)
selectedNodeIdstring | null-현재 선택된 노드 ID (controlled)
focusedNodeIdstring | nullnull포커스 하이라이트 노드 ID
invalidNodeIdsstring[][]유효성 오류 노드 ID 목록
renderNode(props: WorkflowNodeRenderProps) => ReactNode-커스텀 노드 렌더러
nodeActionsWorkflowNodeAction[]기본(삭제, 설정)노드 hover/선택 시 표시되는 액션 버튼
paletteTitlestring"워크플로우 구성"팔레트 타이틀
titlestring-캔버스 상단 타이틀
descriptionstring-캔버스 상단 설명
headerReactNode-커스텀 헤더 (설정 시 title/description보다 우선)
readOnlybooleanfalse읽기 전용 모드
defaultScalenumber1초기 줌 레벨
minScalenumber0.25최소 줌 레벨
maxScalenumber2최대 줌 레벨
nodeWidthnumber200노드 기본 너비 (px)
nodeGapXnumber60노드 간 수평 간격 (px)
nodeGapYnumber60노드 간 수직 간격 (px)
layoutDirection"vertical" | "horizontal""vertical"레이아웃 방향
generateNodeId() => stringcrypto.randomUUID노드 ID 생성기
canShowAddButton(parentNodeId: string) => boolean-커넥터 + 버튼 표시 여부 외부 제어 (미제공 시 자동 판단)
sidePanelReactNode-우측 사이드 패널 (전체 화면 시에도 함께 표시됨)
classNamestring-루트 요소 CSS 클래스

Events

EventTypeDescription
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 콜백에 전달되는 컨텍스트입니다.

PropertyTypeDescription
parentNodeWorkflowNode+ 버튼이 클릭된 부모 노드
siblingsWorkflowNode[]부모의 현재 직속 자식 노드 목록
direction"vertical" | "horizontal" | null추가 방향 힌트
insertBeforeNodeIdstring | null삽입 대상 노드 ID (기존 자식 앞에 끼워넣기 시)
branchIndexnumber | null분기 슬롯 인덱스 (branch 부모의 특정 레인)
allNodesWorkflowNode[]전체 노드 배열

WorkflowNode

노드 데이터 구조입니다.

PropertyTypeDefaultDescription
idstring필수노드 고유 식별자
typestring필수노드 유형 (NodeTypeDefinition.type과 매칭)
titlestring필수노드 타이틀
descriptionstring-노드 설명 (요약 정보)
captionstring-우상단 캡션 (날짜, 상태 텍스트 등)
statusWorkflowNodeStatus"default"노드 상태
iconReactNode-노드 아이콘
badgestring-상태 뱃지 텍스트
parentIdstring | null-부모 노드 ID (null = 루트 노드)
branchIndexnumber0부모 노드의 분기 인덱스 (0-based)
maxChildrennumber-이 노드의 최대 자식 수 (개별 오버라이드)
dataRecord<string, unknown>-사용자 정의 데이터
lockedbooleanfalse잠금 상태
invalidbooleanfalse유효성 오류 여부
attachedToIdstring-종속 대상 노드 ID (종속 노드일 때 설정)

NodeTypeDefinition

팔레트에 표시되는 노드 유형 정의입니다.

PropertyTypeDefaultDescription
typestring필수노드 유형 식별자
labelstring필수노드 유형 라벨
iconReactNode-노드 아이콘
groupstring-팔레트 그룹명
isAllowedstring[] | (ctx: CanAddToContext) => boolean-추가 가능 여부 (disabled 결정). 배열: 허용 부모 타입, 함수: 동적 판단
isRecommended(ctx: CanAddToContext) => boolean-추천 여부 (하이라이트 결정). 미제공 시 normal 상태 (전체 미정의면 모두 highlighted)
maxCountnumber-최대 배치 가능 수
maxChildrennumber1기본 최대 자식 수 (1=선형, 2+=분기)
isBranchingbooleanfalse조건 분기 노드 여부
branchLabelsstring[]-분기 라벨 (예: ["Yes", "No"])
branchColorsstring[]-분기 색상 (예: ["#22c55e", "#ef4444"])
childrenLayout"vertical" | "horizontal" | "branch" | string[]"vertical"자식 배치 방향 ("branch": 분기 레인별 배치 + 빈 레인 placeholder)
attachedNodesAttachedNodeDefinition[]-노드 추가 시 자동 생성되는 종속 노드 정의
hiddenbooleanfalsetrue이면 팔레트에서 숨김 (종속 노드 전용)
allowedAfterstring[]-⚠️ deprecated — isAllowed를 사용하세요
canAddTo(ctx: CanAddToContext) => boolean-⚠️ deprecated — isAllowed를 사용하세요

AttachedNodeDefinition

종속 노드 자동 생성 시 사용되는 정의입니다. NodeTypeDefinition.attachedNodes에서 사용됩니다.

PropertyTypeDescription
typestring종속 노드의 유형 (nodeTypestype과 매칭)
titlestring종속 노드의 기본 타이틀

WorkflowNodeAction

노드 hover/선택 시 표시되는 액션 버튼입니다. position으로 상단/하단 배치를 지정할 수 있습니다.

PropertyTypeDefaultDescription
idstring필수액션 식별자
iconReactNode필수액션 아이콘
labelstring필수액션 라벨 (툴팁)
onClick(nodeId: string) => void필수클릭 핸들러
visible(node: WorkflowNode) => boolean-표시 조건 (생략 시 항상 표시)
position"top" | "bottom""top"렌더링 위치 (노드 상단 또는 하단)

WorkflowNodeRenderProps

renderNode 콜백에 전달되는 props입니다.

PropertyTypeDescription
nodeWorkflowNode노드 데이터
isSelectedboolean선택 상태
isFocusedboolean포커스 상태
isHighlightedboolean하이라이트 상태

WorkflowConnector

노드 간 연결선 데이터입니다. 생략 시 parentId 관계에서 자동 생성됩니다.

PropertyTypeDescription
idstring커넥터 고유 식별자
sourceIdstring시작 노드 ID
targetIdstring끝 노드 ID
labelstring커넥터 라벨
colorstring커넥터 색상
branchIndexnumber분기 인덱스

기본 사용법

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 아이콘으로 전환

노드 추가/삭제 플로우

추가

  1. 커넥터 중간 또는 리프 노드 하단의 + 버튼 클릭
  2. 팔레트에서 추가 가능한 노드 유형이 하이라이트됨 (isAllowed 기반 필터링 + isRecommended로 강조 구분)
  3. 노드 유형 클릭 → onNodesChange로 새 노드가 포함된 배열 전달
  4. onNodeAdd 콜백도 함께 호출 (기존 호환)

삭제

  1. 노드 hover/선택 시 상단 삭제 버튼 클릭
  2. onNodesChange가 있으면 내장 로직으로 해당 노드 + 모든 하위 노드를 일괄 삭제
    • 종속 노드(attachedToId)도 함께 삭제됨
    • 종속 노드는 단독 삭제 불가 (소유 노드 삭제 시에만 제거)
  3. onNodeDelete 콜백도 함께 호출

전체 화면 (Fullscreen)

전체 화면 모드는 createPortal을 사용하여 document.body에 렌더링됩니다.

  • 진입: 툴바의 전체 화면 버튼 클릭
  • 해제: ESC 키 또는 전체 화면 버튼 재클릭
  • z-index: 50 (모달/토스트와 같은 Portal 레벨이며, DOM 순서로 모달이 위에 표시됨)
  • sidePanel: 전체 화면에서도 sidePanel prop이 정상 표시됨

분기 노드 (Branch Nodes)

childrenLayout: "branch"를 설정하면 분기 노드의 각 레인별로 자식이 배치되고, 자식이 없는 빈 레인에는 자동으로 + 버튼(placeholder)이 표시됩니다. 각 + 버튼에는 branchLabelsbranchColors가 함께 적용되어 Yes/No 같은 분기 경로를 한 번에 시각화할 수 있습니다.

정의

const nodeTypes: NodeTypeDefinition[] = [ { type: "condition", label: "조건", maxChildren: 2, isBranching: true, branchLabels: ["Yes", "No"], branchColors: ["#22c55e", "#ef4444"], childrenLayout: "branch", // 핵심: 레인별 배치 + 빈 레인 placeholder }, ]

동작

  • 레인 개수: branchLabels.lengthmaxChildren → 기본 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)를 지정하지 않으면 콘텐츠가 잘릴 수 있음

관련 컴포넌트

  • DataTable: 테이블 형태의 데이터 표시
  • Editor: 리치 텍스트 편집기
Last updated on