From ef4c533ebacc2cdc97e518f30e9a9350004fcdfb Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Apr 2025 02:13:30 +0000 Subject: ~20250428 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data/sedp-compare-dialog.tsx | 372 +++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 components/form-data/sedp-compare-dialog.tsx (limited to 'components/form-data/sedp-compare-dialog.tsx') diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx new file mode 100644 index 00000000..461a3630 --- /dev/null +++ b/components/form-data/sedp-compare-dialog.tsx @@ -0,0 +1,372 @@ +import * as React from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Loader, RefreshCw, AlertCircle, CheckCircle, Info } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "sonner"; +import { DataTableColumnJSON } from "./form-data-table-columns"; +import { ExcelDownload } from "./sedp-excel-download"; + +interface SEDPCompareDialogProps { + isOpen: boolean; + onClose: () => void; + tableData: any[]; + columnsJSON: DataTableColumnJSON[]; + projectCode: string; + formCode: string; + fetchTagDataFromSEDP: (projectCode: string, formCode: string) => Promise; +} + +interface ComparisonResult { + tagNo: string; + tagDesc: string; + isMatching: boolean; + attributes: { + key: string; + label: string; + localValue: any; + sedpValue: any; + isMatching: boolean; + uom?: string; + }[]; +} + +// Component for formatting display value with UOM +const DisplayValue = ({ value, uom, isSedp = false }: { value: any; uom?: string; isSedp?: boolean }) => { + if (value === "" || value === null || value === undefined) { + return (empty); + } + + // SEDP 값은 UOM을 표시하지 않음 (이미 포함되어 있다고 가정) + if (isSedp) { + return {value}; + } + + // 로컬 값은 UOM과 함께 표시 + return ( + + {value} + {uom && {uom}} + + ); +}; + +// 범례 컴포넌트 추가 +const ColorLegend = () => { + return ( +
+
+ + 범례: +
+
+
+
+ 로컬 값 +
+
+
+ SEDP 값 +
+
+
+ ); +}; + +export function SEDPCompareDialog({ + isOpen, + onClose, + tableData, + columnsJSON, + projectCode, + formCode, + fetchTagDataFromSEDP, +}: SEDPCompareDialogProps) { + const [isLoading, setIsLoading] = React.useState(false); + const [comparisonResults, setComparisonResults] = React.useState([]); + const [activeTab, setActiveTab] = React.useState("all"); + const [isExporting, setIsExporting] = React.useState(false); + + // Stats for summary + const totalTags = comparisonResults.length; + const matchingTags = comparisonResults.filter(r => r.isMatching).length; + const nonMatchingTags = totalTags - matchingTags; + + // Get column label map and UOM map for better display + const { columnLabelMap, columnUomMap } = React.useMemo(() => { + const labelMap: Record = {}; + const uomMap: Record = {}; + + columnsJSON.forEach(col => { + labelMap[col.key] = col.displayLabel || col.label; + if (col.uom) { + uomMap[col.key] = col.uom; + } + }); + + return { columnLabelMap: labelMap, columnUomMap: uomMap }; + }, [columnsJSON]); + + const fetchAndCompareData = React.useCallback(async () => { + if (!projectCode || !formCode) { + toast.error("Project code or form code is missing"); + return; + } + + try { + setIsLoading(true); + + // Fetch data from SEDP API + const sedpData = await fetchTagDataFromSEDP(projectCode, formCode); + + // Get the table name from the response + const tableName = Object.keys(sedpData)[0]; + const sedpTagEntries = sedpData[tableName] || []; + + // Create a map of SEDP data by TAG_NO for quick lookup + const sedpTagMap = new Map(); + sedpTagEntries.forEach((entry: any) => { + const tagNo = entry.TAG_NO; + const attributesMap = new Map(); + + // Convert attributes array to map for easier access + if (Array.isArray(entry.ATTRIBUTES)) { + entry.ATTRIBUTES.forEach((attr: any) => { + attributesMap.set(attr.ATT_ID, attr.VALUE); + }); + } + + sedpTagMap.set(tagNo, { + tagDesc: entry.TAG_DESC, + attributes: attributesMap + }); + }); + + // Compare with local table data + const results: ComparisonResult[] = tableData.map(localItem => { + const tagNo = localItem.TAG_NO; + const sedpItem = sedpTagMap.get(tagNo); + + // If tag not found in SEDP data + if (!sedpItem) { + return { + tagNo, + tagDesc: localItem.TAG_DESC || "", + isMatching: false, + attributes: columnsJSON + .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC") + .map(col => ({ + key: col.key, + label: columnLabelMap[col.key] || col.key, + localValue: localItem[col.key], + sedpValue: null, + isMatching: false, + uom: columnUomMap[col.key] + })) + }; + } + + // Compare attributes + const attributeComparisons = columnsJSON + .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC") + .map(col => { + const localValue = localItem[col.key]; + const sedpValue = sedpItem.attributes.get(col.key); + const uom = columnUomMap[col.key]; + + // Compare values (with type handling) + let isMatching = false; + + // 문자열 비교 + // Normalize empty values + const normalizedLocal = localValue === undefined || localValue === null ? "" : String(localValue).trim(); + const normalizedSedp = sedpValue === undefined || sedpValue === null ? "" : String(sedpValue).trim(); + isMatching = normalizedLocal === normalizedSedp; + + + return { + key: col.key, + label: columnLabelMap[col.key] || col.key, + localValue, + sedpValue, + isMatching, + uom + }; + }); + + // Item is matching if all attributes match + const isItemMatching = attributeComparisons.every(attr => attr.isMatching); + + return { + tagNo, + tagDesc: localItem.TAG_DESC || "", + isMatching: isItemMatching, + attributes: attributeComparisons + }; + }); + + setComparisonResults(results); + + // Show summary in toast + const matchCount = results.filter(r => r.isMatching).length; + const nonMatchCount = results.length - matchCount; + + if (nonMatchCount > 0) { + toast.warning(`Found ${nonMatchCount} tags with differences`); + } else if (results.length > 0) { + toast.success(`All ${results.length} tags match with SEDP data`); + } else { + toast.info("No tags to compare"); + } + + } catch (error) { + console.error("SEDP comparison error:", error); + toast.error(`Failed to compare with SEDP: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + setIsLoading(false); + } + }, [projectCode, formCode, tableData, columnsJSON, fetchTagDataFromSEDP, columnLabelMap, columnUomMap]); + + // Fetch data when dialog opens + React.useEffect(() => { + if (isOpen) { + fetchAndCompareData(); + } + }, [isOpen, fetchAndCompareData]); + + // Filter results based on active tab + const filteredResults = React.useMemo(() => { + switch (activeTab) { + case "matching": + return comparisonResults.filter(r => r.isMatching); + case "differences": + return comparisonResults.filter(r => !r.isMatching); + case "all": + default: + return comparisonResults; + } + }, [comparisonResults, activeTab]); + + return ( + + + + + SEDP 데이터 비교 +
+ + {matchingTags} / {totalTags} 일치 + + +
+
+
+ + {/* 범례 추가 */} +
+ +
+ + + + 전체 태그 ({totalTags}) + 차이 있음 ({nonMatchingTags}) + 일치함 ({matchingTags}) + + + + {isLoading ? ( +
+ + 데이터 비교 중... +
+ ) : filteredResults.length > 0 ? ( + + + + Tag Number + Tag Description + 상태 + 차이점 + + + + {filteredResults.map((result) => { + // Find differences to display + const differences = result.attributes.filter(attr => !attr.isMatching); + + return ( + + {result.tagNo} + {result.tagDesc} + + {result.isMatching ? ( + + + 일치 + + ) : ( + + + 차이 있음 + + )} + + + {differences.length > 0 ? ( +
+ {differences.map((diff) => ( +
+ {diff.label}: + + + + + + +
+ ))} +
+ ) : ( + 모든 값이 일치합니다 + )} +
+
+ ); + })} +
+
+ ) : ( +
+ 현재 필터에 맞는 태그가 없습니다 +
+ )} +
+
+ + + + + +
+
+ ); +} \ No newline at end of file -- cgit v1.2.3