ERP-node/frontend/components/flow/FlowNodeComponent.tsx

105 lines
3.4 KiB
TypeScript
Raw Normal View History

2025-10-20 10:55:33 +09:00
/**
*
* React Flow
*/
import { memo } from "react";
import { Handle, Position, NodeProps } from "reactflow";
import { FlowNodeData, FlowCondition, ConditionOperator } from "@/types/flow";
import { Badge } from "@/components/ui/badge";
// 조건을 자연어로 변환하는 헬퍼 함수
const formatCondition = (cond: FlowCondition): string => {
const operatorLabels: Record<ConditionOperator, string> = {
equals: "=",
not_equals: "≠",
greater_than: ">",
less_than: "<",
greater_than_or_equal: "≥",
less_than_or_equal: "≤",
in: "IN",
not_in: "NOT IN",
like: "LIKE",
not_like: "NOT LIKE",
is_null: "IS NULL",
is_not_null: "IS NOT NULL",
};
const operatorLabel = operatorLabels[cond.operator] || cond.operator;
if (cond.operator === "is_null" || cond.operator === "is_not_null") {
return `${cond.column} ${operatorLabel}`;
}
return `${cond.column} ${operatorLabel} "${cond.value || ""}"`;
};
const formatAllConditions = (data: FlowNodeData): string => {
if (!data.condition || data.condition.conditions.length === 0) {
return "조건 없음";
}
const conditions = data.condition.conditions;
const type = data.condition.type;
// 조건이 많으면 간략하게 표시
if (conditions.length > 2) {
return `${conditions.length}개 조건 (${type})`;
}
const connector = type === "AND" ? " AND " : " OR ";
return conditions.map(formatCondition).join(connector);
};
export const FlowNodeComponent = memo(({ data }: NodeProps<FlowNodeData>) => {
return (
<div className="bg-card min-w-[200px] rounded-lg border px-4 py-3 shadow-sm transition-shadow hover:shadow-md">
{/* 입력 핸들 */}
<Handle type="target" position={Position.Left} className="border-primary bg-background h-3 w-3 border-2" />
{/* 노드 내용 */}
<div>
<div className="mb-2 flex items-center justify-between gap-2">
<Badge variant="outline" className="text-xs">
{data.stepOrder}
</Badge>
</div>
<div className="text-foreground mb-2 text-sm font-semibold">{data.label}</div>
{/* 테이블 정보 */}
{data.tableName && (
<div className="bg-muted text-muted-foreground mb-2 flex items-center gap-1 rounded-md px-2 py-1 text-xs">
<span>📊</span>
<span className="truncate">{data.tableName}</span>
</div>
)}
{/* 데이터 건수 */}
{data.count !== undefined && (
<Badge variant="secondary" className="mb-2 text-xs">
{data.count}
</Badge>
)}
{/* 조건 미리보기 */}
{data.condition && data.condition.conditions.length > 0 ? (
<div className="mt-2">
<div className="text-muted-foreground mb-1 text-xs font-medium">:</div>
<div className="text-muted-foreground text-xs break-words" style={{ lineHeight: "1.4" }}>
{formatAllConditions(data)}
</div>
</div>
) : (
<div className="text-muted-foreground mt-2 text-xs"> </div>
)}
</div>
{/* 출력 핸들 */}
<Handle type="source" position={Position.Right} className="border-primary bg-background h-3 w-3 border-2" />
</div>
);
});
FlowNodeComponent.displayName = "FlowNodeComponent";