ERP-node/frontend/components/dataflow/connection/redesigned/RightPanel/DataflowVisualization.tsx

342 lines
14 KiB
TypeScript
Raw Normal View History

2025-10-02 11:12:45 +09:00
"use client";
import React from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Database, Filter, Zap, CheckCircle, XCircle, Edit } from "lucide-react";
import { DataConnectionState } from "../types/redesigned";
interface DataflowVisualizationProps {
state: Partial<DataConnectionState> & {
dataflowActions?: Array<{
actionType: string;
targetTable?: string;
name?: string;
fieldMappings?: any[];
}>;
};
onEdit: (step: "source" | "conditions" | "actions") => void;
}
/**
* 🎯 Sankey
*/
export const DataflowVisualization: React.FC<DataflowVisualizationProps> = ({ state, onEdit }) => {
const { fromTable, toTable, controlConditions = [], dataflowActions = [], fromColumns = [], toColumns = [] } = state;
// 상태 계산
const hasSource = !!fromTable;
const hasConditions = controlConditions.length > 0;
const hasActions = dataflowActions.length > 0;
const isComplete = hasSource && hasActions;
// 필드명을 라벨명으로 변환하는 함수
const getFieldLabel = (fieldName: string) => {
// fromColumns와 toColumns에서 해당 필드 찾기
const allColumns = [...fromColumns, ...toColumns];
const column = allColumns.find((col) => col.columnName === fieldName);
return column?.displayName || column?.labelKo || fieldName;
};
// 테이블명을 라벨명으로 변환하는 함수
const getTableLabel = (tableName: string) => {
// fromTable 또는 toTable의 라벨 반환
if (fromTable?.tableName === tableName) {
return fromTable?.tableLabel || fromTable?.displayName || tableName;
}
if (toTable?.tableName === tableName) {
return toTable?.tableLabel || toTable?.displayName || tableName;
}
return tableName;
};
// 액션 그룹별로 대표 액션 1개씩만 표시
const actionGroups = dataflowActions.reduce(
(acc, action, index) => {
// 각 액션을 개별 그룹으로 처리 (실제로는 actionGroups에서 온 것)
const groupKey = `group_${index}`;
acc[groupKey] = [action];
return acc;
},
{} as Record<string, typeof dataflowActions>,
);
return (
<div className="space-y-4 p-4 sm:space-y-6 sm:p-6">
2025-10-02 11:12:45 +09:00
{/* 헤더 */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="text-muted-foreground text-xs sm:text-sm"> </p>
<Badge variant={isComplete ? "default" : "secondary"} className="w-fit text-xs sm:text-sm">
2025-10-02 11:12:45 +09:00
{isComplete ? "✅ 설정 완료" : "⚠️ 설정 필요"}
</Badge>
</div>
{/* Sankey 다이어그램 */}
<div className="relative flex flex-col items-center justify-center gap-6 py-8 sm:flex-row sm:gap-0 sm:py-12">
2025-10-02 11:12:45 +09:00
{/* 연결선 레이어 */}
<svg className="absolute inset-0 h-full w-full" style={{ zIndex: 0 }}>
{/* 소스 → 조건 선 */}
{hasSource && <line x1="25%" y1="50%" x2="50%" y2="50%" stroke="#60a5fa" strokeWidth="3" />}
{/* 조건 → 액션들 선 (여러 개) */}
{hasConditions &&
hasActions &&
Object.keys(actionGroups).map((groupKey, index) => {
const totalActions = Object.keys(actionGroups).length;
const startY = 50; // 조건 노드 중앙
// 액션이 여러 개면 위에서 아래로 분산
const endY =
totalActions > 1
? 30 + (index * 40) / (totalActions - 1) // 30%~70% 사이에 분산
: 50; // 액션이 1개면 중앙
return (
<line
key={groupKey}
x1="50%"
y1={`${startY}%`}
x2="75%"
y2={`${endY}%`}
stroke="#34d399"
strokeWidth="2"
opacity="0.7"
/>
);
})}
</svg>
<div
className="relative flex w-full flex-col items-center justify-around gap-4 sm:flex-row sm:gap-0"
style={{ zIndex: 1 }}
>
2025-10-02 11:12:45 +09:00
{/* 1. 소스 노드 */}
<div className="flex w-full flex-col items-center sm:w-[28%]">
2025-10-02 11:12:45 +09:00
<Card
className={`w-full cursor-pointer border-2 transition-all hover:shadow-lg ${
hasSource ? "border-primary bg-primary/10" : "border-border bg-muted"
2025-10-02 11:12:45 +09:00
}`}
onClick={() => onEdit("source")}
>
<CardContent>
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="mb-2 flex items-center gap-2">
<Database className="text-primary h-5 w-5" />
<span className="text-foreground text-sm font-semibold"> </span>
2025-10-02 11:12:45 +09:00
</div>
{hasSource ? (
<div className="space-y-1">
<p className="text-foreground text-sm font-medium">
{fromTable.tableLabel || fromTable.displayName || fromTable.tableName}
</p>
{(fromTable.tableLabel || fromTable.displayName) && (
<p className="text-muted-foreground text-xs">({fromTable.tableName})</p>
)}
</div>
) : (
<p className="text-muted-foreground text-xs"></p>
)}
</div>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
<Edit className="h-3 w-3" />
</Button>
2025-10-02 11:12:45 +09:00
</div>
</div>
2025-10-02 11:12:45 +09:00
</CardContent>
</Card>
</div>
{/* 2. 조건 노드 (중앙) */}
<div className="flex w-full flex-col items-center sm:w-[28%]">
2025-10-02 11:12:45 +09:00
<Card
className={`w-full cursor-pointer border-2 transition-all hover:shadow-lg ${
hasConditions ? "border-warning bg-warning/10" : "border-border bg-muted"
2025-10-02 11:12:45 +09:00
}`}
onClick={() => onEdit("conditions")}
>
<CardContent>
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="mb-2 flex items-center gap-2">
<Filter className="text-warning h-5 w-5" />
<span className="text-foreground text-sm font-semibold"> </span>
</div>
{hasConditions ? (
<div className="space-y-2">
{/* 실제 조건들 표시 */}
<div className="max-h-32 space-y-1 overflow-y-auto">
{controlConditions.slice(0, 3).map((condition, index) => (
<div key={index} className="bg-background/50 rounded px-2 py-1">
<p className="text-foreground text-xs font-medium">
{index > 0 && (
<span className="text-primary mr-1 font-bold">
{condition.logicalOperator || "AND"}
</span>
)}
<span className="text-primary">{getFieldLabel(condition.field)}</span>{" "}
<span className="text-muted-foreground">{condition.operator}</span>{" "}
<span className="text-success">{condition.value}</span>
</p>
</div>
))}
{controlConditions.length > 3 && (
<p className="text-muted-foreground px-2 text-xs">
{controlConditions.length - 3}...
2025-10-02 11:12:45 +09:00
</p>
)}
2025-10-02 11:12:45 +09:00
</div>
<div className="mt-2 flex items-center justify-between border-t pt-2">
<div className="flex items-center gap-1">
<CheckCircle className="text-success h-3 w-3" />
<span className="text-muted-foreground text-xs"> </span>
</div>
<div className="flex items-center gap-1">
<XCircle className="text-destructive h-3 w-3" />
<span className="text-muted-foreground text-xs"> </span>
</div>
2025-10-02 11:12:45 +09:00
</div>
</div>
) : (
<p className="text-muted-foreground text-xs"> ( )</p>
)}
</div>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
<Edit className="h-3 w-3" />
</Button>
2025-10-02 11:12:45 +09:00
</div>
</div>
2025-10-02 11:12:45 +09:00
</CardContent>
</Card>
</div>
{/* 3. 액션 노드들 (우측) */}
<div className="flex w-full flex-col items-center gap-3 sm:w-[28%]">
2025-10-02 11:12:45 +09:00
<Button
variant="ghost"
size="sm"
onClick={() => onEdit("actions")}
className="mb-2 flex items-center gap-2 self-end"
>
<Edit className="h-3 w-3" />
<span className="text-xs"> </span>
</Button>
{hasActions ? (
<div className="w-full space-y-3">
{Object.entries(actionGroups).map(([groupKey, actions]) => (
<ActionFlowCard
key={groupKey}
type={actions[0].actionType}
actions={actions}
getTableLabel={getTableLabel}
/>
))}
</div>
) : (
<Card className="border-border bg-muted w-full border-2 border-dashed">
<CardContent>
<div className="p-4 text-center">
<Zap className="text-muted-foreground mx-auto mb-2 h-6 w-6" />
<p className="text-muted-foreground text-xs"> </p>
</div>
2025-10-02 11:12:45 +09:00
</CardContent>
</Card>
)}
</div>
</div>
{/* 조건 불만족 시 중단 표시 (하단) */}
{hasConditions && (
<div
className="border-destructive/30 bg-destructive/10 absolute bottom-0 flex items-center gap-2 rounded-lg border-2 px-3 py-2"
2025-10-02 11:12:45 +09:00
style={{ left: "50%", transform: "translateX(-50%)" }}
>
<XCircle className="text-destructive h-4 w-4" />
<span className="text-destructive text-xs font-medium"> </span>
2025-10-02 11:12:45 +09:00
</div>
)}
</div>
{/* 통계 요약 */}
<Card className="border-border from-muted to-muted/50 bg-gradient-to-r">
<CardContent>
<div className="p-4">
<div className="flex flex-col items-center justify-around gap-4 text-center sm:flex-row sm:gap-0">
<div>
<p className="text-muted-foreground text-xs"></p>
<p className="text-primary text-base font-bold sm:text-lg">{hasSource ? 1 : 0}</p>
</div>
<div className="bg-border hidden h-8 w-px sm:block"></div>
<div>
<p className="text-muted-foreground text-xs"></p>
<p className="text-warning text-base font-bold sm:text-lg">{controlConditions.length}</p>
</div>
<div className="bg-border hidden h-8 w-px sm:block"></div>
<div>
<p className="text-muted-foreground text-xs"></p>
<p className="text-success text-base font-bold sm:text-lg">{dataflowActions.length}</p>
</div>
2025-10-02 11:12:45 +09:00
</div>
</div>
</CardContent>
</Card>
</div>
);
};
// 액션 플로우 카드 컴포넌트
interface ActionFlowCardProps {
type: string;
actions: Array<{
actionType: string;
targetTable?: string;
name?: string;
fieldMappings?: any[];
}>;
getTableLabel: (tableName: string) => string;
}
const ActionFlowCard: React.FC<ActionFlowCardProps> = ({ type, actions, getTableLabel }) => {
const actionColors = {
insert: { bg: "bg-primary/10", border: "border-primary/30", text: "text-primary", icon: "text-primary" },
update: { bg: "bg-success/10", border: "border-success/30", text: "text-success", icon: "text-success" },
delete: {
bg: "bg-destructive/10",
border: "border-destructive/30",
text: "text-destructive",
icon: "text-destructive",
},
upsert: { bg: "bg-primary/10", border: "border-primary/30", text: "text-primary", icon: "text-primary" },
2025-10-02 11:12:45 +09:00
};
const colors = actionColors[type as keyof typeof actionColors] || actionColors.insert;
const action = actions[0]; // 그룹당 1개만 표시
const displayName = action.targetTable ? getTableLabel(action.targetTable) : action.name || "액션";
const isTableLabel = action.targetTable && getTableLabel(action.targetTable) !== action.targetTable;
return (
<Card className={`border-2 ${colors.border} ${colors.bg}`}>
<CardContent>
<div className="p-3">
<div className="mb-2 flex items-center gap-2">
<Zap className={`h-4 w-4 ${colors.icon}`} />
<span className={`text-sm font-semibold ${colors.text}`}>{type.toUpperCase()}</span>
</div>
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2 text-xs">
<Database className="text-muted-foreground h-3 w-3" />
<span className="text-foreground truncate font-medium">{displayName}</span>
</div>
{isTableLabel && action.targetTable && (
<span className="text-muted-foreground ml-5 truncate text-xs">({action.targetTable})</span>
)}
2025-10-02 11:12:45 +09:00
</div>
</div>
2025-10-02 11:12:45 +09:00
</CardContent>
</Card>
);
};