diff options
Diffstat (limited to 'components/form-data/sedp-compare-dialog.tsx')
| -rw-r--r-- | components/form-data/sedp-compare-dialog.tsx | 434 |
1 files changed, 290 insertions, 144 deletions
diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx index 461a3630..37fe18ed 100644 --- a/components/form-data/sedp-compare-dialog.tsx +++ b/components/form-data/sedp-compare-dialog.tsx @@ -3,11 +3,12 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from " 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 { Loader, RefreshCw, AlertCircle, CheckCircle, Info, EyeOff } 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"; +import { Switch } from "../ui/switch"; interface SEDPCompareDialogProps { isOpen: boolean; @@ -38,12 +39,12 @@ const DisplayValue = ({ value, uom, isSedp = false }: { value: any; uom?: string if (value === "" || value === null || value === undefined) { return <span>(empty)</span>; } - + // SEDP 값은 UOM을 표시하지 않음 (이미 포함되어 있다고 가정) if (isSedp) { return <span>{value}</span>; } - + // 로컬 값은 UOM과 함께 표시 return ( <span> @@ -88,27 +89,72 @@ export function SEDPCompareDialog({ const [comparisonResults, setComparisonResults] = React.useState<ComparisonResult[]>([]); const [activeTab, setActiveTab] = React.useState("all"); const [isExporting, setIsExporting] = React.useState(false); - + const [missingTags, setMissingTags] = React.useState<{ + localOnly: { tagNo: string; tagDesc: string }[]; + sedpOnly: { tagNo: string; tagDesc: string }[]; + }>( + { localOnly: [], sedpOnly: [] } + ); + // 추가: 차이점만 표시하는 옵션 + const [showOnlyDifferences, setShowOnlyDifferences] = React.useState(true); + // Stats for summary const totalTags = comparisonResults.length; const matchingTags = comparisonResults.filter(r => r.isMatching).length; const nonMatchingTags = totalTags - matchingTags; + const totalMissingTags = missingTags.localOnly.length + missingTags.sedpOnly.length; // Get column label map and UOM map for better display const { columnLabelMap, columnUomMap } = React.useMemo(() => { const labelMap: Record<string, string> = {}; const uomMap: Record<string, string> = {}; - + columnsJSON.forEach(col => { labelMap[col.key] = col.displayLabel || col.label; if (col.uom) { uomMap[col.key] = col.uom; } }); - + return { columnLabelMap: labelMap, columnUomMap: uomMap }; }, [columnsJSON]); + // 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]); + + // 변경: 표시할 컬럼 결정 (차이가 있는 컬럼만 or 모든 컬럼) + const columnsToDisplay = React.useMemo(() => { + // 기본 컬럼 (TAG_NO, TAG_DESC 제외) + const columns = columnsJSON.filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC"); + + if (!showOnlyDifferences) { + return columns; // 모든 컬럼 표시 + } + + // 하나라도 차이가 있는 속성만 필터링 + const columnsWithDifferences = new Set<string>(); + comparisonResults.forEach(result => { + result.attributes.forEach(attr => { + if (!attr.isMatching) { + columnsWithDifferences.add(attr.key); + } + }); + }); + + // 차이가 있는 컬럼만 반환 + return columns.filter(col => columnsWithDifferences.has(col.key)); + }, [columnsJSON, comparisonResults, showOnlyDifferences]); + const fetchAndCompareData = React.useCallback(async () => { if (!projectCode || !formCode) { toast.error("Project code or form code is missing"); @@ -117,110 +163,120 @@ export function SEDPCompareDialog({ 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) { + + // Create sets for finding missing tags + const localTagNos = new Set(tableData.map(item => item.TAG_NO)); + const sedpTagNos = new Set(sedpTagMap.keys()); + + // Find missing tags + const localOnlyTags = tableData + .filter(item => !sedpTagMap.has(item.TAG_NO)) + .map(item => ({ tagNo: item.TAG_NO, tagDesc: item.TAG_DESC || "" })); + + const sedpOnlyTags = Array.from(sedpTagMap.entries()) + .filter(([tagNo]) => !localTagNos.has(tagNo)) + .map(([tagNo, data]) => ({ tagNo, tagDesc: data.tagDesc || "" })); + + setMissingTags({ + localOnly: localOnlyTags, + sedpOnly: sedpOnlyTags + }); + + // Compare with local table data (only for tags that exist in both systems) + const results: ComparisonResult[] = tableData + .filter(localItem => sedpTagMap.has(localItem.TAG_NO)) + .map(localItem => { + const tagNo = localItem.TAG_NO; + const sedpItem = sedpTagMap.get(tagNo); + + // 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; + + // Special case: Empty SEDP value and 0 local value + if ((sedpValue === "" || sedpValue === null || sedpValue === undefined) && + (localValue === 0 || localValue === "0")) { + isMatching = true; + } else { + // Standard string comparison for other cases + 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: 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] - })) + isMatching: isItemMatching, + attributes: attributeComparisons }; - } - - // 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; - + const missingCount = localOnlyTags.length + sedpOnlyTags.length; + + if (missingCount > 0) { + toast.error(`Found ${missingCount} missing tags between systems`); + } + if (nonMatchCount > 0) { toast.warning(`Found ${nonMatchCount} tags with differences`); - } else if (results.length > 0) { + } else if (results.length > 0 && missingCount === 0) { toast.success(`All ${results.length} tags match with SEDP data`); - } else { + } else if (results.length === 0 && missingCount === 0) { 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'}`); @@ -228,41 +284,38 @@ export function SEDPCompareDialog({ 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 ( <Dialog open={isOpen} onOpenChange={onClose}> <DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col"> <DialogHeader> - <DialogTitle className="flex items-center justify-between"> - <span>SEDP 데이터 비교</span> + <DialogTitle className="mb-2">SEDP 데이터 비교</DialogTitle> + <div className="flex items-center justify-between gap-2 pr-8"> + <div className="flex items-center gap-2"> + <Switch + checked={showOnlyDifferences} + onCheckedChange={setShowOnlyDifferences} + id="show-differences" + /> + <label htmlFor="show-differences" className="text-sm cursor-pointer"> + 차이가 있는 항목만 표시 + </label> + </div> <div className="flex items-center gap-2"> - <Badge variant={matchingTags === totalTags ? "default" : "destructive"}> - {matchingTags} / {totalTags} 일치 + <Badge variant={matchingTags === totalTags && totalMissingTags === 0 ? "default" : "destructive"}> + {matchingTags} / {totalTags} 일치 {totalMissingTags > 0 ? `(${totalMissingTags} 누락)` : ''} </Badge> - <Button - variant="outline" - size="sm" - onClick={fetchAndCompareData} + <Button + variant="outline" + size="sm" + onClick={fetchAndCompareData} disabled={isLoading} > {isLoading ? ( @@ -273,47 +326,125 @@ export function SEDPCompareDialog({ <span className="ml-2">새로고침</span> </Button> </div> - </DialogTitle> + </div> </DialogHeader> - + {/* 범례 추가 */} <div className="mb-4"> <ColorLegend /> </div> - + <Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden"> <TabsList> <TabsTrigger value="all">전체 태그 ({totalTags})</TabsTrigger> <TabsTrigger value="differences">차이 있음 ({nonMatchingTags})</TabsTrigger> <TabsTrigger value="matching">일치함 ({matchingTags})</TabsTrigger> + <TabsTrigger value="missing" className={totalMissingTags > 0 ? "text-red-500" : ""}> + 누락된 태그 ({totalMissingTags}) + </TabsTrigger> </TabsList> - + <TabsContent value={activeTab} className="flex-1 overflow-auto"> {isLoading ? ( <div className="flex items-center justify-center h-full"> <Loader className="h-8 w-8 animate-spin mr-2" /> <span>데이터 비교 중...</span> </div> + ) : activeTab === "missing" ? ( + // Missing tags tab content + <div className="space-y-6"> + {missingTags.localOnly.length > 0 && ( + <div> + <h3 className="text-sm font-medium mb-2">로컬에만 있는 태그 ({missingTags.localOnly.length})</h3> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[180px]">Tag Number</TableHead> + <TableHead>Tag Description</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {missingTags.localOnly.map((tag) => ( + <TableRow key={tag.tagNo} className="bg-yellow-50 dark:bg-yellow-950/20"> + <TableCell className="font-medium">{tag.tagNo}</TableCell> + <TableCell>{tag.tagDesc}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + + {missingTags.sedpOnly.length > 0 && ( + <div> + <h3 className="text-sm font-medium mb-2">SEDP에만 있는 태그 ({missingTags.sedpOnly.length})</h3> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-[180px]">Tag Number</TableHead> + <TableHead>Tag Description</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {missingTags.sedpOnly.map((tag) => ( + <TableRow key={tag.tagNo} className="bg-blue-50 dark:bg-blue-950/20"> + <TableCell className="font-medium">{tag.tagNo}</TableCell> + <TableCell>{tag.tagDesc}</TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + + {totalMissingTags === 0 && ( + <div className="flex items-center justify-center h-full text-muted-foreground"> + 모든 태그가 양쪽 시스템에 존재합니다 + </div> + )} + </div> ) : filteredResults.length > 0 ? ( - <Table> - <TableHeader> - <TableRow> - <TableHead className="w-[180px]">Tag Number</TableHead> - <TableHead className="w-[200px]">Tag Description</TableHead> - <TableHead className="w-[120px]">상태</TableHead> - <TableHead>차이점</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {filteredResults.map((result) => { - // Find differences to display - const differences = result.attributes.filter(attr => !attr.isMatching); - - return ( + // 개선된 테이블 구조 + <div className="overflow-x-auto"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="sticky left-0 z-10 bg-background w-[180px]">Tag Number</TableHead> + <TableHead className="sticky left-[180px] z-10 bg-background w-[200px]">Tag Description</TableHead> + <TableHead className="sticky left-[380px] z-10 bg-background w-[100px]">상태</TableHead> + + {/* 동적으로 속성 열 헤더 생성 */} + {columnsToDisplay.length > 0 ? ( + columnsToDisplay.map(col => ( + <TableHead key={col.key} className="min-w-[150px]"> + {columnLabelMap[col.key] || col.key} + {columnUomMap[col.key] && ( + <span className="text-xs text-muted-foreground ml-1"> + ({columnUomMap[col.key]}) + </span> + )} + </TableHead> + )) + ) : ( + <TableHead> + <div className="flex items-center justify-center text-muted-foreground"> + <EyeOff className="h-4 w-4 mr-2" /> + <span>차이가 있는 항목이 없습니다</span> + </div> + </TableHead> + )} + </TableRow> + </TableHeader> + <TableBody> + {filteredResults.map((result) => ( <TableRow key={result.tagNo} className={!result.isMatching ? "bg-muted/30" : ""}> - <TableCell className="font-medium">{result.tagNo}</TableCell> - <TableCell>{result.tagDesc}</TableCell> - <TableCell> + <TableCell className="sticky left-0 z-10 bg-background font-medium"> + {result.tagNo} + </TableCell> + <TableCell className="sticky left-[180px] z-10 bg-background"> + {result.tagDesc} + </TableCell> + <TableCell className="sticky left-[380px] z-10 bg-background"> {result.isMatching ? ( <Badge variant="default" className="flex items-center gap-1"> <CheckCircle className="h-3 w-3" /> @@ -326,30 +457,44 @@ export function SEDPCompareDialog({ </Badge> )} </TableCell> - <TableCell> - {differences.length > 0 ? ( - <div className="space-y-1"> - {differences.map((diff) => ( - <div key={diff.key} className="text-sm"> - <span className="font-medium">{diff.label}: </span> - <span className="line-through text-red-500 mr-2"> - <DisplayValue value={diff.localValue} uom={diff.uom} isSedp={false} /> - </span> - <span className="text-green-500"> - <DisplayValue value={diff.sedpValue} uom={diff.uom} isSedp={true} /> - </span> - </div> - ))} - </div> - ) : ( + + {/* 각 속성에 대한 셀 동적 생성 */} + {columnsToDisplay.length > 0 ? ( + columnsToDisplay.map(col => { + const attr = result.attributes.find(a => a.key === col.key); + + if (!attr) return <TableCell key={col.key}>-</TableCell>; + + return ( + <TableCell + key={col.key} + className={!attr.isMatching ? "bg-muted/50" : ""} + > + {attr.isMatching ? ( + <DisplayValue value={attr.localValue} uom={attr.uom} /> + ) : ( + <div className="flex flex-col gap-1"> + <div className="line-through text-red-500"> + <DisplayValue value={attr.localValue} uom={attr.uom} isSedp={false} /> + </div> + <div className="text-green-500"> + <DisplayValue value={attr.sedpValue} uom={attr.uom} isSedp={true} /> + </div> + </div> + )} + </TableCell> + ); + }) + ) : ( + <TableCell> <span className="text-muted-foreground">모든 값이 일치합니다</span> - )} - </TableCell> + </TableCell> + )} </TableRow> - ); - })} - </TableBody> - </Table> + ))} + </TableBody> + </Table> + </div> ) : ( <div className="flex items-center justify-center h-full text-muted-foreground"> 현재 필터에 맞는 태그가 없습니다 @@ -357,12 +502,13 @@ export function SEDPCompareDialog({ )} </TabsContent> </Tabs> - + <DialogFooter className="flex justify-between items-center gap-4 pt-4 border-t"> <ExcelDownload comparisonResults={comparisonResults} + missingTags={missingTags} formCode={formCode} - disabled={isLoading || nonMatchingTags === 0} + disabled={isLoading || (nonMatchingTags === 0 && totalMissingTags === 0)} /> <Button onClick={onClose}>닫기</Button> </DialogFooter> |
