summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-12 11:32:59 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-12 11:32:59 +0000
commit4b76297a7b9f36fdbffe58b152e5ba418b0e6237 (patch)
tree782652a769832729174e2a5a796febb644fe735b /components
parent20b1a8e6e39b3adf058b32f1b2e219ee93a9f1c7 (diff)
(대표님) S-EDP 관련 components/form-data
Diffstat (limited to 'components')
-rw-r--r--components/form-data/form-data-table-columns.tsx50
-rw-r--r--components/form-data/sedp-compare-dialog.tsx434
-rw-r--r--components/form-data/sedp-excel-download.tsx213
-rw-r--r--components/form-data/update-form-sheet.tsx82
4 files changed, 554 insertions, 225 deletions
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
index 4db3a724..a1fbcae1 100644
--- a/components/form-data/form-data-table-columns.tsx
+++ b/components/form-data/form-data-table-columns.tsx
@@ -38,6 +38,8 @@ export interface DataTableColumnJSON {
options?: string[];
uom?: string;
uomId?: string;
+ shi?: boolean;
+
}
/**
* getColumns 함수에 필요한 props
@@ -78,17 +80,33 @@ export function getColumns<TData extends object>({
minWidth: 80,
paddingFactor: 1.2,
maxWidth: col.key === "TAG_NO" ? 120 : 150,
+ isReadOnly: col.shi === true, // shi 정보를 메타데이터에 저장
},
// (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능
cell: ({ row }) => {
const cellValue = row.getValue(col.key);
+
+ // shi 속성이 true인 경우 적용할 스타일
+ const isReadOnly = col.shi === true;
+ const readOnlyClass = isReadOnly ? "read-only-cell" : "";
+
+ // 읽기 전용 셀의 스타일 (인라인 스타일과 클래스 동시 적용)
+ const cellStyle = isReadOnly
+ ? { backgroundColor: '#f5f5f5', color: '#666', cursor: 'not-allowed' }
+ : {};
// 데이터 타입별 처리
switch (col.type) {
case "NUMBER":
// 예: number인 경우 콤마 등 표시
return (
- <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div>
+ <div
+ className={readOnlyClass}
+ style={cellStyle}
+ title={isReadOnly ? "읽기 전용 필드입니다" : ""}
+ >
+ {cellValue ? Number(cellValue).toLocaleString() : ""}
+ </div>
);
// case "date":
@@ -101,11 +119,27 @@ export function getColumns<TData extends object>({
case "LIST":
// 예: select인 경우 label만 표시
- return <div>{String(cellValue ?? "")}</div>;
+ return (
+ <div
+ className={readOnlyClass}
+ style={cellStyle}
+ title={isReadOnly ? "읽기 전용 필드입니다" : ""}
+ >
+ {String(cellValue ?? "")}
+ </div>
+ );
case "STRING":
default:
- return <div>{String(cellValue ?? "")}</div>;
+ return (
+ <div
+ className={readOnlyClass}
+ style={cellStyle}
+ title={isReadOnly ? "읽기 전용 필드입니다" : ""}
+ >
+ {String(cellValue ?? "")}
+ </div>
+ );
}
},
}));
@@ -127,7 +161,15 @@ export function getColumns<TData extends object>({
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "update" })}
+ onSelect={() => {
+ // 행에 있는 모든 필드가 읽기 전용인지 확인할 수도 있습니다 (선택 사항)
+ // const allColumnsReadOnly = columnsJSON.every(col => col.shi === true);
+ // if(allColumnsReadOnly) {
+ // toast.info("이 항목은 읽기 전용입니다.");
+ // return;
+ // }
+ setRowAction({ row, type: "update" });
+ }}
>
Edit
</DropdownMenuItem>
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>
diff --git a/components/form-data/sedp-excel-download.tsx b/components/form-data/sedp-excel-download.tsx
index 70f5c46a..24e1003d 100644
--- a/components/form-data/sedp-excel-download.tsx
+++ b/components/form-data/sedp-excel-download.tsx
@@ -18,11 +18,15 @@ interface ExcelDownloadProps {
uom?: string;
}>;
}>;
+ missingTags: {
+ localOnly: Array<{ tagNo: string; tagDesc: string }>;
+ sedpOnly: Array<{ tagNo: string; tagDesc: string }>;
+ };
formCode: string;
disabled: boolean;
}
-export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDownloadProps) {
+export function ExcelDownload({ comparisonResults, missingTags, formCode, disabled }: ExcelDownloadProps) {
const [isExporting, setIsExporting] = React.useState(false);
// Function to generate and download Excel file with differences
@@ -32,8 +36,9 @@ export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDo
// Get only items with differences
const itemsWithDifferences = comparisonResults.filter(item => !item.isMatching);
+ const hasMissingTags = missingTags.localOnly.length > 0 || missingTags.sedpOnly.length > 0;
- if (itemsWithDifferences.length === 0) {
+ if (itemsWithDifferences.length === 0 && !hasMissingTags) {
toast.info("차이가 없어 다운로드할 내용이 없습니다");
return;
}
@@ -43,65 +48,122 @@ export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDo
workbook.creator = 'SEDP Compare Tool';
workbook.created = new Date();
- // Add a worksheet
- const worksheet = workbook.addWorksheet('SEDP Differences');
-
- // Add headers
- worksheet.columns = [
- { header: 'Tag Number', key: 'tagNo', width: 20 },
- { header: 'Tag Description', key: 'tagDesc', width: 30 },
- { header: 'Attribute', key: 'attribute', width: 25 },
- { header: 'Local Value', key: 'localValue', width: 20 },
- { header: 'SEDP Value', key: 'sedpValue', width: 20 }
- ];
-
- // Style the header row
- const headerRow = worksheet.getRow(1);
- headerRow.eachCell((cell) => {
- cell.font = { bold: true };
- cell.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFE0E0E0' }
- };
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- });
-
- // Add data rows
- let rowIndex = 2;
- itemsWithDifferences.forEach(item => {
- const differences = item.attributes.filter(attr => !attr.isMatching);
+ // Add a worksheet for attribute differences
+ if (itemsWithDifferences.length > 0) {
+ const worksheet = workbook.addWorksheet('속성 차이');
- if (differences.length === 0) return;
+ // Add headers
+ worksheet.columns = [
+ { header: 'Tag Number', key: 'tagNo', width: 20 },
+ { header: 'Tag Description', key: 'tagDesc', width: 30 },
+ { header: 'Attribute', key: 'attribute', width: 25 },
+ { header: 'Local Value', key: 'localValue', width: 20 },
+ { header: 'SEDP Value', key: 'sedpValue', width: 20 }
+ ];
- differences.forEach(diff => {
- const row = worksheet.getRow(rowIndex++);
+ // Style the header row
+ const headerRow = worksheet.getRow(1);
+ headerRow.eachCell((cell) => {
+ cell.font = { bold: true };
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ };
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+
+ // Add data rows
+ let rowIndex = 2;
+ itemsWithDifferences.forEach(item => {
+ const differences = item.attributes.filter(attr => !attr.isMatching);
+
+ if (differences.length === 0) return;
- // Format local value with UOM
- const localDisplay = diff.localValue === null || diff.localValue === undefined || diff.localValue === ''
- ? "(empty)"
- : diff.uom ? `${diff.localValue} ${diff.uom}` : diff.localValue;
+ differences.forEach(diff => {
+ const row = worksheet.getRow(rowIndex++);
+
+ // Format local value with UOM
+ const localDisplay = diff.localValue === null || diff.localValue === undefined || diff.localValue === ''
+ ? "(empty)"
+ : diff.uom ? `${diff.localValue} ${diff.uom}` : diff.localValue;
+
+ // SEDP value is displayed as-is
+ const sedpDisplay = diff.sedpValue === null || diff.sedpValue === undefined || diff.sedpValue === ''
+ ? "(empty)"
+ : diff.sedpValue;
+
+ // Set cell values
+ row.getCell('tagNo').value = item.tagNo;
+ row.getCell('tagDesc').value = item.tagDesc;
+ row.getCell('attribute').value = diff.label;
+ row.getCell('localValue').value = localDisplay;
+ row.getCell('sedpValue').value = sedpDisplay;
- // SEDP value is displayed as-is
- const sedpDisplay = diff.sedpValue === null || diff.sedpValue === undefined || diff.sedpValue === ''
- ? "(empty)"
- : diff.sedpValue;
+ // Style the row
+ row.getCell('localValue').font = { color: { argb: 'FFFF0000' } }; // Red for local value
+ row.getCell('sedpValue').font = { color: { argb: 'FF008000' } }; // Green for SEDP value
+
+ // Add borders
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ });
- // Set cell values
- row.getCell('tagNo').value = item.tagNo;
- row.getCell('tagDesc').value = item.tagDesc;
- row.getCell('attribute').value = diff.label;
- row.getCell('localValue').value = localDisplay;
- row.getCell('sedpValue').value = sedpDisplay;
+ // Add a blank row after each tag for better readability
+ rowIndex++;
+ });
+ }
+
+ // Add a worksheet for missing tags if there are any
+ if (hasMissingTags) {
+ const missingWorksheet = workbook.addWorksheet('누락된 태그');
+
+ // Add headers
+ missingWorksheet.columns = [
+ { header: 'Tag Number', key: 'tagNo', width: 20 },
+ { header: 'Tag Description', key: 'tagDesc', width: 30 },
+ { header: 'Status', key: 'status', width: 20 }
+ ];
+
+ // Style the header row
+ const headerRow = missingWorksheet.getRow(1);
+ headerRow.eachCell((cell) => {
+ cell.font = { bold: true };
+ cell.fill = {
+ type: 'pattern',
+ pattern: 'solid',
+ fgColor: { argb: 'FFE0E0E0' }
+ };
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+
+ // Add local-only tags
+ let rowIndex = 2;
+ missingTags.localOnly.forEach(tag => {
+ const row = missingWorksheet.getRow(rowIndex++);
- // Style the row
- row.getCell('localValue').font = { color: { argb: 'FFFF0000' } }; // Red for local value
- row.getCell('sedpValue').font = { color: { argb: 'FF008000' } }; // Green for SEDP value
+ row.getCell('tagNo').value = tag.tagNo;
+ row.getCell('tagDesc').value = tag.tagDesc;
+ row.getCell('status').value = '로컬에만 존재';
+
+ // Style the status cell
+ row.getCell('status').font = { color: { argb: 'FFFF8C00' } }; // Orange for local-only
// Add borders
row.eachCell((cell) => {
@@ -114,9 +176,33 @@ export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDo
});
});
- // Add a blank row after each tag for better readability
- rowIndex++;
- });
+ // Add a blank row
+ if (missingTags.localOnly.length > 0 && missingTags.sedpOnly.length > 0) {
+ rowIndex++;
+ }
+
+ // Add SEDP-only tags
+ missingTags.sedpOnly.forEach(tag => {
+ const row = missingWorksheet.getRow(rowIndex++);
+
+ row.getCell('tagNo').value = tag.tagNo;
+ row.getCell('tagDesc').value = tag.tagDesc;
+ row.getCell('status').value = 'SEDP에만 존재';
+
+ // Style the status cell
+ row.getCell('status').font = { color: { argb: 'FF0000FF' } }; // Blue for SEDP-only
+
+ // Add borders
+ row.eachCell((cell) => {
+ cell.border = {
+ top: { style: 'thin' },
+ left: { style: 'thin' },
+ bottom: { style: 'thin' },
+ right: { style: 'thin' }
+ };
+ });
+ });
+ }
// Generate Excel file
const buffer = await workbook.xlsx.writeBuffer();
@@ -128,7 +214,7 @@ export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDo
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
- a.download = `SEDP_Differences_${formCode}_${new Date().toISOString().slice(0, 10)}.xlsx`;
+ a.download = `SEDP_차이점_${formCode}_${new Date().toISOString().slice(0, 10)}.xlsx`;
document.body.appendChild(a);
a.click();
@@ -136,7 +222,7 @@ export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDo
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
- toast.success("차이점 Excel 다운로드 완료");
+ toast.success("Excel 다운로드 완료");
} catch (error) {
console.error("Error exporting to Excel:", error);
toast.error("Excel 다운로드 실패");
@@ -145,11 +231,16 @@ export function ExcelDownload({ comparisonResults, formCode, disabled }: ExcelDo
}
};
+ // Determine if there are any differences or missing tags
+ const hasDifferences = comparisonResults.some(item => !item.isMatching);
+ const hasMissingTags = missingTags && (missingTags.localOnly.length > 0 || missingTags.sedpOnly.length > 0);
+ const hasExportableContent = hasDifferences || hasMissingTags;
+
return (
<Button
variant="secondary"
onClick={handleExportDifferences}
- disabled={disabled || isExporting}
+ disabled={disabled || isExporting || !hasExportableContent}
className="flex items-center gap-2"
>
{isExporting ? (
diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx
index 27f426c1..6f2a4722 100644
--- a/components/form-data/update-form-sheet.tsx
+++ b/components/form-data/update-form-sheet.tsx
@@ -4,9 +4,9 @@ import * as React from "react";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
-import { Check, ChevronsUpDown, Loader } from "lucide-react";
+import { Check, ChevronsUpDown, Loader, LockIcon } from "lucide-react";
import { toast } from "sonner";
-import { useRouter } from "next/navigation"; // Add this import
+import { useRouter } from "next/navigation";
import {
Sheet,
@@ -26,6 +26,7 @@ import {
FormLabel,
FormControl,
FormMessage,
+ FormDescription,
} from "@/components/ui/form";
import {
Popover,
@@ -68,7 +69,7 @@ export function UpdateTagSheet({
...props
}: UpdateTagSheetProps) {
const [isPending, startTransition] = React.useTransition();
- const router = useRouter(); // Add router hook
+ const router = useRouter();
// 1) zod 스키마
const dynamicSchema = React.useMemo(() => {
@@ -114,10 +115,19 @@ export function UpdateTagSheet({
async function onSubmit(values: Record<string, any>) {
startTransition(async () => {
try {
+ // 제출 전에 읽기 전용 필드를 원본 값으로 복원
+ const finalValues = { ...values };
+ for (const col of columns) {
+ if (col.shi || col.key === "TAG_NO" || col.key === "TAG_DESC") {
+ // 읽기 전용 필드는 원본 값으로 복원
+ finalValues[col.key] = rowData?.[col.key] ?? "";
+ }
+ }
+
const { success, message } = await updateFormDataInDB(
formCode,
contractItemId,
- values
+ finalValues
);
if (!success) {
@@ -131,7 +141,7 @@ export function UpdateTagSheet({
// Create a merged object of original rowData and new values
const updatedData = {
...rowData,
- ...values,
+ ...finalValues,
TAG_NO: rowData?.TAG_NO,
};
@@ -157,7 +167,7 @@ export function UpdateTagSheet({
<SheetHeader className="text-left">
<SheetTitle>Update Row</SheetTitle>
<SheetDescription>
- Modify the fields below and save changes
+ Modify the fields below and save changes. Fields with <LockIcon className="inline h-3 w-3" /> are read-only.
</SheetDescription>
</SheetHeader>
@@ -169,8 +179,11 @@ export function UpdateTagSheet({
<div className="overflow-y-auto max-h-[80vh] flex-1 pr-4 -mr-4">
<div className="flex flex-col gap-4 pt-2">
{columns.map((col) => {
- const isTagNumberField =
- col.key === "TAG_NO" || col.key === "TAG_DESC";
+ // 읽기 전용 조건 업데이트: shi가 true이거나 TAG_NO/TAG_DESC인 경우
+ const isReadOnly = col.shi === true ||
+ col.key === "TAG_NO" ||
+ col.key === "TAG_DESC";
+
return (
<FormField
key={col.key}
@@ -181,18 +194,31 @@ export function UpdateTagSheet({
case "NUMBER":
return (
<FormItem>
- <FormLabel>{col.displayLabel}</FormLabel>
+ <FormLabel className="flex items-center">
+ {col.displayLabel || col.label}
+ {isReadOnly && (
+ <LockIcon className="ml-1 h-3 w-3 text-gray-400" />
+ )}
+ </FormLabel>
<FormControl>
<Input
type="number"
- readOnly={isTagNumberField}
+ readOnly={isReadOnly}
onChange={(e) => {
const num = parseFloat(e.target.value);
field.onChange(isNaN(num) ? "" : num);
}}
value={field.value ?? ""}
+ className={cn(
+ isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300"
+ )}
/>
</FormControl>
+ {isReadOnly && col.shi && (
+ <FormDescription className="text-xs text-gray-500">
+ This field is read-only
+ </FormDescription>
+ )}
<FormMessage />
</FormItem>
);
@@ -200,16 +226,22 @@ export function UpdateTagSheet({
case "LIST":
return (
<FormItem>
- <FormLabel>{col.label}</FormLabel>
+ <FormLabel className="flex items-center">
+ {col.label}
+ {isReadOnly && (
+ <LockIcon className="ml-1 h-3 w-3 text-gray-400" />
+ )}
+ </FormLabel>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
- disabled={isTagNumberField}
+ disabled={isReadOnly}
className={cn(
"w-full justify-between",
- !field.value && "text-muted-foreground"
+ !field.value && "text-muted-foreground",
+ isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300"
)}
>
{field.value
@@ -246,6 +278,11 @@ export function UpdateTagSheet({
</Command>
</PopoverContent>
</Popover>
+ {isReadOnly && col.shi && (
+ <FormDescription className="text-xs text-gray-500">
+ This field is read-only
+ </FormDescription>
+ )}
<FormMessage />
</FormItem>
);
@@ -257,7 +294,7 @@ export function UpdateTagSheet({
// <FormControl>
// <Input
// type="date"
- // readOnly={isTagNumberField}
+ // readOnly={isReadOnly}
// onChange={field.onChange}
// value={field.value ?? ""}
// />
@@ -270,13 +307,26 @@ export function UpdateTagSheet({
default:
return (
<FormItem>
- <FormLabel>{col.label}</FormLabel>
+ <FormLabel className="flex items-center">
+ {col.label}
+ {isReadOnly && (
+ <LockIcon className="ml-1 h-3 w-3 text-gray-400" />
+ )}
+ </FormLabel>
<FormControl>
<Input
- readOnly={isTagNumberField}
+ readOnly={isReadOnly}
{...field}
+ className={cn(
+ isReadOnly && "bg-gray-100 text-gray-600 cursor-not-allowed border-gray-300"
+ )}
/>
</FormControl>
+ {isReadOnly && col.shi && (
+ <FormDescription className="text-xs text-gray-500">
+ This field is read-only
+ </FormDescription>
+ )}
<FormMessage />
</FormItem>
);