ERP-node/frontend/components/dataflow/node-editor/nodes/FormulaTransformNode.tsx

166 lines
6.4 KiB
TypeScript
Raw Normal View History

2025-12-10 18:28:27 +09:00
"use client";
/**
* (Formula Transform Node)
* , , .
* UPSERT .
*/
import { memo } from "react";
import { Handle, Position, NodeProps } from "reactflow";
import { Calculator, Database, ArrowRight } from "lucide-react";
import type { FormulaTransformNodeData, FormulaType } from "@/types/node-editor";
// 수식 타입별 라벨
const FORMULA_TYPE_LABELS: Record<FormulaType, { label: string; color: string }> = {
arithmetic: { label: "산술", color: "bg-orange-500" },
function: { label: "함수", color: "bg-blue-500" },
condition: { label: "조건", color: "bg-yellow-500" },
static: { label: "정적", color: "bg-gray-500" },
};
// 연산자 표시
const OPERATOR_LABELS: Record<string, string> = {
"+": "+",
"-": "-",
"*": "x",
"/": "/",
"%": "%",
};
2026-01-13 09:30:19 +09:00
// 피연산자를 문자열로 변환
function getOperandStr(operand: any): string {
if (!operand) return "?";
if (operand.type === "static") return String(operand.value || "?");
if (operand.fieldLabel) return operand.fieldLabel;
return operand.field || operand.resultField || "?";
}
2025-12-10 18:28:27 +09:00
// 수식 요약 생성
function getFormulaSummary(transformation: FormulaTransformNodeData["transformations"][0]): string {
const { formulaType, arithmetic, function: func, condition, staticValue } = transformation;
switch (formulaType) {
case "arithmetic": {
if (!arithmetic) return "미설정";
2026-01-13 09:30:19 +09:00
const leftStr = getOperandStr(arithmetic.leftOperand);
const rightStr = getOperandStr(arithmetic.rightOperand);
let formula = `${leftStr} ${OPERATOR_LABELS[arithmetic.operator]} ${rightStr}`;
// 추가 연산 표시
if (arithmetic.additionalOperations && arithmetic.additionalOperations.length > 0) {
for (const addOp of arithmetic.additionalOperations) {
const opStr = getOperandStr(addOp.operand);
formula += ` ${OPERATOR_LABELS[addOp.operator] || addOp.operator} ${opStr}`;
}
}
return formula;
2025-12-10 18:28:27 +09:00
}
case "function": {
if (!func) return "미설정";
const args = func.arguments
.map((arg) => (arg.type === "static" ? arg.value : `${arg.type}.${arg.field || arg.resultField}`))
.join(", ");
return `${func.name}(${args})`;
}
case "condition": {
if (!condition) return "미설정";
return "CASE WHEN ... THEN ... ELSE ...";
}
case "static": {
return staticValue !== undefined ? String(staticValue) : "미설정";
}
default:
return "미설정";
}
}
export const FormulaTransformNode = memo(({ data, selected }: NodeProps<FormulaTransformNodeData>) => {
const transformationCount = data.transformations?.length || 0;
const hasTargetLookup = !!data.targetLookup?.tableName;
return (
<div
className={`min-w-[300px] rounded-lg border-2 bg-white shadow-md transition-all ${
selected ? "border-orange-500 shadow-lg" : "border-gray-200"
}`}
>
{/* 헤더 */}
<div className="flex items-center gap-2 rounded-t-lg bg-orange-500 px-3 py-2 text-white">
<Calculator className="h-4 w-4" />
<div className="flex-1">
<div className="text-sm font-semibold">{data.displayName || "수식 변환"}</div>
<div className="text-xs opacity-80">
{transformationCount} {hasTargetLookup && "| 타겟 조회"}
</div>
</div>
</div>
{/* 본문 */}
<div className="space-y-3 p-3">
{/* 타겟 테이블 조회 설정 */}
{hasTargetLookup && (
<div className="rounded bg-blue-50 p-2">
<div className="mb-1 flex items-center gap-1">
<Database className="h-3 w-3 text-blue-600" />
<span className="text-xs font-medium text-blue-700"> </span>
</div>
<div className="text-xs text-blue-600">{data.targetLookup?.tableLabel || data.targetLookup?.tableName}</div>
{data.targetLookup?.lookupKeys && data.targetLookup.lookupKeys.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{data.targetLookup.lookupKeys.slice(0, 2).map((key, idx) => (
<span
key={idx}
className="inline-flex items-center gap-1 rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700"
>
{key.sourceFieldLabel || key.sourceField}
<ArrowRight className="h-2 w-2" />
{key.targetFieldLabel || key.targetField}
</span>
))}
{data.targetLookup.lookupKeys.length > 2 && (
<span className="text-xs text-blue-500">+{data.targetLookup.lookupKeys.length - 2}</span>
)}
</div>
)}
</div>
)}
{/* 변환 규칙들 */}
{transformationCount > 0 ? (
<div className="space-y-2">
{data.transformations.slice(0, 4).map((trans, idx) => {
const typeInfo = FORMULA_TYPE_LABELS[trans.formulaType];
return (
<div key={trans.id || idx} className="rounded bg-gray-50 p-2">
<div className="flex items-center justify-between">
<span className={`rounded px-1.5 py-0.5 text-xs font-medium text-white ${typeInfo.color}`}>
{typeInfo.label}
</span>
<span className="text-xs font-medium text-gray-700">
{trans.outputFieldLabel || trans.outputField}
</span>
</div>
<div className="mt-1 truncate font-mono text-xs text-gray-500">{getFormulaSummary(trans)}</div>
</div>
);
})}
{data.transformations.length > 4 && (
<div className="text-center text-xs text-gray-400">... {data.transformations.length - 4}</div>
)}
</div>
) : (
<div className="py-4 text-center text-xs text-gray-400"> </div>
)}
</div>
{/* 핸들 */}
<Handle type="target" position={Position.Left} className="!h-3 !w-3 !bg-orange-500" />
<Handle type="source" position={Position.Right} className="!h-3 !w-3 !bg-orange-500" />
</div>
);
});
FormulaTransformNode.displayName = "FormulaTransformNode";