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