diff options
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/import-excel-form.tsx | 34 | ||||
| -rw-r--r-- | components/form-data/sedp-compare-dialog.tsx | 317 |
2 files changed, 228 insertions, 123 deletions
diff --git a/components/form-data/import-excel-form.tsx b/components/form-data/import-excel-form.tsx index fd9adc1c..d425a909 100644 --- a/components/form-data/import-excel-form.tsx +++ b/components/form-data/import-excel-form.tsx @@ -49,8 +49,14 @@ export async function importExcelData({ try { if (onPendingChange) onPendingChange(true); - // Get existing tag numbers + // Get existing tag numbers and create a map for quick lookup const existingTagNumbers = new Set(tableData.map((d) => d.TAG_NO)); + const existingDataMap = new Map<string, GenericData>(); + tableData.forEach(item => { + if (item.TAG_NO) { + existingDataMap.set(item.TAG_NO, item); + } + }); const workbook = new ExcelJS.Workbook(); // const arrayBuffer = await file.arrayBuffer(); @@ -130,12 +136,38 @@ export async function importExcelData({ let errorMessage = ""; const rowObj: Record<string, any> = {}; + + // Get the TAG_NO first to identify existing data + const tagNoColIndex = keyToIndexMap.get("TAG_NO"); + const tagNo = tagNoColIndex ? String(rowValues[tagNoColIndex] ?? "").trim() : ""; + const existingRowData = existingDataMap.get(tagNo); // Process each column columnsJSON.forEach((col) => { const colIndex = keyToIndexMap.get(col.key); if (colIndex === undefined) return; + // Check if this column should be ignored (col.shi === true) + if (col.shi === true) { + // Use existing value instead of Excel value + if (existingRowData && existingRowData[col.key] !== undefined) { + rowObj[col.key] = existingRowData[col.key]; + } else { + // If no existing data, use appropriate default + switch (col.type) { + case "NUMBER": + rowObj[col.key] = null; + break; + case "STRING": + case "LIST": + default: + rowObj[col.key] = ""; + break; + } + } + return; // Skip processing Excel value for this column + } + const cellValue = rowValues[colIndex] ?? ""; let stringVal = String(cellValue).trim(); diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx index 37fe18ed..3107193a 100644 --- a/components/form-data/sedp-compare-dialog.tsx +++ b/components/form-data/sedp-compare-dialog.tsx @@ -3,12 +3,14 @@ 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, EyeOff } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Loader, RefreshCw, AlertCircle, CheckCircle, Info, EyeOff, ChevronDown, ChevronRight, Search } 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"; +import { Card, CardContent } from "@/components/ui/card"; interface SEDPCompareDialogProps { isOpen: boolean; @@ -37,7 +39,7 @@ interface ComparisonResult { // 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 <span>(empty)</span>; + return <span className="text-muted-foreground italic">(empty)</span>; } // SEDP 값은 UOM을 표시하지 않음 (이미 포함되어 있다고 가정) @@ -54,7 +56,7 @@ const DisplayValue = ({ value, uom, isSedp = false }: { value: any; uom?: string ); }; -// 범례 컴포넌트 추가 +// 범례 컴포넌트 const ColorLegend = () => { return ( <div className="flex items-center gap-4 text-sm p-2 bg-muted/20 rounded"> @@ -76,6 +78,64 @@ const ColorLegend = () => { ); }; +// 확장 가능한 차이점 표시 컴포넌트 +const DifferencesCard = ({ + attributes, + columnLabelMap, + showOnlyDifferences +}: { + attributes: ComparisonResult['attributes']; + columnLabelMap: Record<string, string>; + showOnlyDifferences: boolean; +}) => { + const attributesToShow = showOnlyDifferences + ? attributes.filter(attr => !attr.isMatching) + : attributes; + + if (attributesToShow.length === 0) { + return ( + <div className="text-center text-muted-foreground py-4"> + 모든 속성이 일치합니다 + </div> + ); + } + + return ( + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 p-4"> + {attributesToShow.map((attr) => ( + <Card key={attr.key} className={`${!attr.isMatching ? 'border-red-200' : 'border-green-200'}`}> + <CardContent className="p-3"> + <div className="font-medium text-sm mb-2 truncate" title={attr.label}> + {attr.label} + {attr.uom && <span className="text-xs text-muted-foreground ml-1">({attr.uom})</span>} + </div> + {attr.isMatching ? ( + <div className="text-sm"> + <DisplayValue value={attr.localValue} uom={attr.uom} /> + </div> + ) : ( + <div className="space-y-2"> + <div className="flex items-center gap-2 text-sm"> + <span className="text-xs text-muted-foreground min-w-[35px]">로컬:</span> + <span className="line-through text-red-500 flex-1"> + <DisplayValue value={attr.localValue} uom={attr.uom} isSedp={false} /> + </span> + </div> + <div className="flex items-center gap-2 text-sm"> + <span className="text-xs text-muted-foreground min-w-[35px]">SEDP:</span> + <span className="text-green-500 flex-1"> + <DisplayValue value={attr.sedpValue} uom={attr.uom} isSedp={true} /> + </span> + </div> + </div> + )} + </CardContent> + </Card> + ))} + </div> + ); +}; + export function SEDPCompareDialog({ isOpen, onClose, @@ -92,11 +152,10 @@ export function SEDPCompareDialog({ const [missingTags, setMissingTags] = React.useState<{ localOnly: { tagNo: string; tagDesc: string }[]; sedpOnly: { tagNo: string; tagDesc: string }[]; - }>( - { localOnly: [], sedpOnly: [] } - ); - // 추가: 차이점만 표시하는 옵션 + }>({ localOnly: [], sedpOnly: [] }); const [showOnlyDifferences, setShowOnlyDifferences] = React.useState(true); + const [searchTerm, setSearchTerm] = React.useState(""); + const [expandedRows, setExpandedRows] = React.useState<Set<string>>(new Set()); // Stats for summary const totalTags = comparisonResults.length; @@ -119,41 +178,56 @@ export function SEDPCompareDialog({ return { columnLabelMap: labelMap, columnUomMap: uomMap }; }, [columnsJSON]); - // Filter results based on active tab + // Filter and search results const filteredResults = React.useMemo(() => { + let results = comparisonResults; + + // Filter by tab switch (activeTab) { case "matching": - return comparisonResults.filter(r => r.isMatching); + results = results.filter(r => r.isMatching); + break; case "differences": - return comparisonResults.filter(r => !r.isMatching); + results = results.filter(r => !r.isMatching); + break; case "all": default: - return comparisonResults; + break; + } + + // Apply search filter + if (searchTerm.trim()) { + const search = searchTerm.toLowerCase(); + results = results.filter(r => + r.tagNo.toLowerCase().includes(search) || + r.tagDesc.toLowerCase().includes(search) + ); } - }, [comparisonResults, activeTab]); - // 변경: 표시할 컬럼 결정 (차이가 있는 컬럼만 or 모든 컬럼) - const columnsToDisplay = React.useMemo(() => { - // 기본 컬럼 (TAG_NO, TAG_DESC 제외) - const columns = columnsJSON.filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC"); + return results; + }, [comparisonResults, activeTab, searchTerm]); - if (!showOnlyDifferences) { - return columns; // 모든 컬럼 표시 + // Toggle row expansion + const toggleRowExpansion = (tagNo: string) => { + const newExpanded = new Set(expandedRows); + if (newExpanded.has(tagNo)) { + newExpanded.delete(tagNo); + } else { + newExpanded.add(tagNo); } + setExpandedRows(newExpanded); + }; - // 하나라도 차이가 있는 속성만 필터링 - const columnsWithDifferences = new Set<string>(); - comparisonResults.forEach(result => { - result.attributes.forEach(attr => { - if (!attr.isMatching) { - columnsWithDifferences.add(attr.key); - } + // Auto-expand rows with differences when switching to differences tab + React.useEffect(() => { + if (activeTab === "differences") { + const newExpanded = new Set<string>(); + filteredResults.filter(r => !r.isMatching).forEach(r => { + newExpanded.add(r.tagNo); }); - }); - - // 차이가 있는 컬럼만 반환 - return columns.filter(col => columnsWithDifferences.has(col.key)); - }, [columnsJSON, comparisonResults, showOnlyDifferences]); + setExpandedRows(newExpanded); + } + }, [activeTab, filteredResults]); const fetchAndCompareData = React.useCallback(async () => { if (!projectCode || !formCode) { @@ -294,20 +368,34 @@ export function SEDPCompareDialog({ return ( <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col"> + <DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col"> <DialogHeader> <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 className="flex items-center gap-4"> + <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="relative"> + <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" /> + <Input + placeholder="태그 번호 또는 설명 검색..." + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + className="pl-8 w-64" + /> + </div> </div> + <div className="flex items-center gap-2"> <Badge variant={matchingTags === totalTags && totalMissingTags === 0 ? "default" : "destructive"}> {matchingTags} / {totalTags} 일치 {totalMissingTags > 0 ? `(${totalMissingTags} 누락)` : ''} @@ -329,7 +417,7 @@ export function SEDPCompareDialog({ </div> </DialogHeader> - {/* 범례 추가 */} + {/* 범례 */} <div className="mb-4"> <ColorLegend /> </div> @@ -357,7 +445,7 @@ export function SEDPCompareDialog({ <div> <h3 className="text-sm font-medium mb-2">로컬에만 있는 태그 ({missingTags.localOnly.length})</h3> <Table> - <TableHeader> + <TableHeader className="sticky top-0 bg-background z-10"> <TableRow> <TableHead className="w-[180px]">Tag Number</TableHead> <TableHead>Tag Description</TableHead> @@ -379,7 +467,7 @@ export function SEDPCompareDialog({ <div> <h3 className="text-sm font-medium mb-2">SEDP에만 있는 태그 ({missingTags.sedpOnly.length})</h3> <Table> - <TableHeader> + <TableHeader className="sticky top-0 bg-background z-10"> <TableRow> <TableHead className="w-[180px]">Tag Number</TableHead> <TableHead>Tag Description</TableHead> @@ -404,100 +492,85 @@ export function SEDPCompareDialog({ )} </div> ) : filteredResults.length > 0 ? ( - // 개선된 테이블 구조 - <div className="overflow-x-auto"> + // 개선된 확장 가능한 테이블 구조 + <div className="border rounded-md"> <Table> - <TableHeader> + <TableHeader className="sticky top-0 bg-muted/50 z-10"> <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> - )} + <TableHead className="w-12"></TableHead> + <TableHead className="w-[180px]">Tag Number</TableHead> + <TableHead className="w-[250px]">Tag Description</TableHead> + <TableHead className="w-[120px]">상태</TableHead> + <TableHead>차이점 개수</TableHead> </TableRow> </TableHeader> <TableBody> {filteredResults.map((result) => ( - <TableRow key={result.tagNo} className={!result.isMatching ? "bg-muted/30" : ""}> - <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" /> - <span>일치</span> - </Badge> - ) : ( - <Badge variant="destructive" className="flex items-center gap-1"> - <AlertCircle className="h-3 w-3" /> - <span>차이 있음</span> - </Badge> - )} - </TableCell> - - {/* 각 속성에 대한 셀 동적 생성 */} - {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> - ); - }) - ) : ( + <React.Fragment key={result.tagNo}> + {/* 메인 행 */} + <TableRow + className={`hover:bg-muted/20 cursor-pointer ${!result.isMatching ? "bg-muted/10" : ""}`} + onClick={() => toggleRowExpansion(result.tagNo)} + > <TableCell> - <span className="text-muted-foreground">모든 값이 일치합니다</span> + {result.attributes.some(attr => !attr.isMatching) ? ( + expandedRows.has(result.tagNo) ? ( + <ChevronDown className="h-4 w-4" /> + ) : ( + <ChevronRight className="h-4 w-4" /> + ) + ) : null} + </TableCell> + <TableCell className="font-medium"> + {result.tagNo} </TableCell> + <TableCell title={result.tagDesc}> + <div className="truncate"> + {result.tagDesc} + </div> + </TableCell> + <TableCell> + {result.isMatching ? ( + <Badge variant="default" className="flex items-center gap-1"> + <CheckCircle className="h-3 w-3" /> + <span>일치</span> + </Badge> + ) : ( + <Badge variant="destructive" className="flex items-center gap-1"> + <AlertCircle className="h-3 w-3" /> + <span>차이</span> + </Badge> + )} + </TableCell> + <TableCell> + {!result.isMatching && ( + <span className="text-sm text-muted-foreground"> + {result.attributes.filter(attr => !attr.isMatching).length}개 속성이 다름 + </span> + )} + </TableCell> + </TableRow> + + {/* 확장된 차이점 표시 행 */} + {expandedRows.has(result.tagNo) && ( + <TableRow> + <TableCell colSpan={5} className="p-0 bg-muted/5"> + <DifferencesCard + attributes={result.attributes} + columnLabelMap={columnLabelMap} + showOnlyDifferences={showOnlyDifferences} + /> + </TableCell> + </TableRow> )} - </TableRow> + </React.Fragment> ))} </TableBody> </Table> </div> ) : ( <div className="flex items-center justify-center h-full text-muted-foreground"> - 현재 필터에 맞는 태그가 없습니다 + {searchTerm ? "검색 결과가 없습니다" : "현재 필터에 맞는 태그가 없습니다"} </div> )} </TabsContent> |
