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) - 개발 문서 작성 - 노드 기반 제어 시스템 개선 계획 - 노드 연결 규칙 설계 - 노드 실행 엔진 설계 - 노드 구조 개선안 - 버튼 통합 분석
This commit is contained in:
@@ -8,36 +8,20 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Settings,
|
||||
GitBranch,
|
||||
Clock,
|
||||
Zap,
|
||||
Info
|
||||
} from "lucide-react";
|
||||
import { ComponentData, ButtonDataflowConfig } from "@/types/screen";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { Settings, Clock, Zap, Info, Workflow } from "lucide-react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { getNodeFlows, NodeFlow } from "@/lib/api/nodeFlows";
|
||||
|
||||
interface ImprovedButtonControlConfigPanelProps {
|
||||
component: ComponentData;
|
||||
onUpdateProperty: (path: string, value: any) => void;
|
||||
}
|
||||
|
||||
interface RelationshipOption {
|
||||
id: string;
|
||||
name: string;
|
||||
sourceTable: string;
|
||||
targetTable: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 단순화된 버튼 제어 설정 패널
|
||||
*
|
||||
* 관계 실행만 지원:
|
||||
* - 관계 선택 및 실행 타이밍 설정
|
||||
* - 관계 내부에 데이터 저장/외부호출 로직 포함
|
||||
*
|
||||
* 노드 플로우 실행만 지원:
|
||||
* - 플로우 선택 및 실행 타이밍 설정
|
||||
*/
|
||||
export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlConfigPanelProps> = ({
|
||||
component,
|
||||
@@ -47,57 +31,45 @@ export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlCon
|
||||
const dataflowConfig = config.dataflowConfig || {};
|
||||
|
||||
// 🔥 State 관리
|
||||
const [relationships, setRelationships] = useState<RelationshipOption[]>([]);
|
||||
const [flows, setFlows] = useState<NodeFlow[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 🔥 관계 목록 로딩
|
||||
// 🔥 플로우 목록 로딩
|
||||
useEffect(() => {
|
||||
if (config.enableDataflowControl) {
|
||||
loadRelationships();
|
||||
loadFlows();
|
||||
}
|
||||
}, [config.enableDataflowControl]);
|
||||
|
||||
/**
|
||||
* 🔥 전체 관계 목록 로드 (관계도별 구분 없이)
|
||||
* 🔥 플로우 목록 로드
|
||||
*/
|
||||
const loadRelationships = async () => {
|
||||
const loadFlows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log("🔍 전체 관계 목록 로딩...");
|
||||
console.log("🔍 플로우 목록 로딩...");
|
||||
|
||||
const response = await apiClient.get("/test-button-dataflow/relationships/all");
|
||||
|
||||
if (response.data.success && Array.isArray(response.data.data)) {
|
||||
const relationshipList = response.data.data.map((rel: any) => ({
|
||||
id: rel.id,
|
||||
name: rel.name || `${rel.sourceTable} → ${rel.targetTable}`,
|
||||
sourceTable: rel.sourceTable,
|
||||
targetTable: rel.targetTable,
|
||||
category: rel.category || "데이터 흐름",
|
||||
}));
|
||||
|
||||
setRelationships(relationshipList);
|
||||
console.log(`✅ 관계 ${relationshipList.length}개 로딩 완료`);
|
||||
}
|
||||
const flowList = await getNodeFlows();
|
||||
setFlows(flowList);
|
||||
console.log(`✅ 플로우 ${flowList.length}개 로딩 완료`);
|
||||
} catch (error) {
|
||||
console.error("❌ 관계 목록 로딩 실패:", error);
|
||||
setRelationships([]);
|
||||
console.error("❌ 플로우 목록 로딩 실패:", error);
|
||||
setFlows([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 관계 선택 핸들러
|
||||
* 🔥 플로우 선택 핸들러
|
||||
*/
|
||||
const handleRelationshipSelect = (relationshipId: string) => {
|
||||
const selectedRelationship = relationships.find(r => r.id === relationshipId);
|
||||
if (selectedRelationship) {
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.relationshipConfig", {
|
||||
relationshipId: selectedRelationship.id,
|
||||
relationshipName: selectedRelationship.name,
|
||||
executionTiming: "after", // 기본값
|
||||
const handleFlowSelect = (flowId: string) => {
|
||||
const selectedFlow = flows.find((f) => f.flowId.toString() === flowId);
|
||||
if (selectedFlow) {
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.flowConfig", {
|
||||
flowId: selectedFlow.flowId,
|
||||
flowName: selectedFlow.flowName,
|
||||
executionTiming: "before", // 기본값
|
||||
contextData: {},
|
||||
});
|
||||
}
|
||||
@@ -110,7 +82,7 @@ export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlCon
|
||||
// 기존 설정 초기화
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig", {
|
||||
controlMode: controlType,
|
||||
relationshipConfig: controlType === "relationship" ? undefined : null,
|
||||
flowConfig: controlType === "flow" ? undefined : null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -138,46 +110,45 @@ export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlCon
|
||||
<CardTitle>버튼 제어 설정</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs
|
||||
value={dataflowConfig.controlMode || "none"}
|
||||
onValueChange={handleControlTypeChange}
|
||||
>
|
||||
<Tabs value={dataflowConfig.controlMode || "none"} onValueChange={handleControlTypeChange}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="none">제어 없음</TabsTrigger>
|
||||
<TabsTrigger value="relationship">관계 실행</TabsTrigger>
|
||||
<TabsTrigger value="flow">노드 플로우</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="none" className="mt-4">
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Zap className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
<Zap className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
||||
<p>추가 제어 없이 기본 버튼 액션만 실행됩니다.</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="relationship" className="mt-4">
|
||||
<RelationshipSelector
|
||||
relationships={relationships}
|
||||
selectedRelationshipId={dataflowConfig.relationshipConfig?.relationshipId}
|
||||
onSelect={handleRelationshipSelect}
|
||||
<TabsContent value="flow" className="mt-4">
|
||||
<FlowSelector
|
||||
flows={flows}
|
||||
selectedFlowId={dataflowConfig.flowConfig?.flowId}
|
||||
onSelect={handleFlowSelect}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
{dataflowConfig.relationshipConfig && (
|
||||
|
||||
{dataflowConfig.flowConfig && (
|
||||
<div className="mt-4 space-y-4">
|
||||
<Separator />
|
||||
<ExecutionTimingSelector
|
||||
value={dataflowConfig.relationshipConfig.executionTiming}
|
||||
onChange={(timing) =>
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.relationshipConfig.executionTiming", timing)
|
||||
value={dataflowConfig.flowConfig.executionTiming}
|
||||
onChange={(timing) =>
|
||||
onUpdateProperty("webTypeConfig.dataflowConfig.flowConfig.executionTiming", timing)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="rounded bg-blue-50 p-3">
|
||||
|
||||
<div className="rounded bg-green-50 p-3">
|
||||
<div className="flex items-start space-x-2">
|
||||
<Info className="h-4 w-4 text-blue-600 mt-0.5" />
|
||||
<div className="text-xs text-blue-800">
|
||||
<p className="font-medium">관계 실행 정보:</p>
|
||||
<p className="mt-1">선택한 관계에 설정된 데이터 저장, 외부호출 등의 모든 액션이 자동으로 실행됩니다.</p>
|
||||
<Info className="mt-0.5 h-4 w-4 text-green-600" />
|
||||
<div className="text-xs text-green-800">
|
||||
<p className="font-medium">노드 플로우 실행 정보:</p>
|
||||
<p className="mt-1">선택한 플로우의 모든 노드가 순차적/병렬로 실행됩니다.</p>
|
||||
<p className="mt-1">• 독립 트랜잭션: 각 액션은 독립적으로 커밋/롤백</p>
|
||||
<p>• 연쇄 중단: 부모 노드 실패 시 자식 노드 스킵</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,38 +164,41 @@ export const ImprovedButtonControlConfigPanel: React.FC<ImprovedButtonControlCon
|
||||
};
|
||||
|
||||
/**
|
||||
* 🔥 관계 선택 컴포넌트
|
||||
* 🔥 플로우 선택 컴포넌트
|
||||
*/
|
||||
const RelationshipSelector: React.FC<{
|
||||
relationships: RelationshipOption[];
|
||||
selectedRelationshipId?: string;
|
||||
onSelect: (relationshipId: string) => void;
|
||||
const FlowSelector: React.FC<{
|
||||
flows: NodeFlow[];
|
||||
selectedFlowId?: number;
|
||||
onSelect: (flowId: string) => void;
|
||||
loading: boolean;
|
||||
}> = ({ relationships, selectedRelationshipId, onSelect, loading }) => {
|
||||
}> = ({ flows, selectedFlowId, onSelect, loading }) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<GitBranch className="h-4 w-4 text-blue-600" />
|
||||
<Label>실행할 관계 선택</Label>
|
||||
<Workflow className="h-4 w-4 text-green-600" />
|
||||
<Label>실행할 노드 플로우 선택</Label>
|
||||
</div>
|
||||
|
||||
<Select value={selectedRelationshipId || ""} onValueChange={onSelect}>
|
||||
|
||||
<Select value={selectedFlowId?.toString() || ""} onValueChange={onSelect}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="관계를 선택하세요" />
|
||||
<SelectValue placeholder="플로우를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{loading ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">관계 목록을 불러오는 중...</div>
|
||||
) : relationships.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">사용 가능한 관계가 없습니다</div>
|
||||
<div className="p-4 text-center text-sm text-gray-500">플로우 목록을 불러오는 중...</div>
|
||||
) : flows.length === 0 ? (
|
||||
<div className="p-4 text-center text-sm text-gray-500">
|
||||
<p>사용 가능한 플로우가 없습니다</p>
|
||||
<p className="mt-2 text-xs">노드 편집기에서 플로우를 먼저 생성하세요</p>
|
||||
</div>
|
||||
) : (
|
||||
relationships.map((rel) => (
|
||||
<SelectItem key={rel.id} value={rel.id}>
|
||||
flows.map((flow) => (
|
||||
<SelectItem key={flow.flowId} value={flow.flowId.toString()}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{rel.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{rel.sourceTable} → {rel.targetTable}
|
||||
</span>
|
||||
<span className="font-medium">{flow.flowName}</span>
|
||||
{flow.flowDescription && (
|
||||
<span className="text-muted-foreground text-xs">{flow.flowDescription}</span>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
@@ -235,7 +209,6 @@ const RelationshipSelector: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 🔥 실행 타이밍 선택 컴포넌트
|
||||
*/
|
||||
@@ -249,7 +222,7 @@ const ExecutionTimingSelector: React.FC<{
|
||||
<Clock className="h-4 w-4 text-orange-600" />
|
||||
<Label>실행 타이밍</Label>
|
||||
</div>
|
||||
|
||||
|
||||
<Select value={value} onValueChange={onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="실행 타이밍을 선택하세요" />
|
||||
@@ -258,19 +231,19 @@ const ExecutionTimingSelector: React.FC<{
|
||||
<SelectItem value="before">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Before (사전 실행)</span>
|
||||
<span className="text-xs text-muted-foreground">버튼 액션 실행 전에 제어를 실행합니다</span>
|
||||
<span className="text-muted-foreground text-xs">버튼 액션 실행 전에 플로우를 실행합니다</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="after">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">After (사후 실행)</span>
|
||||
<span className="text-xs text-muted-foreground">버튼 액션 실행 후에 제어를 실행합니다</span>
|
||||
<span className="text-muted-foreground text-xs">버튼 액션 실행 후에 플로우를 실행합니다</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="replace">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">Replace (대체 실행)</span>
|
||||
<span className="text-xs text-muted-foreground">버튼 액션 대신 제어만 실행합니다</span>
|
||||
<span className="text-muted-foreground text-xs">버튼 액션 대신 플로우만 실행합니다</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -278,4 +251,3 @@ const ExecutionTimingSelector: React.FC<{
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user