summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-02 02:27:28 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-02 02:27:28 +0000
commit37611339fea096e47aaa42311a13a6313b4200db (patch)
treedd9c7dba27a3db2aebd18bf2087c6a30987aa957 /components/form-data
parentbac0228d21b7195065e9cddcc327ae33659c7bcc (diff)
(대표님) 20250602 오전 작업사항 (코드프리징)
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/export-excel-form.tsx321
-rw-r--r--components/form-data/form-data-table-columns.tsx47
-rw-r--r--components/form-data/import-excel-form.tsx284
-rw-r--r--components/form-data/sedp-compare-dialog.tsx2
4 files changed, 583 insertions, 71 deletions
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 내보내기 실패.");
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
index bba2a208..3749fe02 100644
--- a/components/form-data/form-data-table-columns.tsx
+++ b/components/form-data/form-data-table-columns.tsx
@@ -2,6 +2,7 @@ import type { ColumnDef, Row } from "@tanstack/react-table";
import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge"; // Badge import 추가
import { Ellipsis } from "lucide-react";
import { formatDate } from "@/lib/utils";
import {
@@ -61,6 +62,31 @@ interface GetColumnsProps<TData> {
}
/**
+ * status 값에 따라 Badge variant를 결정하는 헬퍼 함수
+ */
+function getStatusBadgeVariant(status: string): "default" | "secondary" | "destructive" | "outline" {
+ const statusStr = String(status).toLowerCase();
+
+ switch (statusStr) {
+ case 'NEW':
+ case 'New':
+ // case 'approved':
+ return 'default'; // 초록색 계열
+ case 'Updated or Modified':
+ // case 'in progress':
+ // case 'processing':
+ return 'secondary'; // 노란색 계열
+ case 'inactive':
+ case 'rejected':
+ case 'failed':
+ case 'cancelled':
+ return 'destructive'; // 빨간색 계열
+ default:
+ return 'outline'; // 기본 회색 계열
+ }
+}
+
+/**
* getColumns 함수
* 1) columnsJSON 배열을 순회하면서 accessorKey / header / cell 등을 설정
* 2) 체크박스 컬럼 추가 (showBatchSelection이 true일 때)
@@ -122,8 +148,7 @@ export function getColumns<TData extends object>({
),
enableSorting: false,
enableHiding: false,
- enablePinning: true, // ← 이 줄 추가
-
+ enablePinning: true,
size: 40,
};
columns.push(selectColumn);
@@ -160,6 +185,24 @@ export function getColumns<TData extends object>({
// 툴팁 메시지 설정 (SHI 필드만)
const tooltipMessage = isReadOnly ? "SHI 전용 필드입니다" : "";
+ // status 컬럼인 경우 Badge 적용
+ if (col.key === "status") {
+ const statusValue = String(cellValue ?? "");
+ const badgeVariant = getStatusBadgeVariant(statusValue);
+
+ return (
+ <div
+ className={readOnlyClass}
+ style={cellStyle}
+ title={tooltipMessage}
+ >
+ <Badge variant={badgeVariant}>
+ {statusValue}
+ </Badge>
+ </div>
+ );
+ }
+
// 데이터 타입별 처리
switch (col.type) {
case "NUMBER":
diff --git a/components/form-data/import-excel-form.tsx b/components/form-data/import-excel-form.tsx
index 6f0828b0..82c7afc8 100644
--- a/components/form-data/import-excel-form.tsx
+++ b/components/form-data/import-excel-form.tsx
@@ -5,6 +5,18 @@ import { DataTableColumnJSON } from "./form-data-table-columns";
import { updateFormDataInDB } from "@/lib/forms/services";
import { decryptWithServerAction } from "../drm/drmUtils";
+// Define error structure for import
+export interface ImportError {
+ tagNo: string;
+ rowIndex: number;
+ columnKey: string;
+ columnLabel: string;
+ errorType: string;
+ errorMessage: string;
+ currentValue?: any;
+ expectedFormat?: string;
+}
+
// Simplified options interface without editableFieldsMap
export interface ImportExcelOptions {
file: File;
@@ -22,6 +34,8 @@ export interface ImportExcelResult {
error?: any;
message?: string;
skippedFields?: { tagNo: string, fields: string[] }[]; // 건너뛴 필드 정보
+ errorCount?: number;
+ hasErrors?: boolean;
}
export interface ExportExcelOptions {
@@ -35,6 +49,133 @@ interface GenericData {
[key: string]: any;
}
+/**
+ * Create error sheet with import validation results
+ */
+function createImportErrorSheet(workbook: ExcelJS.Workbook, errors: ImportError[], headerErrors?: string[]) {
+ const errorSheet = workbook.addWorksheet("Import_Errors");
+
+ // Add header error section if exists
+ if (headerErrors && headerErrors.length > 0) {
+ errorSheet.addRow(["HEADER VALIDATION ERRORS"]);
+ const headerErrorTitleRow = errorSheet.getRow(1);
+ headerErrorTitleRow.font = { bold: true, size: 14, color: { argb: "FFFFFFFF" } };
+ headerErrorTitleRow.getCell(1).fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFDC143C" },
+ };
+
+ headerErrors.forEach((error, index) => {
+ const errorRow = errorSheet.addRow([`${index + 1}. ${error}`]);
+ errorRow.getCell(1).fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFFFCCCC" },
+ };
+ });
+
+ errorSheet.addRow([]); // Empty row for separation
+ }
+
+ // Data validation errors section
+ const startRow = errorSheet.rowCount + 1;
+
+ // Summary row
+ errorSheet.addRow([`DATA VALIDATION ERRORS: ${errors.length} errors found`]);
+ const summaryRow = errorSheet.getRow(startRow);
+ summaryRow.font = { bold: true, size: 12 };
+ if (errors.length > 0) {
+ summaryRow.getCell(1).fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFFFC0C0" },
+ };
+ }
+
+ if (errors.length > 0) {
+ // Error data headers
+ const errorHeaders = [
+ "TAG NO",
+ "Row Number",
+ "Column",
+ "Error Type",
+ "Error Message",
+ "Current Value",
+ "Expected Format",
+ ];
+
+ errorSheet.addRow(errorHeaders);
+ const headerRow = errorSheet.getRow(errorSheet.rowCount);
+ headerRow.font = { bold: true, color: { argb: "FFFFFFFF" } };
+ headerRow.alignment = { horizontal: "center" };
+
+ headerRow.eachCell((cell) => {
+ cell.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "FFDC143C" },
+ };
+ });
+
+ // 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) => {
+ let bgColor = "FFFFFFFF"; // Default white
+
+ switch (error.errorType) {
+ case "MISSING_TAG_NO":
+ bgColor = "FFFFCCCC"; // Light red
+ break;
+ case "TAG_NOT_FOUND":
+ bgColor = "FFFFDDDD"; // Very light red
+ break;
+ case "TYPE_MISMATCH":
+ bgColor = "FFFFEECC"; // Light orange
+ break;
+ case "INVALID_OPTION":
+ bgColor = "FFFFFFE0"; // Light yellow
+ break;
+ case "HEADER_MISMATCH":
+ 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);
+ });
+
+ return errorSheet;
+}
+
export async function importExcelData({
file,
tableData,
@@ -80,13 +221,13 @@ export async function importExcelData({
}
// Validate headers
- let headerErrorMessage = "";
+ const headerErrors: string[] = [];
// Check for missing required columns
columnsJSON.forEach((col) => {
const label = col.label;
if (!headerToIndexMap.has(label)) {
- headerErrorMessage += `Column "${label}" is missing. `;
+ headerErrors.push(`Column "${label}" is missing from Excel file`);
}
});
@@ -94,23 +235,24 @@ export async function importExcelData({
headerToIndexMap.forEach((index, headerLabel) => {
const found = columnsJSON.some((col) => col.label === headerLabel);
if (!found) {
- headerErrorMessage += `Unexpected column "${headerLabel}" found in Excel. `;
+ headerErrors.push(`Unexpected column "${headerLabel}" found in Excel file`);
}
});
- // Add error column
- const lastColIndex = worksheet.columnCount + 1;
- worksheet.getRow(1).getCell(lastColIndex).value = "Error";
-
- // If header validation fails, download error report and exit
- if (headerErrorMessage) {
- headerRow.getCell(lastColIndex).value = headerErrorMessage.trim();
+ // If header validation fails, create error report and exit
+ if (headerErrors.length > 0) {
+ createImportErrorSheet(workbook, [], headerErrors);
const outBuffer = await workbook.xlsx.writeBuffer();
- saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`);
+ saveAs(new Blob([outBuffer]), `import-error-report_${Date.now()}.xlsx`);
- toast.error(`Header mismatch found. Please check downloaded file.`);
- return { success: false, error: "Header mismatch" };
+ toast.error(`Header validation failed. ${headerErrors.length} errors found. Check downloaded error report.`);
+ return {
+ success: false,
+ error: "Header validation errors",
+ errorCount: headerErrors.length,
+ hasErrors: true
+ };
}
// Create column key to Excel index mapping
@@ -124,8 +266,8 @@ export async function importExcelData({
// Parse and validate data rows
const importedData: GenericData[] = [];
+ const validationErrors: ImportError[] = [];
const lastRowNumber = worksheet.lastRow?.number || 1;
- let errorCount = 0;
const skippedFieldsLog: { tagNo: string, fields: string[] }[] = []; // 건너뛴 필드 로그
// Process each data row
@@ -134,16 +276,40 @@ export async function importExcelData({
const rowValues = row.values as ExcelJS.CellValue[];
if (!rowValues || rowValues.length <= 1) continue; // Skip empty rows
- let errorMessage = "";
- let warningMessage = "";
const rowObj: Record<string, any> = {};
const skippedFields: string[] = []; // 현재 행에서 건너뛴 필드들
+ let hasErrors = false;
// 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);
+ // Validate TAG_NO first
+ if (!tagNo) {
+ validationErrors.push({
+ tagNo: `Row-${rowNum}`,
+ rowIndex: rowNum,
+ columnKey: "TAG_NO",
+ columnLabel: "TAG NO",
+ errorType: "MISSING_TAG_NO",
+ errorMessage: "TAG_NO is empty or missing",
+ currentValue: tagNo,
+ });
+ hasErrors = true;
+ } else if (!existingTagNumbers.has(tagNo)) {
+ validationErrors.push({
+ tagNo: tagNo,
+ rowIndex: rowNum,
+ columnKey: "TAG_NO",
+ columnLabel: "TAG NO",
+ errorType: "TAG_NOT_FOUND",
+ errorMessage: "TAG_NO not found in current data",
+ currentValue: tagNo,
+ });
+ hasErrors = true;
+ }
+
// Process each column
columnsJSON.forEach((col) => {
const colIndex = keyToIndexMap.get(col.key);
@@ -179,9 +345,6 @@ export async function importExcelData({
// Type-specific validation
switch (col.type) {
case "STRING":
- if (!stringVal && col.key === "TAG_NO") {
- errorMessage += `[${col.label}] is empty. `;
- }
rowObj[col.key] = stringVal;
break;
@@ -189,7 +352,17 @@ export async function importExcelData({
if (stringVal) {
const num = parseFloat(stringVal);
if (isNaN(num)) {
- errorMessage += `[${col.label}] '${stringVal}' is not a valid number. `;
+ validationErrors.push({
+ tagNo: tagNo || `Row-${rowNum}`,
+ rowIndex: rowNum,
+ columnKey: col.key,
+ columnLabel: col.label,
+ errorType: "TYPE_MISMATCH",
+ errorMessage: "Value is not a valid number",
+ currentValue: stringVal,
+ expectedFormat: "Number",
+ });
+ hasErrors = true;
} else {
rowObj[col.key] = num;
}
@@ -199,14 +372,18 @@ export async function importExcelData({
break;
case "LIST":
- if (
- stringVal &&
- col.options &&
- !col.options.includes(stringVal)
- ) {
- errorMessage += `[${
- col.label
- }] '${stringVal}' not in ${col.options.join(", ")}. `;
+ if (stringVal && col.options && !col.options.includes(stringVal)) {
+ validationErrors.push({
+ tagNo: tagNo || `Row-${rowNum}`,
+ rowIndex: rowNum,
+ columnKey: col.key,
+ columnLabel: col.label,
+ errorType: "INVALID_OPTION",
+ errorMessage: "Value is not in the allowed options list",
+ currentValue: stringVal,
+ expectedFormat: col.options.join(", "),
+ });
+ hasErrors = true;
}
rowObj[col.key] = stringVal;
break;
@@ -223,26 +400,10 @@ export async function importExcelData({
tagNo: tagNo,
fields: skippedFields
});
- warningMessage += `Skipped ${skippedFields.length} SHI-only fields. `;
}
- // Validate TAG_NO
- const tagNum = rowObj["TAG_NO"];
- if (!tagNum) {
- errorMessage += `No TAG_NO found. `;
- } else if (!existingTagNumbers.has(tagNum)) {
- errorMessage += `TagNumber '${tagNum}' is not in current data. `;
- }
-
- // Record errors or add to valid data
- if (errorMessage) {
- row.getCell(lastColIndex).value = errorMessage.trim();
- errorCount++;
- } else {
- // Add warning message to Excel if there are skipped fields
- if (warningMessage) {
- row.getCell(lastColIndex).value = `WARNING: ${warningMessage.trim()}`;
- }
+ // Add to valid data only if no errors
+ if (!hasErrors) {
importedData.push(rowObj);
}
}
@@ -256,16 +417,22 @@ export async function importExcelData({
);
}
- // If there are validation errors, download error report and exit
- if (errorCount > 0) {
+ // If there are validation errors, create error report and exit
+ if (validationErrors.length > 0) {
+ createImportErrorSheet(workbook, validationErrors);
+
const outBuffer = await workbook.xlsx.writeBuffer();
- saveAs(new Blob([outBuffer]), `import-check-result_${Date.now()}.xlsx`);
+ saveAs(new Blob([outBuffer]), `import-error-report_${Date.now()}.xlsx`);
+
toast.error(
- `There are ${errorCount} error row(s). Please check downloaded file.`
+ `Data validation failed. ${validationErrors.length} errors found across ${new Set(validationErrors.map(e => e.tagNo)).size} TAG(s). Check downloaded error report.`
);
+
return {
success: false,
error: "Data validation errors",
+ errorCount: validationErrors.length,
+ hasErrors: true,
skippedFields: skippedFieldsLog
};
}
@@ -342,6 +509,8 @@ export async function importExcelData({
success: true,
importedCount: successCount,
message: `Partially successful: ${successCount} rows updated, ${errorCount} errors`,
+ errorCount: errorCount,
+ hasErrors: errorCount > 0,
skippedFields: skippedFieldsLog
};
} else {
@@ -349,6 +518,8 @@ export async function importExcelData({
success: false,
error: "All updates failed",
message: errors.join("\n"),
+ errorCount: errorCount,
+ hasErrors: true,
skippedFields: skippedFieldsLog
};
}
@@ -368,6 +539,8 @@ export async function importExcelData({
success: true,
importedCount: successCount,
message: "All data imported and saved to database",
+ errorCount: 0,
+ hasErrors: false,
skippedFields: skippedFieldsLog
};
} catch (saveError) {
@@ -376,6 +549,8 @@ export async function importExcelData({
return {
success: false,
error: saveError,
+ errorCount: 1,
+ hasErrors: true,
skippedFields: skippedFieldsLog
};
}
@@ -393,6 +568,8 @@ export async function importExcelData({
return {
success: true,
importedCount: importedData.length,
+ errorCount: 0,
+ hasErrors: false,
skippedFields: skippedFieldsLog
};
}
@@ -400,7 +577,12 @@ export async function importExcelData({
} catch (err) {
console.error("Excel import error:", err);
toast.error("Excel import failed.");
- return { success: false, error: err };
+ return {
+ success: false,
+ error: err,
+ errorCount: 1,
+ hasErrors: true
+ };
} finally {
if (onPendingChange) onPendingChange(false);
}
diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx
index 3107193a..647f2810 100644
--- a/components/form-data/sedp-compare-dialog.tsx
+++ b/components/form-data/sedp-compare-dialog.tsx
@@ -291,7 +291,7 @@ export function SEDPCompareDialog({
// Compare attributes
const attributeComparisons = columnsJSON
- .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC")
+ .filter(col => col.key !== "TAG_NO" && col.key !== "TAG_DESC"&& col.key !== "status")
.map(col => {
const localValue = localItem[col.key];
const sedpValue = sedpItem.attributes.get(col.key);