summaryrefslogtreecommitdiff
path: root/components/form-data/sedp-compare-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data/sedp-compare-dialog.tsx')
-rw-r--r--components/form-data/sedp-compare-dialog.tsx434
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>