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

196 lines
7.3 KiB
TypeScript
Raw Normal View History

2025-10-24 15:40:08 +09:00
"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;
}
2025-10-29 11:26:00 +09:00
export const ValidationNotification = memo(({ validations, onNodeClick, onClose }: ValidationNotificationProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const summary = summarizeValidations(validations);
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
if (validations.length === 0) {
return null;
}
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
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": "병렬 테이블 접근",
2025-10-24 15:40:08 +09:00
};
2025-10-29 11:26:00 +09:00
return labels[type] || type;
};
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
// 타입별로 그룹화
const groupedValidations = validations.reduce(
(acc, validation) => {
2025-10-24 15:40:08 +09:00
if (!acc[validation.type]) {
acc[validation.type] = [];
}
acc[validation.type].push(validation);
return acc;
2025-10-29 11:26:00 +09:00
},
{} as Record<string, FlowValidation[]>,
);
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
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",
2025-10-29 11:26:00 +09:00
summary.hasBlockingIssues
? "border-destructive"
2025-10-29 11:26:00 +09:00
: summary.warningCount > 0
? "border-warning"
: "border-primary",
2025-10-29 11:26:00 +09:00
)}
>
{/* 헤더 */}
2025-10-24 15:40:08 +09:00
<div
className={cn(
2025-10-29 11:26:00 +09:00
"flex cursor-pointer items-center justify-between p-3",
summary.hasBlockingIssues ? "bg-destructive/10" : summary.warningCount > 0 ? "bg-warning/10" : "bg-primary/10",
2025-10-24 15:40:08 +09:00
)}
2025-10-29 11:26:00 +09:00
onClick={() => setIsExpanded(!isExpanded)}
2025-10-24 15:40:08 +09:00
>
2025-10-29 11:26:00 +09:00
<div className="flex items-center gap-2">
{summary.hasBlockingIssues ? (
<AlertCircle className="h-5 w-5 text-destructive" />
2025-10-29 11:26:00 +09:00
) : summary.warningCount > 0 ? (
<AlertTriangle className="h-5 w-5 text-warning" />
2025-10-29 11:26:00 +09:00
) : (
<Info className="h-5 w-5 text-primary" />
2025-10-24 15:40:08 +09:00
)}
<span className="text-sm font-semibold text-foreground"> </span>
2025-10-24 15:40:08 +09:00
<div className="flex items-center gap-1">
2025-10-29 11:26:00 +09:00
{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>
2025-10-24 15:40:08 +09:00
)}
2025-10-29 11:26:00 +09:00
{summary.infoCount > 0 && (
<Badge variant="secondary" className="h-5 text-[10px]">
{summary.infoCount}
</Badge>
2025-10-24 15:40:08 +09:00
)}
</div>
</div>
2025-10-29 11:26:00 +09:00
<div className="flex items-center gap-1">
{isExpanded ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
2025-10-29 11:26:00 +09:00
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
2025-10-29 11:26:00 +09:00
)}
{onClose && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onClose();
}}
className="h-6 w-6 p-0 hover:bg-muted"
2025-10-29 11:26:00 +09:00
>
<X className="h-3.5 w-3.5" />
</Button>
)}
</div>
</div>
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
{/* 확장된 내용 */}
{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;
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
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"
2025-10-29 11:26:00 +09:00
: firstValidation.severity === "warning"
? "bg-warning/10 text-warning"
: "bg-primary/10 text-primary",
2025-10-29 11:26:00 +09:00
)}
>
<Icon className="h-3 w-3" />
{getTypeLabel(type)}
<span className="ml-auto">{typeValidations.length}</span>
</div>
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
{/* 검증 항목들 */}
<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"
2025-10-29 11:26:00 +09:00
onClick={() => onNodeClick?.(validation.nodeId)}
>
<p className="leading-relaxed text-foreground">{validation.message}</p>
2025-10-29 11:26:00 +09:00
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
<div className="mt-1 text-[10px] text-muted-foreground">
2025-10-29 11:26:00 +09:00
: {validation.affectedNodes.length}
2025-10-24 15:40:08 +09:00
</div>
2025-10-29 11:26:00 +09:00
)}
<div className="mt-1 text-[10px] text-primary opacity-0 transition-opacity group-hover:opacity-100">
2025-10-29 11:26:00 +09:00
2025-10-24 15:40:08 +09:00
</div>
2025-10-29 11:26:00 +09:00
</div>
))}
2025-10-24 15:40:08 +09:00
</div>
2025-10-29 11:26:00 +09:00
</div>
);
})}
2025-10-24 15:40:08 +09:00
</div>
2025-10-29 11:26:00 +09:00
</div>
)}
2025-10-24 15:40:08 +09:00
2025-10-29 11:26:00 +09:00
{/* 요약 메시지 (닫혀있을 때) */}
{!isExpanded && (
<div className="border-t px-3 py-2">
<p className="text-xs text-muted-foreground">
2025-10-29 11:26:00 +09:00
{summary.hasBlockingIssues
? "⛔ 오류를 해결해야 저장할 수 있습니다"
: summary.warningCount > 0
? "⚠️ 경고 사항을 확인하세요"
: " 정보를 확인하세요"}
</p>
</div>
)}
2025-10-24 15:40:08 +09:00
</div>
2025-10-29 11:26:00 +09:00
</div>
);
});
2025-10-24 15:40:08 +09:00
ValidationNotification.displayName = "ValidationNotification";