166 lines
6.4 KiB
TypeScript
166 lines
6.4 KiB
TypeScript
"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",
|
|
"/": "/",
|
|
"%": "%",
|
|
};
|
|
|
|
// 피연산자를 문자열로 변환
|
|
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 || "?";
|
|
}
|
|
|
|
// 수식 요약 생성
|
|
function getFormulaSummary(transformation: FormulaTransformNodeData["transformations"][0]): string {
|
|
const { formulaType, arithmetic, function: func, condition, staticValue } = transformation;
|
|
|
|
switch (formulaType) {
|
|
case "arithmetic": {
|
|
if (!arithmetic) return "미설정";
|
|
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;
|
|
}
|
|
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";
|