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

430 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
};