feat: 노드 기반 데이터 플로우 시스템 구현
- 노드 에디터 UI 구현 (React Flow 기반)
- TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드
- 드래그앤드롭 노드 추가 및 연결
- 속성 패널을 통한 노드 설정
- 실시간 필드 라벨 표시 (column_labels 테이블 연동)
- 데이터 변환 노드 (DataTransform) 기능
- EXPLODE: 구분자로 1개 행 → 여러 행 확장
- UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입
- In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기)
- 변환된 필드가 하위 액션 노드에 자동 전달
- 노드 플로우 실행 엔진
- 위상 정렬을 통한 노드 실행 순서 결정
- 레벨별 병렬 실행 (Promise.allSettled)
- 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵)
- 트랜잭션 기반 안전한 데이터 처리
- UPSERT 액션 로직 구현
- DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식
- 복합 충돌 키 지원 (예: sales_no + product_name)
- 파라미터 인덱스 정확한 매핑
- 데이터 소스 자동 감지
- 테이블 선택 데이터 (selectedRowsData) 자동 주입
- 폼 입력 데이터 (formData) 자동 주입
- TableSource 노드가 외부 데이터 우선 사용
- 버튼 컴포넌트 통합
- 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원
- 노드 플로우 선택 UI 추가
- API 클라이언트 통합 (Axios)
- 개발 문서 작성
- 노드 기반 제어 시스템 개선 계획
- 노드 연결 규칙 설계
- 노드 실행 엔진 설계
- 노드 구조 개선안
- 버튼 통합 분석
2025-10-02 16:22:29 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 노드 기반 플로우 에디터 메인 컴포넌트
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useCallback, useRef } from "react";
|
|
|
|
|
import ReactFlow, { Background, Controls, MiniMap, Panel, ReactFlowProvider, useReactFlow } from "reactflow";
|
|
|
|
|
import "reactflow/dist/style.css";
|
|
|
|
|
|
|
|
|
|
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
|
|
|
|
import { NodePalette } from "./sidebar/NodePalette";
|
|
|
|
|
import { PropertiesPanel } from "./panels/PropertiesPanel";
|
|
|
|
|
import { FlowToolbar } from "./FlowToolbar";
|
|
|
|
|
import { TableSourceNode } from "./nodes/TableSourceNode";
|
|
|
|
|
import { ExternalDBSourceNode } from "./nodes/ExternalDBSourceNode";
|
|
|
|
|
import { ConditionNode } from "./nodes/ConditionNode";
|
|
|
|
|
import { FieldMappingNode } from "./nodes/FieldMappingNode";
|
|
|
|
|
import { InsertActionNode } from "./nodes/InsertActionNode";
|
|
|
|
|
import { UpdateActionNode } from "./nodes/UpdateActionNode";
|
|
|
|
|
import { DeleteActionNode } from "./nodes/DeleteActionNode";
|
|
|
|
|
import { UpsertActionNode } from "./nodes/UpsertActionNode";
|
|
|
|
|
import { DataTransformNode } from "./nodes/DataTransformNode";
|
|
|
|
|
import { RestAPISourceNode } from "./nodes/RestAPISourceNode";
|
|
|
|
|
import { CommentNode } from "./nodes/CommentNode";
|
|
|
|
|
import { LogNode } from "./nodes/LogNode";
|
|
|
|
|
|
|
|
|
|
// 노드 타입들
|
|
|
|
|
const nodeTypes = {
|
|
|
|
|
// 데이터 소스
|
|
|
|
|
tableSource: TableSourceNode,
|
|
|
|
|
externalDBSource: ExternalDBSourceNode,
|
|
|
|
|
restAPISource: RestAPISourceNode,
|
|
|
|
|
// 변환/조건
|
|
|
|
|
condition: ConditionNode,
|
|
|
|
|
fieldMapping: FieldMappingNode,
|
|
|
|
|
dataTransform: DataTransformNode,
|
|
|
|
|
// 액션
|
|
|
|
|
insertAction: InsertActionNode,
|
|
|
|
|
updateAction: UpdateActionNode,
|
|
|
|
|
deleteAction: DeleteActionNode,
|
|
|
|
|
upsertAction: UpsertActionNode,
|
|
|
|
|
// 유틸리티
|
|
|
|
|
comment: CommentNode,
|
|
|
|
|
log: LogNode,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FlowEditor 내부 컴포넌트
|
|
|
|
|
*/
|
|
|
|
|
function FlowEditorInner() {
|
|
|
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
|
|
|
|
const { screenToFlowPosition } = useReactFlow();
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
nodes,
|
|
|
|
|
edges,
|
|
|
|
|
onNodesChange,
|
|
|
|
|
onEdgesChange,
|
|
|
|
|
onConnect,
|
|
|
|
|
addNode,
|
|
|
|
|
showPropertiesPanel,
|
|
|
|
|
selectNodes,
|
|
|
|
|
selectedNodes,
|
|
|
|
|
removeNodes,
|
|
|
|
|
} = useFlowEditorStore();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 노드 선택 변경 핸들러
|
|
|
|
|
*/
|
|
|
|
|
const onSelectionChange = useCallback(
|
|
|
|
|
({ nodes: selectedNodes }: { nodes: any[] }) => {
|
|
|
|
|
const selectedIds = selectedNodes.map((node) => node.id);
|
|
|
|
|
selectNodes(selectedIds);
|
|
|
|
|
console.log("🔍 선택된 노드:", selectedIds);
|
|
|
|
|
},
|
|
|
|
|
[selectNodes],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 키보드 이벤트 핸들러 (Delete/Backspace 키로 노드 삭제)
|
|
|
|
|
*/
|
|
|
|
|
const onKeyDown = useCallback(
|
|
|
|
|
(event: React.KeyboardEvent) => {
|
|
|
|
|
if ((event.key === "Delete" || event.key === "Backspace") && selectedNodes.length > 0) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
console.log("🗑️ 선택된 노드 삭제:", selectedNodes);
|
|
|
|
|
removeNodes(selectedNodes);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[selectedNodes, removeNodes],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 드래그 앤 드롭 핸들러
|
|
|
|
|
*/
|
|
|
|
|
const onDragOver = useCallback((event: React.DragEvent) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.dataTransfer.dropEffect = "move";
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const onDrop = useCallback(
|
|
|
|
|
(event: React.DragEvent) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
const type = event.dataTransfer.getData("application/reactflow");
|
|
|
|
|
if (!type) return;
|
|
|
|
|
|
|
|
|
|
const position = screenToFlowPosition({
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
y: event.clientY,
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-02 17:51:15 +09:00
|
|
|
// 🔥 노드 타입별 기본 데이터 설정
|
|
|
|
|
const defaultData: any = {
|
|
|
|
|
displayName: `새 ${type} 노드`,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 액션 노드의 경우 targetType 기본값 설정
|
|
|
|
|
if (["insertAction", "updateAction", "deleteAction", "upsertAction"].includes(type)) {
|
|
|
|
|
defaultData.targetType = "internal"; // 기본값: 내부 DB
|
|
|
|
|
defaultData.fieldMappings = [];
|
|
|
|
|
defaultData.options = {};
|
|
|
|
|
|
|
|
|
|
if (type === "updateAction" || type === "deleteAction") {
|
|
|
|
|
defaultData.whereConditions = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === "upsertAction") {
|
|
|
|
|
defaultData.conflictKeys = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: 노드 기반 데이터 플로우 시스템 구현
- 노드 에디터 UI 구현 (React Flow 기반)
- TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드
- 드래그앤드롭 노드 추가 및 연결
- 속성 패널을 통한 노드 설정
- 실시간 필드 라벨 표시 (column_labels 테이블 연동)
- 데이터 변환 노드 (DataTransform) 기능
- EXPLODE: 구분자로 1개 행 → 여러 행 확장
- UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입
- In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기)
- 변환된 필드가 하위 액션 노드에 자동 전달
- 노드 플로우 실행 엔진
- 위상 정렬을 통한 노드 실행 순서 결정
- 레벨별 병렬 실행 (Promise.allSettled)
- 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵)
- 트랜잭션 기반 안전한 데이터 처리
- UPSERT 액션 로직 구현
- DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식
- 복합 충돌 키 지원 (예: sales_no + product_name)
- 파라미터 인덱스 정확한 매핑
- 데이터 소스 자동 감지
- 테이블 선택 데이터 (selectedRowsData) 자동 주입
- 폼 입력 데이터 (formData) 자동 주입
- TableSource 노드가 외부 데이터 우선 사용
- 버튼 컴포넌트 통합
- 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원
- 노드 플로우 선택 UI 추가
- API 클라이언트 통합 (Axios)
- 개발 문서 작성
- 노드 기반 제어 시스템 개선 계획
- 노드 연결 규칙 설계
- 노드 실행 엔진 설계
- 노드 구조 개선안
- 버튼 통합 분석
2025-10-02 16:22:29 +09:00
|
|
|
const newNode: any = {
|
|
|
|
|
id: `node_${Date.now()}`,
|
|
|
|
|
type,
|
|
|
|
|
position,
|
2025-10-02 17:51:15 +09:00
|
|
|
data: defaultData,
|
feat: 노드 기반 데이터 플로우 시스템 구현
- 노드 에디터 UI 구현 (React Flow 기반)
- TableSource, DataTransform, INSERT, UPDATE, DELETE, UPSERT 노드
- 드래그앤드롭 노드 추가 및 연결
- 속성 패널을 통한 노드 설정
- 실시간 필드 라벨 표시 (column_labels 테이블 연동)
- 데이터 변환 노드 (DataTransform) 기능
- EXPLODE: 구분자로 1개 행 → 여러 행 확장
- UPPERCASE, LOWERCASE, TRIM, CONCAT, SPLIT, REPLACE 등 12가지 변환 타입
- In-place 변환 지원 (타겟 필드 생략 시 소스 필드 덮어쓰기)
- 변환된 필드가 하위 액션 노드에 자동 전달
- 노드 플로우 실행 엔진
- 위상 정렬을 통한 노드 실행 순서 결정
- 레벨별 병렬 실행 (Promise.allSettled)
- 부분 실패 허용 (한 노드 실패 시 연결된 하위 노드만 스킵)
- 트랜잭션 기반 안전한 데이터 처리
- UPSERT 액션 로직 구현
- DB 제약 조건 없이 SELECT → UPDATE or INSERT 방식
- 복합 충돌 키 지원 (예: sales_no + product_name)
- 파라미터 인덱스 정확한 매핑
- 데이터 소스 자동 감지
- 테이블 선택 데이터 (selectedRowsData) 자동 주입
- 폼 입력 데이터 (formData) 자동 주입
- TableSource 노드가 외부 데이터 우선 사용
- 버튼 컴포넌트 통합
- 기존 관계 실행 + 새 노드 플로우 실행 하이브리드 지원
- 노드 플로우 선택 UI 추가
- API 클라이언트 통합 (Axios)
- 개발 문서 작성
- 노드 기반 제어 시스템 개선 계획
- 노드 연결 규칙 설계
- 노드 실행 엔진 설계
- 노드 구조 개선안
- 버튼 통합 분석
2025-10-02 16:22:29 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
addNode(newNode);
|
|
|
|
|
},
|
|
|
|
|
[screenToFlowPosition, addNode],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full w-full">
|
|
|
|
|
{/* 좌측 노드 팔레트 */}
|
|
|
|
|
<div className="w-[250px] border-r bg-white">
|
|
|
|
|
<NodePalette />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 중앙 캔버스 */}
|
|
|
|
|
<div className="relative flex-1" ref={reactFlowWrapper} onKeyDown={onKeyDown} tabIndex={0}>
|
|
|
|
|
<ReactFlow
|
|
|
|
|
nodes={nodes}
|
|
|
|
|
edges={edges}
|
|
|
|
|
onNodesChange={onNodesChange}
|
|
|
|
|
onEdgesChange={onEdgesChange}
|
|
|
|
|
onConnect={onConnect}
|
|
|
|
|
onSelectionChange={onSelectionChange}
|
|
|
|
|
onDragOver={onDragOver}
|
|
|
|
|
onDrop={onDrop}
|
|
|
|
|
nodeTypes={nodeTypes}
|
|
|
|
|
fitView
|
|
|
|
|
className="bg-gray-50"
|
|
|
|
|
deleteKeyCode={["Delete", "Backspace"]}
|
|
|
|
|
>
|
|
|
|
|
{/* 배경 그리드 */}
|
|
|
|
|
<Background gap={16} size={1} color="#E5E7EB" />
|
|
|
|
|
|
|
|
|
|
{/* 컨트롤 버튼 */}
|
|
|
|
|
<Controls className="bg-white shadow-md" />
|
|
|
|
|
|
|
|
|
|
{/* 미니맵 */}
|
|
|
|
|
<MiniMap
|
|
|
|
|
className="bg-white shadow-md"
|
|
|
|
|
nodeColor={(node) => {
|
|
|
|
|
// 노드 타입별 색상 (추후 구현)
|
|
|
|
|
return "#3B82F6";
|
|
|
|
|
}}
|
|
|
|
|
maskColor="rgba(0, 0, 0, 0.1)"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 상단 툴바 */}
|
|
|
|
|
<Panel position="top-center" className="pointer-events-auto">
|
|
|
|
|
<FlowToolbar />
|
|
|
|
|
</Panel>
|
|
|
|
|
</ReactFlow>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 우측 속성 패널 */}
|
|
|
|
|
{showPropertiesPanel && (
|
|
|
|
|
<div className="w-[350px] border-l bg-white">
|
|
|
|
|
<PropertiesPanel />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FlowEditor 메인 컴포넌트 (Provider로 감싸기)
|
|
|
|
|
*/
|
|
|
|
|
export function FlowEditor() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-[calc(100vh-200px)] min-h-[700px] w-full">
|
|
|
|
|
<ReactFlowProvider>
|
|
|
|
|
<FlowEditorInner />
|
|
|
|
|
</ReactFlowProvider>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|