105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
/**
|
|
* 플로우 노드 컴포넌트
|
|
* React Flow 커스텀 노드
|
|
*/
|
|
|
|
import { memo } from "react";
|
|
import { Handle, Position, NodeProps } from "reactflow";
|
|
import { FlowNodeData, FlowCondition, ConditionOperator } from "@/types/flow";
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
// 조건을 자연어로 변환하는 헬퍼 함수
|
|
const formatCondition = (cond: FlowCondition): string => {
|
|
const operatorLabels: Record<ConditionOperator, string> = {
|
|
equals: "=",
|
|
not_equals: "≠",
|
|
greater_than: ">",
|
|
less_than: "<",
|
|
greater_than_or_equal: "≥",
|
|
less_than_or_equal: "≤",
|
|
in: "IN",
|
|
not_in: "NOT IN",
|
|
like: "LIKE",
|
|
not_like: "NOT LIKE",
|
|
is_null: "IS NULL",
|
|
is_not_null: "IS NOT NULL",
|
|
};
|
|
|
|
const operatorLabel = operatorLabels[cond.operator] || cond.operator;
|
|
|
|
if (cond.operator === "is_null" || cond.operator === "is_not_null") {
|
|
return `${cond.column} ${operatorLabel}`;
|
|
}
|
|
|
|
return `${cond.column} ${operatorLabel} "${cond.value || ""}"`;
|
|
};
|
|
|
|
const formatAllConditions = (data: FlowNodeData): string => {
|
|
if (!data.condition || data.condition.conditions.length === 0) {
|
|
return "조건 없음";
|
|
}
|
|
|
|
const conditions = data.condition.conditions;
|
|
const type = data.condition.type;
|
|
|
|
// 조건이 많으면 간략하게 표시
|
|
if (conditions.length > 2) {
|
|
return `${conditions.length}개 조건 (${type})`;
|
|
}
|
|
|
|
const connector = type === "AND" ? " AND " : " OR ";
|
|
return conditions.map(formatCondition).join(connector);
|
|
};
|
|
|
|
export const FlowNodeComponent = memo(({ data }: NodeProps<FlowNodeData>) => {
|
|
return (
|
|
<div className="bg-card min-w-[200px] rounded-lg border px-4 py-3 shadow-sm transition-shadow hover:shadow-md">
|
|
{/* 입력 핸들 */}
|
|
<Handle type="target" position={Position.Left} className="border-primary bg-background h-3 w-3 border-2" />
|
|
|
|
{/* 노드 내용 */}
|
|
<div>
|
|
<div className="mb-2 flex items-center justify-between gap-2">
|
|
<Badge variant="outline" className="text-xs">
|
|
단계 {data.stepOrder}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="text-foreground mb-2 text-sm font-semibold">{data.label}</div>
|
|
|
|
{/* 테이블 정보 */}
|
|
{data.tableName && (
|
|
<div className="bg-muted text-muted-foreground mb-2 flex items-center gap-1 rounded-md px-2 py-1 text-xs">
|
|
<span>📊</span>
|
|
<span className="truncate">{data.tableName}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 데이터 건수 */}
|
|
{data.count !== undefined && (
|
|
<Badge variant="secondary" className="mb-2 text-xs">
|
|
{data.count}건
|
|
</Badge>
|
|
)}
|
|
|
|
{/* 조건 미리보기 */}
|
|
{data.condition && data.condition.conditions.length > 0 ? (
|
|
<div className="mt-2">
|
|
<div className="text-muted-foreground mb-1 text-xs font-medium">조건:</div>
|
|
<div className="text-muted-foreground text-xs break-words" style={{ lineHeight: "1.4" }}>
|
|
{formatAllConditions(data)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-muted-foreground mt-2 text-xs">조건 없음</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 출력 핸들 */}
|
|
<Handle type="source" position={Position.Right} className="border-primary bg-background h-3 w-3 border-2" />
|
|
</div>
|
|
);
|
|
});
|
|
|
|
FlowNodeComponent.displayName = "FlowNodeComponent";
|