From 4b76297a7b9f36fdbffe58b152e5ba418b0e6237 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 12 May 2025 11:32:59 +0000 Subject: (대표님) S-EDP 관련 components/form-data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data/form-data-table-columns.tsx | 50 ++- components/form-data/sedp-compare-dialog.tsx | 434 +++++++++++++++-------- components/form-data/sedp-excel-download.tsx | 213 +++++++---- components/form-data/update-form-sheet.tsx | 82 ++++- 4 files changed, 554 insertions(+), 225 deletions(-) (limited to 'components/form-data') 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({ 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 ( -
{cellValue ? Number(cellValue).toLocaleString() : ""}
+
+ {cellValue ? Number(cellValue).toLocaleString() : ""} +
); // case "date": @@ -101,11 +119,27 @@ export function getColumns({ case "LIST": // 예: select인 경우 label만 표시 - return
{String(cellValue ?? "")}
; + return ( +
+ {String(cellValue ?? "")} +
+ ); case "STRING": default: - return
{String(cellValue ?? "")}
; + return ( +
+ {String(cellValue ?? "")} +
+ ); } }, })); @@ -127,7 +161,15 @@ export function getColumns({ setRowAction({ row, type: "update" })} + onSelect={() => { + // 행에 있는 모든 필드가 읽기 전용인지 확인할 수도 있습니다 (선택 사항) + // const allColumnsReadOnly = columnsJSON.every(col => col.shi === true); + // if(allColumnsReadOnly) { + // toast.info("이 항목은 읽기 전용입니다."); + // return; + // } + setRowAction({ row, type: "update" }); + }} > Edit 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 (empty); } - + // SEDP 값은 UOM을 표시하지 않음 (이미 포함되어 있다고 가정) if (isSedp) { return {value}; } - + // 로컬 값은 UOM과 함께 표시 return ( @@ -88,27 +89,72 @@ export function SEDPCompareDialog({ const [comparisonResults, setComparisonResults] = React.useState([]); 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 = {}; const uomMap: Record = {}; - + 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(); + 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 ( - - SEDP 데이터 비교 + SEDP 데이터 비교 +
+
+ + +
- - {matchingTags} / {totalTags} 일치 + + {matchingTags} / {totalTags} 일치 {totalMissingTags > 0 ? `(${totalMissingTags} 누락)` : ''} -
- +
- + {/* 범례 추가 */}
- + 전체 태그 ({totalTags}) 차이 있음 ({nonMatchingTags}) 일치함 ({matchingTags}) + 0 ? "text-red-500" : ""}> + 누락된 태그 ({totalMissingTags}) + - + {isLoading ? (
데이터 비교 중...
+ ) : activeTab === "missing" ? ( + // Missing tags tab content +
+ {missingTags.localOnly.length > 0 && ( +
+

로컬에만 있는 태그 ({missingTags.localOnly.length})

+ + + + Tag Number + Tag Description + + + + {missingTags.localOnly.map((tag) => ( + + {tag.tagNo} + {tag.tagDesc} + + ))} + +
+
+ )} + + {missingTags.sedpOnly.length > 0 && ( +
+

SEDP에만 있는 태그 ({missingTags.sedpOnly.length})

+ + + + Tag Number + Tag Description + + + + {missingTags.sedpOnly.map((tag) => ( + + {tag.tagNo} + {tag.tagDesc} + + ))} + +
+
+ )} + + {totalMissingTags === 0 && ( +
+ 모든 태그가 양쪽 시스템에 존재합니다 +
+ )} +
) : filteredResults.length > 0 ? ( - - - - Tag Number - Tag Description - 상태 - 차이점 - - - - {filteredResults.map((result) => { - // Find differences to display - const differences = result.attributes.filter(attr => !attr.isMatching); - - return ( + // 개선된 테이블 구조 +
+
+ + + Tag Number + Tag Description + 상태 + + {/* 동적으로 속성 열 헤더 생성 */} + {columnsToDisplay.length > 0 ? ( + columnsToDisplay.map(col => ( + + {columnLabelMap[col.key] || col.key} + {columnUomMap[col.key] && ( + + ({columnUomMap[col.key]}) + + )} + + )) + ) : ( + +
+ + 차이가 있는 항목이 없습니다 +
+
+ )} +
+
+ + {filteredResults.map((result) => ( - {result.tagNo} - {result.tagDesc} - + + {result.tagNo} + + + {result.tagDesc} + + {result.isMatching ? ( @@ -326,30 +457,44 @@ export function SEDPCompareDialog({ )} - - {differences.length > 0 ? ( -
- {differences.map((diff) => ( -
- {diff.label}: - - - - - - -
- ))} -
- ) : ( + + {/* 각 속성에 대한 셀 동적 생성 */} + {columnsToDisplay.length > 0 ? ( + columnsToDisplay.map(col => { + const attr = result.attributes.find(a => a.key === col.key); + + if (!attr) return -; + + return ( + + {attr.isMatching ? ( + + ) : ( +
+
+ +
+
+ +
+
+ )} +
+ ); + }) + ) : ( + 모든 값이 일치합니다 - )} - +
+ )}
- ); - })} -
-
+ ))} + + + ) : (
현재 필터에 맞는 태그가 없습니다 @@ -357,12 +502,13 @@ export function SEDPCompareDialog({ )} - + 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 (