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 { useState } from "react";
|
|
|
|
|
import { Play, Save, FileCheck, Undo2, Redo2, ZoomIn, ZoomOut, FolderOpen, Download, Trash2 } from "lucide-react";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { useFlowEditorStore } from "@/lib/stores/flowEditorStore";
|
|
|
|
|
import { useReactFlow } from "reactflow";
|
|
|
|
|
import { LoadFlowDialog } from "./dialogs/LoadFlowDialog";
|
|
|
|
|
import { getNodeFlow } from "@/lib/api/nodeFlows";
|
|
|
|
|
|
|
|
|
|
export function FlowToolbar() {
|
|
|
|
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
|
|
|
|
const {
|
|
|
|
|
flowName,
|
|
|
|
|
setFlowName,
|
|
|
|
|
validateFlow,
|
|
|
|
|
saveFlow,
|
|
|
|
|
exportFlow,
|
|
|
|
|
isExecuting,
|
|
|
|
|
isSaving,
|
|
|
|
|
selectedNodes,
|
|
|
|
|
removeNodes,
|
2025-10-13 13:28:20 +09:00
|
|
|
undo,
|
|
|
|
|
redo,
|
|
|
|
|
canUndo,
|
|
|
|
|
canRedo,
|
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
|
|
|
} = useFlowEditorStore();
|
|
|
|
|
const [showLoadDialog, setShowLoadDialog] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleValidate = () => {
|
|
|
|
|
const result = validateFlow();
|
|
|
|
|
if (result.valid) {
|
|
|
|
|
alert("✅ 검증 성공! 오류가 없습니다.");
|
|
|
|
|
} else {
|
|
|
|
|
alert(`❌ 검증 실패\n\n${result.errors.map((e) => `- ${e.message}`).join("\n")}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
const result = await saveFlow();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
alert(`✅ ${result.message}\nFlow ID: ${result.flowId}`);
|
|
|
|
|
} else {
|
|
|
|
|
alert(`❌ 저장 실패\n\n${result.message}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleExport = () => {
|
|
|
|
|
const json = exportFlow();
|
|
|
|
|
const blob = new Blob([json], { type: "application/json" });
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `${flowName || "flow"}.json`;
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
alert("✅ JSON 파일로 내보내기 완료!");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleLoad = async (flowId: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const flow = await getNodeFlow(flowId);
|
|
|
|
|
|
|
|
|
|
// flowData가 이미 객체인지 문자열인지 확인
|
|
|
|
|
const parsedData = typeof flow.flowData === "string" ? JSON.parse(flow.flowData) : flow.flowData;
|
|
|
|
|
|
|
|
|
|
// Zustand 스토어의 loadFlow 함수 호출
|
|
|
|
|
useFlowEditorStore
|
|
|
|
|
.getState()
|
|
|
|
|
.loadFlow(flow.flowId, flow.flowName, flow.flowDescription, parsedData.nodes, parsedData.edges);
|
|
|
|
|
alert(`✅ "${flow.flowName}" 플로우를 불러왔습니다!`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("플로우 불러오기 오류:", error);
|
|
|
|
|
alert(error instanceof Error ? error.message : "플로우를 불러올 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleExecute = () => {
|
|
|
|
|
// TODO: 실행 로직 구현
|
|
|
|
|
alert("실행 기능 구현 예정");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDelete = () => {
|
|
|
|
|
if (selectedNodes.length === 0) {
|
|
|
|
|
alert("삭제할 노드를 선택해주세요.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (confirm(`선택된 ${selectedNodes.length}개 노드를 삭제하시겠습니까?`)) {
|
|
|
|
|
removeNodes(selectedNodes);
|
|
|
|
|
alert(`✅ ${selectedNodes.length}개 노드가 삭제되었습니다.`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<LoadFlowDialog open={showLoadDialog} onOpenChange={setShowLoadDialog} onLoad={handleLoad} />
|
|
|
|
|
<div className="flex items-center gap-2 rounded-lg border bg-white p-2 shadow-md">
|
|
|
|
|
{/* 플로우 이름 */}
|
|
|
|
|
<Input
|
|
|
|
|
value={flowName}
|
|
|
|
|
onChange={(e) => setFlowName(e.target.value)}
|
|
|
|
|
className="h-8 w-[200px] text-sm"
|
|
|
|
|
placeholder="플로우 이름"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
|
|
|
|
|
|
{/* 실행 취소/다시 실행 */}
|
2025-10-13 13:28:20 +09:00
|
|
|
<Button variant="ghost" size="sm" title="실행 취소 (Ctrl+Z)" disabled={!canUndo()} onClick={undo}>
|
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
|
|
|
<Undo2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
2025-10-13 13:28:20 +09:00
|
|
|
<Button variant="ghost" size="sm" title="다시 실행 (Ctrl+Y)" disabled={!canRedo()} onClick={redo}>
|
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
|
|
|
<Redo2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
|
|
|
|
|
|
{/* 삭제 버튼 */}
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
disabled={selectedNodes.length === 0}
|
|
|
|
|
title={selectedNodes.length > 0 ? `${selectedNodes.length}개 노드 삭제` : "삭제할 노드를 선택하세요"}
|
|
|
|
|
className="gap-1 text-red-600 hover:bg-red-50 hover:text-red-700 disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
{selectedNodes.length > 0 && <span className="text-xs">({selectedNodes.length})</span>}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
|
|
|
|
|
|
{/* 줌 컨트롤 */}
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => zoomIn()} title="확대">
|
|
|
|
|
<ZoomIn className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => zoomOut()} title="축소">
|
|
|
|
|
<ZoomOut className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={() => fitView()} title="전체 보기">
|
|
|
|
|
<span className="text-xs">전체</span>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
|
|
|
|
|
|
{/* 불러오기 */}
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => setShowLoadDialog(true)} className="gap-1">
|
|
|
|
|
<FolderOpen className="h-4 w-4" />
|
|
|
|
|
<span className="text-xs">불러오기</span>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* 저장 */}
|
|
|
|
|
<Button variant="outline" size="sm" onClick={handleSave} disabled={isSaving} className="gap-1">
|
|
|
|
|
<Save className="h-4 w-4" />
|
|
|
|
|
<span className="text-xs">{isSaving ? "저장 중..." : "저장"}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* 내보내기 */}
|
|
|
|
|
<Button variant="outline" size="sm" onClick={handleExport} className="gap-1">
|
|
|
|
|
<Download className="h-4 w-4" />
|
|
|
|
|
<span className="text-xs">JSON</span>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<div className="h-6 w-px bg-gray-200" />
|
|
|
|
|
|
|
|
|
|
{/* 검증 */}
|
|
|
|
|
<Button variant="outline" size="sm" onClick={handleValidate} className="gap-1">
|
|
|
|
|
<FileCheck className="h-4 w-4" />
|
|
|
|
|
<span className="text-xs">검증</span>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
{/* 테스트 실행 */}
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleExecute}
|
|
|
|
|
disabled={isExecuting}
|
|
|
|
|
className="gap-1 bg-green-600 hover:bg-green-700"
|
|
|
|
|
>
|
|
|
|
|
<Play className="h-4 w-4" />
|
|
|
|
|
<span className="text-xs">{isExecuting ? "실행 중..." : "테스트 실행"}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|