ERP-node/frontend/lib/registry/components/selected-items-detail-input/CalculationBuilder.tsx

430 lines
14 KiB
TypeScript
Raw Normal View History

2025-11-19 10:03:38 +09:00
"use client";
import React, { useState } from "react";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Plus, X, Calculator } from "lucide-react";
import { CalculationNode, CalculationStep, AdditionalFieldDefinition } from "./types";
interface CalculationBuilderProps {
steps: CalculationStep[];
availableFields: AdditionalFieldDefinition[];
onChange: (steps: CalculationStep[]) => void;
}
export const CalculationBuilder: React.FC<CalculationBuilderProps> = ({
steps,
availableFields,
onChange,
}) => {
const [previewValues, setPreviewValues] = useState<Record<string, number>>({});
// 새 단계 추가
const addStep = () => {
const newStep: CalculationStep = {
id: `step_${Date.now()}`,
label: `단계 ${steps.length + 1}`,
expression: {
type: "field",
fieldName: "",
},
};
onChange([...steps, newStep]);
};
// 단계 삭제
const removeStep = (stepId: string) => {
onChange(steps.filter((s) => s.id !== stepId));
};
// 단계 업데이트
const updateStep = (stepId: string, updates: Partial<CalculationStep>) => {
onChange(
steps.map((s) => (s.id === stepId ? { ...s, ...updates } : s))
);
};
// 간단한 표현식 렌더링
const renderSimpleExpression = (step: CalculationStep) => {
return (
<div className="space-y-2">
<div className="flex items-center gap-2">
{/* 왼쪽 항 */}
<Select
value={step.expression.type === "field" ? step.expression.fieldName || "" : step.expression.type}
onValueChange={(value) => {
if (value === "previous") {
updateStep(step.id, {
expression: { type: "previous" },
});
} else if (value === "constant") {
updateStep(step.id, {
expression: { type: "constant", value: 0 },
});
} else {
updateStep(step.id, {
expression: { type: "field", fieldName: value },
});
}
}}
>
<SelectTrigger className="h-8 flex-1 text-xs">
<SelectValue placeholder="항목 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="previous"> </SelectItem>
<SelectItem value="constant"></SelectItem>
{availableFields.map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
{step.expression.type === "constant" && (
<Input
type="number"
value={step.expression.value || 0}
onChange={(e) => {
updateStep(step.id, {
expression: {
...step.expression,
value: parseFloat(e.target.value) || 0,
},
});
}}
className="h-8 w-24 text-xs"
placeholder="값"
/>
)}
</div>
{/* 연산 추가 버튼 */}
{step.expression.type !== "operation" && (
<Button
variant="outline"
size="sm"
onClick={() => {
const currentExpression = step.expression;
updateStep(step.id, {
expression: {
type: "operation",
operator: "+",
left: currentExpression,
right: { type: "constant", value: 0 },
},
});
}}
className="h-7 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
)}
{/* 연산식 */}
{step.expression.type === "operation" && (
<div className="space-y-2 border-l-2 border-primary pl-3 ml-2">
{renderOperationExpression(step)}
</div>
)}
</div>
);
};
// 연산식 렌더링
const renderOperationExpression = (step: CalculationStep) => {
if (step.expression.type !== "operation") return null;
return (
<div className="flex items-center gap-2 flex-wrap">
{/* 왼쪽 항 */}
<div className="text-xs text-muted-foreground">
{renderNodeLabel(step.expression.left)}
</div>
{/* 연산자 */}
<Select
value={step.expression.operator || "+"}
onValueChange={(value) => {
updateStep(step.id, {
expression: {
...step.expression,
operator: value as any,
},
});
}}
>
<SelectTrigger className="h-7 w-16 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="+">+</SelectItem>
<SelectItem value="-">-</SelectItem>
<SelectItem value="*">×</SelectItem>
<SelectItem value="/">÷</SelectItem>
<SelectItem value="%">%</SelectItem>
</SelectContent>
</Select>
{/* 오른쪽 항 */}
<Select
value={
step.expression.right?.type === "field"
? step.expression.right.fieldName || ""
: step.expression.right?.type || ""
}
onValueChange={(value) => {
if (value === "constant") {
updateStep(step.id, {
expression: {
...step.expression,
right: { type: "constant", value: 0 },
},
});
} else {
updateStep(step.id, {
expression: {
...step.expression,
right: { type: "field", fieldName: value },
},
});
}
}}
>
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue placeholder="항목 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="constant"></SelectItem>
{availableFields.map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
{step.expression.right?.type === "constant" && (
<Input
type="number"
value={step.expression.right.value || 0}
onChange={(e) => {
updateStep(step.id, {
expression: {
...step.expression,
right: {
...step.expression.right!,
value: parseFloat(e.target.value) || 0,
},
},
});
}}
className="h-7 w-24 text-xs"
placeholder="값"
/>
)}
</div>
);
};
// 노드 라벨 표시
const renderNodeLabel = (node?: CalculationNode): string => {
if (!node) return "";
switch (node.type) {
case "field":
const field = availableFields.find((f) => f.name === node.fieldName);
return field?.label || node.fieldName || "필드";
case "constant":
return String(node.value || 0);
case "previous":
return "이전 결과";
case "operation":
const left = renderNodeLabel(node.left);
const right = renderNodeLabel(node.right);
const op = node.operator === "*" ? "×" : node.operator === "/" ? "÷" : node.operator;
return `(${left} ${op} ${right})`;
default:
return "";
}
};
// 함수 적용 UI
const renderFunctionStep = (step: CalculationStep) => {
if (step.expression.type !== "function") return null;
return (
<div className="flex items-center gap-2">
<Select
value={step.expression.functionName || "round"}
onValueChange={(value) => {
updateStep(step.id, {
expression: {
type: "function",
functionName: value as any,
params: [{ type: "previous" }],
},
});
}}
>
<SelectTrigger className="h-8 w-32 text-xs">
<SelectValue placeholder="함수 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="round"></SelectItem>
<SelectItem value="floor"></SelectItem>
<SelectItem value="ceil"></SelectItem>
<SelectItem value="abs"></SelectItem>
</SelectContent>
</Select>
{(step.expression.functionName === "round" ||
step.expression.functionName === "floor" ||
step.expression.functionName === "ceil") && (
<>
<span className="text-xs text-muted-foreground">:</span>
<Select
value={
step.expression.params?.[1]?.type === "field"
? step.expression.params[1].fieldName || ""
: String(step.expression.params?.[1]?.value || "1")
}
onValueChange={(value) => {
const isField = availableFields.some((f) => f.name === value);
updateStep(step.id, {
expression: {
...step.expression,
params: [
{ type: "previous" },
isField
? { type: "field", fieldName: value }
: { type: "constant", value: parseFloat(value) },
],
},
});
}}
>
<SelectTrigger className="h-8 w-32 text-xs">
<SelectValue placeholder="단위" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="1000">1,000</SelectItem>
{availableFields.map((field) => (
<SelectItem key={field.name} value={field.name}>
{field.label || field.name}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)}
</div>
);
};
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold"> </Label>
<Button variant="outline" size="sm" onClick={addStep} className="h-7 text-xs">
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{steps.length === 0 ? (
<Card className="border-dashed">
<CardContent className="py-6 text-center">
<Calculator className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
</p>
</CardContent>
</Card>
) : (
<div className="space-y-2">
{steps.map((step, idx) => (
<Card key={step.id} className="border-primary/30">
<CardHeader className="pb-2 pt-3 px-3">
<div className="flex items-center justify-between">
<CardTitle className="text-xs font-medium">
{step.label || `단계 ${idx + 1}`}
</CardTitle>
<div className="flex items-center gap-1">
<Input
value={step.label}
onChange={(e) => updateStep(step.id, { label: e.target.value })}
placeholder={`단계 ${idx + 1}`}
className="h-6 w-24 text-xs"
/>
<Button
variant="ghost"
size="sm"
onClick={() => removeStep(step.id)}
className="h-6 w-6 p-0"
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pb-3 px-3">
{step.expression.type === "function"
? renderFunctionStep(step)
: renderSimpleExpression(step)}
{/* 함수 적용 버튼 */}
{step.expression.type !== "function" && (
<Button
variant="outline"
size="sm"
onClick={() => {
updateStep(step.id, {
expression: {
type: "function",
functionName: "round",
params: [{ type: "previous" }, { type: "constant", value: 1 }],
},
});
}}
className="h-7 text-xs mt-2"
>
</Button>
)}
</CardContent>
</Card>
))}
</div>
)}
{/* 미리보기 */}
{steps.length > 0 && (
<Card className="bg-muted/30">
<CardContent className="py-3 px-3">
<div className="text-xs">
<span className="font-semibold">:</span>
<div className="mt-1 font-mono text-muted-foreground">
{steps.map((step, idx) => (
<div key={step.id}>
{idx + 1}. {renderNodeLabel(step.expression)}
</div>
))}
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
};