ERP-node/frontend/components/dataflow/node-editor/panels/ValidationPanel.tsx

216 lines
8.2 KiB
TypeScript
Raw Normal View History

2025-10-24 15:40:08 +09:00
"use client";
/**
*
*
2025-10-24 15:40:08 +09:00
*
*/
import { memo, useMemo } from "react";
import { AlertTriangle, AlertCircle, Info, ChevronDown, ChevronUp, X } from "lucide-react";
import type { FlowValidation } from "@/lib/utils/flowValidation";
import { summarizeValidations } from "@/lib/utils/flowValidation";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface ValidationPanelProps {
validations: FlowValidation[];
onNodeClick?: (nodeId: string) => void;
onClose?: () => void;
}
export const ValidationPanel = memo(({ validations, onNodeClick, onClose }: ValidationPanelProps) => {
const [expandedTypes, setExpandedTypes] = useState<Set<string>>(new Set());
const summary = useMemo(() => summarizeValidations(validations), [validations]);
2025-10-24 15:40:08 +09:00
// 타입별로 그룹화
const groupedValidations = useMemo(() => {
const groups = new Map<string, FlowValidation[]>();
for (const validation of validations) {
if (!groups.has(validation.type)) {
groups.set(validation.type, []);
2025-10-24 15:40:08 +09:00
}
groups.get(validation.type)!.push(validation);
}
return Array.from(groups.entries()).sort((a, b) => {
// 심각도 순으로 정렬
const severityOrder = { error: 0, warning: 1, info: 2 };
const aSeverity = Math.min(...a[1].map((v) => severityOrder[v.severity]));
const bSeverity = Math.min(...b[1].map((v) => severityOrder[v.severity]));
return aSeverity - bSeverity;
});
}, [validations]);
2025-10-24 15:40:08 +09:00
const toggleExpanded = (type: string) => {
setExpandedTypes((prev) => {
const next = new Set(prev);
if (next.has(type)) {
next.delete(type);
} else {
next.add(type);
}
return next;
});
};
2025-10-24 15:40:08 +09:00
const getTypeLabel = (type: string): string => {
const labels: Record<string, string> = {
"parallel-conflict": "병렬 실행 충돌",
"missing-where": "WHERE 조건 누락",
"circular-reference": "순환 참조",
"data-source-mismatch": "데이터 소스 불일치",
"parallel-table-access": "병렬 테이블 접근",
2025-10-24 15:40:08 +09:00
};
return labels[type] || type;
};
2025-10-24 15:40:08 +09:00
if (validations.length === 0) {
2025-10-24 15:40:08 +09:00
return (
<div className="flex h-full flex-col border-l border-gray-200 bg-white">
<div className="flex items-center justify-between border-b border-gray-200 p-4">
<h3 className="text-sm font-semibold text-gray-900"> </h3>
{onClose && (
<Button variant="ghost" size="sm" onClick={onClose} className="h-6 w-6 p-0">
2025-10-24 15:40:08 +09:00
<X className="h-4 w-4" />
</Button>
)}
</div>
<div className="flex flex-1 items-center justify-center p-8 text-center">
<div>
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<Info className="h-6 w-6 text-green-600" />
</div>
<p className="text-sm font-medium text-gray-900"> </p>
<p className="mt-1 text-xs text-gray-500"> </p>
2025-10-24 15:40:08 +09:00
</div>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col border-l border-gray-200 bg-white">
{/* 헤더 */}
<div className="flex items-center justify-between border-b border-gray-200 p-4">
<h3 className="text-sm font-semibold text-gray-900"> </h3>
{onClose && (
<Button variant="ghost" size="sm" onClick={onClose} className="h-6 w-6 p-0">
<X className="h-4 w-4" />
</Button>
)}
</div>
{/* 요약 */}
<div className="border-b border-gray-200 bg-gray-50 p-4">
<div className="flex items-center gap-3">
{summary.errorCount > 0 && (
<Badge variant="destructive" className="gap-1">
<AlertCircle className="h-3 w-3" />
{summary.errorCount}
</Badge>
)}
{summary.warningCount > 0 && (
<Badge className="gap-1 bg-yellow-500 hover:bg-yellow-600">
<AlertTriangle className="h-3 w-3" />
{summary.warningCount}
</Badge>
)}
{summary.infoCount > 0 && (
<Badge variant="secondary" className="gap-1">
<Info className="h-3 w-3" />
{summary.infoCount}
</Badge>
2025-10-24 15:40:08 +09:00
)}
</div>
{summary.hasBlockingIssues && (
<p className="mt-2 text-xs text-red-600"> </p>
)}
</div>
2025-10-24 15:40:08 +09:00
{/* 검증 결과 목록 */}
<ScrollArea className="flex-1">
<div className="p-2">
{groupedValidations.map(([type, typeValidations]) => {
const isExpanded = expandedTypes.has(type);
const firstValidation = typeValidations[0];
const Icon =
firstValidation.severity === "error"
? AlertCircle
: firstValidation.severity === "warning"
? AlertTriangle
: Info;
2025-10-24 15:40:08 +09:00
return (
<div key={type} className="mb-2">
{/* 그룹 헤더 */}
<button
onClick={() => toggleExpanded(type)}
className={cn(
"flex w-full items-center gap-2 rounded-lg p-3 text-left transition-colors",
firstValidation.severity === "error"
? "bg-red-50 hover:bg-red-100"
: firstValidation.severity === "warning"
? "bg-yellow-50 hover:bg-yellow-100"
: "bg-blue-50 hover:bg-blue-100",
)}
>
<Icon
2025-10-24 15:40:08 +09:00
className={cn(
"h-4 w-4 shrink-0",
2025-10-24 15:40:08 +09:00
firstValidation.severity === "error"
? "text-red-600"
2025-10-24 15:40:08 +09:00
: firstValidation.severity === "warning"
? "text-yellow-600"
: "text-blue-600",
2025-10-24 15:40:08 +09:00
)}
/>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900">{getTypeLabel(type)}</div>
<div className="text-xs text-gray-500">{typeValidations.length} </div>
</div>
{isExpanded ? (
<ChevronUp className="h-4 w-4 text-gray-400" />
) : (
<ChevronDown className="h-4 w-4 text-gray-400" />
)}
</button>
2025-10-24 15:40:08 +09:00
{/* 상세 내용 */}
{isExpanded && (
<div className="mt-1 space-y-1 pr-2 pl-6">
{typeValidations.map((validation, index) => (
<div
key={index}
className="group cursor-pointer rounded-lg border border-gray-200 bg-white p-3 transition-all hover:border-gray-300 hover:shadow-sm"
onClick={() => onNodeClick?.(validation.nodeId)}
>
<div className="text-xs text-gray-700">{validation.message}</div>
{validation.affectedNodes && validation.affectedNodes.length > 1 && (
<div className="mt-2 flex items-center gap-2">
<Badge variant="outline" className="text-[10px]">
: {validation.affectedNodes.length}
</Badge>
2025-10-24 15:40:08 +09:00
</div>
)}
<div className="mt-2 text-[10px] text-gray-400 opacity-0 transition-opacity group-hover:opacity-100">
2025-10-24 15:40:08 +09:00
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</ScrollArea>
</div>
);
});
2025-10-24 15:40:08 +09:00
ValidationPanel.displayName = "ValidationPanel";