From 37611339fea096e47aaa42311a13a6313b4200db Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 2 Jun 2025 02:27:28 +0000
Subject: (대표님) 20250602 오전 작업사항 (코드프리징)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/form-data/export-excel-form.tsx | 321 +++++++++++++++++++++++++++--
 1 file changed, 304 insertions(+), 17 deletions(-)

(limited to 'components/form-data/export-excel-form.tsx')

diff --git a/components/form-data/export-excel-form.tsx b/components/form-data/export-excel-form.tsx
index d0ccf980..07d3c447 100644
--- a/components/form-data/export-excel-form.tsx
+++ b/components/form-data/export-excel-form.tsx
@@ -13,6 +13,7 @@ export interface DataTableColumnJSON {
   type: ColumnType;
   options?: string[];
   shi?: boolean; // SHI-only field indicator
+  required?: boolean; // Required field indicator
   // Add any other properties that might be in columnsJSON
 }
 
@@ -22,18 +23,249 @@ export interface GenericData {
   TAG_NO?: string; // Since TAG_NO seems important in the code
 }
 
+// Define error structure
+export interface DataError {
+  tagNo: string;
+  rowIndex: number;
+  columnKey: string;
+  columnLabel: string;
+  errorType: string;
+  errorMessage: string;
+  currentValue?: any;
+  expectedFormat?: string;
+}
+
 // Define the options interface for the export function
 export interface ExportExcelOptions {
   tableData: GenericData[];
   columnsJSON: DataTableColumnJSON[];
   formCode: string;
   onPendingChange?: (isPending: boolean) => void;
+  validateData?: boolean; // Option to enable/disable data validation
 }
 
 // Define the return type
 export interface ExportExcelResult {
   success: boolean;
   error?: any;
+  errorCount?: number;
+  hasErrors?: boolean;
+}
+
+/**
+ * Validate data and collect errors
+ */
+function validateTableData(
+  tableData: GenericData[],
+  columnsJSON: DataTableColumnJSON[]
+): DataError[] {
+  const errors: DataError[] = [];
+  const tagNoSet = new Set<string>();
+
+  tableData.forEach((rowData, index) => {
+    const rowIndex = index + 2; // Excel row number (header is row 1)
+    const tagNo = rowData.TAG_NO || `Row-${rowIndex}`;
+
+    // Check for duplicate TAG_NO
+    if (rowData.TAG_NO) {
+      if (tagNoSet.has(rowData.TAG_NO)) {
+        errors.push({
+          tagNo,
+          rowIndex,
+          columnKey: "TAG_NO",
+          columnLabel: "TAG NO",
+          errorType: "DUPLICATE",
+          errorMessage: "Duplicate TAG_NO found",
+          currentValue: rowData.TAG_NO,
+        });
+      } else {
+        tagNoSet.add(rowData.TAG_NO);
+      }
+    }
+
+    // Validate each column
+    columnsJSON.forEach((column) => {
+      const value = rowData[column.key];
+      const isEmpty = value === undefined || value === null || value === "";
+
+      // Required field validation
+      if (column.required && isEmpty) {
+        errors.push({
+          tagNo,
+          rowIndex,
+          columnKey: column.key,
+          columnLabel: column.label,
+          errorType: "REQUIRED",
+          errorMessage: "Required field is empty",
+          currentValue: value,
+        });
+      }
+
+      if (!isEmpty) {
+        // Type validation
+        switch (column.type) {
+          case "NUMBER":
+            if (isNaN(Number(value))) {
+              errors.push({
+                tagNo,
+                rowIndex,
+                columnKey: column.key,
+                columnLabel: column.label,
+                errorType: "TYPE_MISMATCH",
+                errorMessage: "Value is not a valid number",
+                currentValue: value,
+                expectedFormat: "Number",
+              });
+            }
+            break;
+
+          case "LIST":
+            if (column.options && !column.options.includes(String(value))) {
+              errors.push({
+                tagNo,
+                rowIndex,
+                columnKey: column.key,
+                columnLabel: column.label,
+                errorType: "INVALID_OPTION",
+                errorMessage: "Value is not in the allowed options list",
+                currentValue: value,
+                expectedFormat: column.options.join(", "),
+              });
+            }
+            break;
+
+          case "STRING":
+            // Additional string validations can be added here
+            if (typeof value !== "string" && typeof value !== "number") {
+              errors.push({
+                tagNo,
+                rowIndex,
+                columnKey: column.key,
+                columnLabel: column.label,
+                errorType: "TYPE_MISMATCH",
+                errorMessage: "Value is not a valid string",
+                currentValue: value,
+                expectedFormat: "String",
+              });
+            }
+            break;
+        }
+      }
+    });
+  });
+
+  return errors;
+}
+
+/**
+ * Create error sheet with validation results
+ */
+function createErrorSheet(workbook: ExcelJS.Workbook, errors: DataError[]) {
+  const errorSheet = workbook.addWorksheet("Errors");
+
+  // Error sheet headers
+  const errorHeaders = [
+    "TAG NO",
+    "Row Number",
+    "Column",
+    "Error Type",
+    "Error Message",
+    "Current Value",
+    "Expected Format",
+  ];
+
+  errorSheet.addRow(errorHeaders);
+
+  // Style error sheet header
+  const errorHeaderRow = errorSheet.getRow(1);
+  errorHeaderRow.font = { bold: true, color: { argb: "FFFFFFFF" } };
+  errorHeaderRow.alignment = { horizontal: "center" };
+  
+  errorHeaderRow.eachCell((cell) => {
+    cell.fill = {
+      type: "pattern",
+      pattern: "solid",
+      fgColor: { argb: "FFDC143C" }, // Crimson background
+    };
+  });
+
+  // Add error data
+  errors.forEach((error) => {
+    const errorRow = errorSheet.addRow([
+      error.tagNo,
+      error.rowIndex,
+      error.columnLabel,
+      error.errorType,
+      error.errorMessage,
+      error.currentValue || "",
+      error.expectedFormat || "",
+    ]);
+
+    // Color code by error type
+    errorRow.eachCell((cell, colNumber) => {
+      let bgColor = "FFFFFFFF"; // Default white
+      
+      switch (error.errorType) {
+        case "REQUIRED":
+          bgColor = "FFFFCCCC"; // Light red
+          break;
+        case "TYPE_MISMATCH":
+          bgColor = "FFFFEECC"; // Light orange
+          break;
+        case "INVALID_OPTION":
+          bgColor = "FFFFFFE0"; // Light yellow
+          break;
+        case "DUPLICATE":
+          bgColor = "FFFFE0E0"; // Very light red
+          break;
+      }
+
+      cell.fill = {
+        type: "pattern",
+        pattern: "solid",
+        fgColor: { argb: bgColor },
+      };
+    });
+  });
+
+  // Auto-fit columns
+  errorSheet.columns.forEach((column) => {
+    let maxLength = 0;
+    column.eachCell({ includeEmpty: false }, (cell) => {
+      const columnLength = String(cell.value).length;
+      if (columnLength > maxLength) {
+        maxLength = columnLength;
+      }
+    });
+    column.width = Math.min(Math.max(maxLength + 2, 10), 50);
+  });
+
+  // Add summary at the top
+  errorSheet.insertRow(1, [`Total Errors Found: ${errors.length}`]);
+  const summaryRow = errorSheet.getRow(1);
+  summaryRow.font = { bold: true, size: 14 };
+  if (errors.length > 0) {
+    summaryRow.getCell(1).fill = {
+      type: "pattern",
+      pattern: "solid",
+      fgColor: { argb: "FFFFC0C0" }, // Light red background
+    };
+  }
+
+  // Adjust header row number
+  const newHeaderRow = errorSheet.getRow(2);
+  newHeaderRow.font = { bold: true, color: { argb: "FFFFFFFF" } };
+  newHeaderRow.alignment = { horizontal: "center" };
+  
+  newHeaderRow.eachCell((cell) => {
+    cell.fill = {
+      type: "pattern",
+      pattern: "solid",
+      fgColor: { argb: "FFDC143C" },
+    };
+  });
+
+  return errorSheet;
 }
 
 /**
@@ -45,11 +277,15 @@ export async function exportExcelData({
   tableData,
   columnsJSON,
   formCode,
-  onPendingChange
+  onPendingChange,
+  validateData = true
 }: ExportExcelOptions): Promise<ExportExcelResult> {
   try {
     if (onPendingChange) onPendingChange(true);
 
+    // Validate data first if validation is enabled
+    const errors = validateData ? validateTableData(tableData, columnsJSON) : [];
+
     // Create a new workbook
     const workbook = new ExcelJS.Workbook();
 
@@ -92,7 +328,13 @@ export async function exportExcelData({
     });
 
     // 2. 데이터 시트에 헤더 추가
-    const headers = columnsJSON.map((col) => col.label);
+    const headers = columnsJSON.map((col) => {
+      let headerLabel = col.label;
+      if (col.required) {
+        headerLabel += " *"; // Required fields marked with asterisk
+      }
+      return headerLabel;
+    });
     worksheet.addRow(headers);
 
     // 헤더 스타일 적용
@@ -106,13 +348,21 @@ export async function exportExcelData({
       const column = columnsJSON[columnIndex];
       
       if (column?.shi === true) {
-        // SHI-only 필드는 더 진한 음영으로 표시 (헤더 라벨은 원본 유지)
+        // SHI-only 필드는 더 진한 음영으로 표시
         cell.fill = {
           type: "pattern",
           pattern: "solid",
           fgColor: { argb: "FFFF9999" }, // 연한 빨간색 배경
         };
         cell.font = { bold: true, color: { argb: "FF800000" } }; // 진한 빨간색 글자
+      } else if (column?.required) {
+        // Required 필드는 파란색 배경
+        cell.fill = {
+          type: "pattern",
+          pattern: "solid",
+          fgColor: { argb: "FFCCE5FF" }, // 연한 파란색 배경
+        };
+        cell.font = { bold: true, color: { argb: "FF000080" } }; // 진한 파란색 글자
       } else {
         // 일반 필드는 기존 스타일
         cell.fill = {
@@ -131,20 +381,34 @@ export async function exportExcelData({
       });
       const dataRow = worksheet.addRow(rowValues);
       
+      // Get errors for this row
+      const rowErrors = errors.filter(err => err.rowIndex === rowIndex + 2);
+      const hasErrors = rowErrors.length > 0;
+      
       // SHI-only 컬럼의 데이터 셀에도 음영 적용
       dataRow.eachCell((cell, colNumber) => {
         const columnIndex = colNumber - 1;
         const column = columnsJSON[columnIndex];
         
+        // Check if this cell has errors
+        const cellHasError = rowErrors.some(err => err.columnKey === column.key);
+        
         if (column?.shi === true) {
           // SHI-only 필드의 데이터 셀에 연한 음영 적용
           cell.fill = {
             type: "pattern",
             pattern: "solid",
-            fgColor: { argb: "FFFFCCCC" }, // 매우 연한 빨간색 배경
+            fgColor: { argb: cellHasError ? "FFFF6666" : "FFFFCCCC" }, // 에러가 있으면 더 진한 빨간색
           };
-          // 읽기 전용임을 나타내기 위해 이탤릭 적용
           cell.font = { italic: true, color: { argb: "FF666666" } };
+        } else if (cellHasError) {
+          // 에러가 있는 셀은 연한 빨간색 배경
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "FFFFDDDD" },
+          };
+          cell.font = { color: { argb: "FFCC0000" } };
         }
       });
     });
@@ -162,7 +426,7 @@ export async function exportExcelData({
         // 유효성 검사 정의
         const validation = {
           type: "list" as const,
-          allowBlank: true,
+          allowBlank: !col.required,
           formulae: [validationRange],
           showErrorMessage: true,
           errorStyle: "warning" as const,
@@ -227,15 +491,28 @@ export async function exportExcelData({
       column.width = Math.min(Math.max(maxLength + 2, 10), 50);
     });
 
-    // 6. 범례 추가 (별도 시트)
+    // 6. 에러 시트 생성 (에러가 있을 경우에만)
+    if (errors.length > 0) {
+      createErrorSheet(workbook, errors);
+    }
+
+    // 7. 범례 추가 (별도 시트)
     const legendSheet = workbook.addWorksheet("Legend");
     legendSheet.addRow(["Excel Template Legend"]);
     legendSheet.addRow([]);
     legendSheet.addRow(["Symbol", "Description"]);
     legendSheet.addRow(["Red background header", "SHI-only fields that cannot be edited"]);
-    legendSheet.addRow(["Gray background header", "Regular editable fields"]);
-    legendSheet.addRow(["Light red background cells", "Data in SHI-only fields (read-only)"]);
-    legendSheet.addRow(["Red text color", "SHI-only field headers"]);
+    legendSheet.addRow(["Blue background header", "Required fields (marked with *)"]);
+    legendSheet.addRow(["Gray background header", "Regular optional fields"]);
+    legendSheet.addRow(["Light red background cells", "Cells with validation errors"]);
+    legendSheet.addRow(["Light red data cells", "Data in SHI-only fields (read-only)"]);
+    
+    if (errors.length > 0) {
+      legendSheet.addRow([]);
+      legendSheet.addRow([`Note: ${errors.length} validation errors found in the 'Errors' sheet`]);
+      const errorNoteRow = legendSheet.getRow(legendSheet.rowCount);
+      errorNoteRow.font = { bold: true, color: { argb: "FFCC0000" } };
+    }
     
     // 범례 스타일 적용
     const legendHeaderRow = legendSheet.getRow(1);
@@ -251,15 +528,25 @@ export async function exportExcelData({
       };
     });
 
-    // 7. 파일 다운로드
+    // 8. 파일 다운로드
     const buffer = await workbook.xlsx.writeBuffer();
-    saveAs(
-      new Blob([buffer]),
-      `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`
-    );
+    const fileName = errors.length > 0 
+      ? `${formCode}_data_with_errors_${new Date().toISOString().slice(0, 10)}.xlsx`
+      : `${formCode}_data_${new Date().toISOString().slice(0, 10)}.xlsx`;
+    
+    saveAs(new Blob([buffer]), fileName);
 
-    toast.success("Excel 내보내기 완료!");
-    return { success: true };
+    const message = errors.length > 0 
+      ? `Excel 내보내기 완료! (${errors.length}개의 검증 오류 발견)`
+      : "Excel 내보내기 완료!";
+    
+    toast.success(message);
+    
+    return { 
+      success: true, 
+      errorCount: errors.length, 
+      hasErrors: errors.length > 0 
+    };
   } catch (err) {
     console.error("Excel export error:", err);
     toast.error("Excel 내보내기 실패.");
-- 
cgit v1.2.3

