108 lines
3.9 KiB
TypeScript
108 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 집계 노드 (Aggregate Node)
|
|
* SUM, COUNT, AVG, MIN, MAX 등 집계 연산을 수행
|
|
*/
|
|
|
|
import { memo } from "react";
|
|
import { Handle, Position, NodeProps } from "reactflow";
|
|
import { Calculator, Layers } from "lucide-react";
|
|
import type { AggregateNodeData, AggregateFunction } from "@/types/node-editor";
|
|
|
|
// 집계 함수별 아이콘/라벨
|
|
const AGGREGATE_FUNCTION_LABELS: Record<AggregateFunction, string> = {
|
|
SUM: "합계",
|
|
COUNT: "개수",
|
|
AVG: "평균",
|
|
MIN: "최소",
|
|
MAX: "최대",
|
|
FIRST: "첫번째",
|
|
LAST: "마지막",
|
|
};
|
|
|
|
export const AggregateNode = memo(({ data, selected }: NodeProps<AggregateNodeData>) => {
|
|
const groupByCount = data.groupByFields?.length || 0;
|
|
const aggregationCount = data.aggregations?.length || 0;
|
|
|
|
return (
|
|
<div
|
|
className={`min-w-[280px] rounded-lg border-2 bg-white shadow-md transition-all ${
|
|
selected ? "border-purple-500 shadow-lg" : "border-gray-200"
|
|
}`}
|
|
>
|
|
{/* 헤더 */}
|
|
<div className="flex items-center gap-2 rounded-t-lg bg-purple-600 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">
|
|
{groupByCount > 0 ? `${groupByCount}개 그룹` : "전체"} / {aggregationCount}개 집계
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 본문 */}
|
|
<div className="p-3 space-y-3">
|
|
{/* 그룹 기준 */}
|
|
{groupByCount > 0 && (
|
|
<div className="rounded bg-purple-50 p-2">
|
|
<div className="flex items-center gap-1 mb-1">
|
|
<Layers className="h-3 w-3 text-purple-600" />
|
|
<span className="text-xs font-medium text-purple-700">그룹 기준</span>
|
|
</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{data.groupByFields.slice(0, 3).map((field, idx) => (
|
|
<span
|
|
key={idx}
|
|
className="inline-flex items-center rounded bg-purple-100 px-2 py-0.5 text-xs text-purple-700"
|
|
>
|
|
{field.fieldLabel || field.field}
|
|
</span>
|
|
))}
|
|
{data.groupByFields.length > 3 && (
|
|
<span className="text-xs text-purple-500">+{data.groupByFields.length - 3}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 집계 연산 */}
|
|
{aggregationCount > 0 ? (
|
|
<div className="space-y-2">
|
|
{data.aggregations.slice(0, 4).map((agg, idx) => (
|
|
<div key={agg.id || idx} className="rounded bg-gray-50 p-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="rounded bg-purple-600 px-1.5 py-0.5 text-xs font-medium text-white">
|
|
{AGGREGATE_FUNCTION_LABELS[agg.function] || agg.function}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
{agg.outputFieldLabel || agg.outputField}
|
|
</span>
|
|
</div>
|
|
<div className="mt-1 text-xs text-gray-600">
|
|
{agg.sourceFieldLabel || agg.sourceField}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{data.aggregations.length > 4 && (
|
|
<div className="text-xs text-gray-400 text-center">
|
|
... 외 {data.aggregations.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-purple-500" />
|
|
<Handle type="source" position={Position.Right} className="!h-3 !w-3 !bg-purple-500" />
|
|
</div>
|
|
);
|
|
});
|
|
|
|
AggregateNode.displayName = "AggregateNode";
|
|
|