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-white shadow-2xl",
summary.hasBlockingIssues
? "border-red-500"
: summary.warningCount > 0
? "border-yellow-500"
: "border-blue-500",
)}
>
{/* 헤더 */}
<div
className={cn(
"flex cursor-pointer items-center justify-between p-3",
summary.hasBlockingIssues ? "bg-red-50" : summary.warningCount > 0 ? "bg-yellow-50" : "bg-blue-50",
)}
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-2">
{summary.hasBlockingIssues ? (
<AlertCircle className="h-5 w-5 text-red-600" />
) : summary.warningCount > 0 ? (
<AlertTriangle className="h-5 w-5 text-yellow-600" />
) : (
<Info className="h-5 w-5 text-blue-600" />
)}
<span className="text-sm font-semibold text-gray-900"> </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-yellow-500 text-[10px] hover:bg-yellow-600">{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-gray-400" />
) : (
<ChevronDown className="h-4 w-4 text-gray-400" />
)}
{onClose && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="h-6 w-6 p-0 hover:bg-white/50"
>
<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-red-100 text-red-700"
: firstValidation.severity === "warning"
? "bg-yellow-100 text-yellow-700"
: "bg-blue-100 text-blue-700",
)}
>
<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-gray-200 bg-gray-50 p-2 text-xs transition-all hover:border-gray-300 hover:bg-white hover:shadow-sm"
onClick={() => onNodeClick?.(validation.nodeId)}
>
<p className="leading-relaxed text-gray-700">{validation.message}</p>
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
<div className="mt-1 text-[10px] text-gray-500">
: {validation.affectedNodes.length}
</div>
)}
<div className="mt-1 text-[10px] text-blue-600 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-gray-600">
{summary.hasBlockingIssues
? "⛔ 오류를 해결해야 저장할 수 있습니다"
: summary.warningCount > 0
? "⚠️ 경고 사항을 확인하세요"
: " 정보를 확인하세요"}
</p>
</div>
)}
</div>
</div>
);
});
ValidationNotification.displayName = "ValidationNotification";