ERP-node/frontend/components/dataflow/node-editor/ValidationNotification.tsx

196 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { memo, useState } from "react";
import { AlertTriangle, AlertCircle, Info, X, ChevronDown, ChevronUp } from "lucide-react";
import type { FlowValidation } from "@/lib/utils/flowValidation";
import { summarizeValidations } from "@/lib/utils/flowValidation";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
interface ValidationNotificationProps {
validations: FlowValidation[];
onNodeClick?: (nodeId: string) => void;
onClose?: () => void;
}
export const ValidationNotification = memo(({ validations, onNodeClick, onClose }: ValidationNotificationProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const summary = summarizeValidations(validations);
if (validations.length === 0) {
return null;
}
const getTypeLabel = (type: string): string => {
const labels: Record<string, string> = {
"disconnected-node": " ",
"parallel-conflict": " ",
"missing-where": "WHERE ",
"circular-reference": " ",
"data-source-mismatch": " ",
"parallel-table-access": " ",
};
return labels[type] || type;
};
// 타입별로 그룹화
const groupedValidations = validations.reduce(
(acc, validation) => {
if (!acc[validation.type]) {
acc[validation.type] = [];
}
acc[validation.type].push(validation);
return acc;
},
{} as Record<string, FlowValidation[]>,
);
return (
<div className="animate-in slide-in-from-right-5 fixed top-4 right-4 z-50 w-80 duration-300">
<div
className={cn(
"rounded-lg border-2 bg-background shadow-2xl",
summary.hasBlockingIssues
? "border-destructive"
: summary.warningCount > 0
? "border-warning"
: "border-primary",
)}
>
{/* 헤더 */}
<div
className={cn(
"flex cursor-pointer items-center justify-between p-3",
summary.hasBlockingIssues ? "bg-destructive/10" : summary.warningCount > 0 ? "bg-warning/10" : "bg-primary/10",
)}
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-2">
{summary.hasBlockingIssues ? (
<AlertCircle className="h-5 w-5 text-destructive" />
) : summary.warningCount > 0 ? (
<AlertTriangle className="h-5 w-5 text-warning" />
) : (
<Info className="h-5 w-5 text-primary" />
)}
<span className="text-sm font-semibold text-foreground">플로우 검증</span>
<div className="flex items-center gap-1">
{summary.errorCount > 0 && (
<Badge variant="destructive" className="h-5 text-[10px]">
{summary.errorCount}
</Badge>
)}
{summary.warningCount > 0 && (
<Badge className="h-5 bg-warning text-[10px] hover:bg-warning/90">{summary.warningCount}</Badge>
)}
{summary.infoCount > 0 && (
<Badge variant="secondary" className="h-5 text-[10px]">
{summary.infoCount}
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-1">
{isExpanded ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
{onClose && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="h-6 w-6 p-0 hover:bg-muted"
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
</div>
{/* 확장된 내용 */}
{isExpanded && (
<div className="max-h-[60vh] overflow-y-auto border-t">
<div className="space-y-2 p-2">
{Object.entries(groupedValidations).map(([type, typeValidations]) => {
const firstValidation = typeValidations[0];
const Icon =
firstValidation.severity === "error"
? AlertCircle
: firstValidation.severity === "warning"
? AlertTriangle
: Info;
return (
<div key={type}>
{/* 타입 헤더 */}
<div
className={cn(
"mb-1 flex items-center gap-2 rounded-md px-2 py-1 text-xs font-medium",
firstValidation.severity === "error"
? "bg-destructive/10 text-destructive"
: firstValidation.severity === "warning"
? "bg-warning/10 text-warning"
: "bg-primary/10 text-primary",
)}
>
<Icon className="h-3 w-3" />
{getTypeLabel(type)}
<span className="ml-auto">{typeValidations.length}개</span>
</div>
{/* 검증 항목들 */}
<div className="space-y-1 pl-5">
{typeValidations.map((validation, index) => (
<div
key={index}
className="group cursor-pointer rounded-md border border-border bg-muted p-2 text-xs transition-all hover:border-primary/50 hover:bg-background hover:shadow-sm"
onClick={() => onNodeClick?.(validation.nodeId)}
>
<p className="leading-relaxed text-foreground">{validation.message}</p>
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
<div className="mt-1 text-[10px] text-muted-foreground">
영향받는 노드: {validation.affectedNodes.length}개
</div>
)}
<div className="mt-1 text-[10px] text-primary opacity-0 transition-opacity group-hover:opacity-100">
클릭하여 노드 보기 →
</div>
</div>
))}
</div>
</div>
);
})}
</div>
</div>
)}
{/* 요약 메시지 (닫혀있을 때) */}
{!isExpanded && (
<div className="border-t px-3 py-2">
<p className="text-xs text-muted-foreground">
{summary.hasBlockingIssues
? " "
: summary.warningCount > 0
? " "
: " "}
</p>
</div>
)}
</div>
</div>
);
});
ValidationNotification.displayName = "ValidationNotification";