"use client"; import * as React from "react"; import dynamic from "next/dynamic"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; import { toast } from "sonner"; import { updateFormDataInDB } from "@/lib/forms/services"; import { Loader, Save, AlertTriangle } from "lucide-react"; import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns"; const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), { ssr: false, loading: () => (
Loading SpreadSheets...
) } ); if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } interface TemplateItem { TMPL_ID: string; NAME: string; TMPL_TYPE: string; SPR_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; GRD_LST_SETUP: { REG_TYPE_ID: string; SPR_ITM_IDS: Array; ATTS: Array<{}>; }; SPR_ITM_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; } interface ValidationError { cellAddress: string; attId: string; value: any; expectedType: ColumnType; message: string; } interface CellMapping { attId: string; cellAddress: string; isEditable: boolean; dataRowIndex?: number; } interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; templateData: TemplateItem[] | any; selectedRow?: GenericData; tableData?: GenericData[]; formCode: string; columnsJSON: DataTableColumnJSON[] contractItemId: number; editableFieldsMap?: Map; onUpdateSuccess?: (updatedValues: Record | GenericData[]) => void; } // ๐Ÿš€ ๋กœ๋”ฉ ํ”„๋กœ๊ทธ๋ ˆ์Šค ์ปดํฌ๋„ŒํŠธ interface LoadingProgressProps { phase: string; progress: number; total: number; isVisible: boolean; } const LoadingProgress: React.FC = ({ phase, progress, total, isVisible }) => { const percentage = total > 0 ? Math.round((progress / total) * 100) : 0; if (!isVisible) return null; return (
Loading Template
{phase}
{progress.toLocaleString()} / {total.toLocaleString()} ({percentage}%)
); }; export function TemplateViewDialog({ isOpen, onClose, templateData, selectedRow, tableData = [], formCode, contractItemId, columnsJSON, editableFieldsMap = new Map(), onUpdateSuccess }: TemplateViewDialogProps) { const [hostStyle, setHostStyle] = React.useState({ width: '100%', height: '100%' }); const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); const [currentSpread, setCurrentSpread] = React.useState(null); const [cellMappings, setCellMappings] = React.useState([]); const [isClient, setIsClient] = React.useState(false); const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null>(null); const [validationErrors, setValidationErrors] = React.useState([]); const [selectedTemplateId, setSelectedTemplateId] = React.useState(""); const [availableTemplates, setAvailableTemplates] = React.useState([]); // ๐Ÿ†• ๋กœ๋”ฉ ์ƒํƒœ ์ถ”๊ฐ€ const [loadingProgress, setLoadingProgress] = React.useState<{ phase: string; progress: number; total: number; } | null>(null); const [isInitializing, setIsInitializing] = React.useState(false); // ๐Ÿ”„ ์ง„ํ–‰์ƒํ™ฉ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ const updateProgress = React.useCallback((phase: string, progress: number, total: number) => { setLoadingProgress({ phase, progress, total }); }, []); const determineTemplateType = React.useCallback((template: TemplateItem): 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null => { if (template.TMPL_TYPE === "SPREAD_LIST" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) { return 'SPREAD_LIST'; } if (template.TMPL_TYPE === "SPREAD_ITEM" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) { return 'SPREAD_ITEM'; } if (template.GRD_LST_SETUP && columnsJSON.length > 0) { return 'GRD_LIST'; } return null; }, [columnsJSON]); const isValidTemplate = React.useCallback((template: TemplateItem): boolean => { return determineTemplateType(template) !== null; }, [determineTemplateType]); React.useEffect(() => { setIsClient(true); }, []); React.useEffect(() => { if (!templateData) return; let templates: TemplateItem[]; if (Array.isArray(templateData)) { templates = templateData as TemplateItem[]; } else { templates = [templateData as TemplateItem]; } const validTemplates = templates.filter(isValidTemplate); setAvailableTemplates(validTemplates); if (validTemplates.length > 0 && !selectedTemplateId) { const firstTemplate = validTemplates[0]; const templateTypeToSet = determineTemplateType(firstTemplate); setSelectedTemplateId(firstTemplate.TMPL_ID); setTemplateType(templateTypeToSet); } }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType]); const handleTemplateChange = (templateId: string) => { const template = availableTemplates.find(t => t.TMPL_ID === templateId); if (template) { const templateTypeToSet = determineTemplateType(template); setSelectedTemplateId(templateId); setTemplateType(templateTypeToSet); setHasChanges(false); setValidationErrors([]); if (currentSpread && template) { initSpread(currentSpread, template); } } }; const selectedTemplate = React.useMemo(() => { return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId); }, [availableTemplates, selectedTemplateId]); const editableFields = React.useMemo(() => { // SPREAD_ITEM์˜ ๊ฒฝ์šฐ์—๋งŒ ์ „์—ญ editableFields ์‚ฌ์šฉ if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) { if (!editableFieldsMap.has(selectedRow.TAG_NO)) { return []; } return editableFieldsMap.get(selectedRow.TAG_NO) || []; } // SPREAD_LIST๋‚˜ GRD_LIST์˜ ๊ฒฝ์šฐ ์ „์—ญ editableFields๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ return []; }, [templateType, selectedRow?.TAG_NO, editableFieldsMap]); const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { const columnConfig = columnsJSON.find(col => col.key === attId); if (columnConfig?.shi === "OUT" || columnConfig?.shi === null) { return false; } if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") { return false; } if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { // ๊ฐ ํ–‰์˜ TAG_NO๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŽธ์ง‘ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ํŒ๋‹จ if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { return false; } const rowEditableFields = editableFieldsMap.get(rowData.TAG_NO) || []; if (!rowEditableFields.includes(attId)) { return false; } if (rowData && (rowData.shi === "OUT" || rowData.shi === null)) { return false; } return true; } // SPREAD_ITEM์˜ ๊ฒฝ์šฐ ๊ธฐ์กด ๋กœ์ง ์œ ์ง€ if (templateType === 'SPREAD_ITEM') { return editableFields.includes(attId); } return true; }, [templateType, columnsJSON, editableFieldsMap]); // editableFields ์˜์กด์„ฑ ์ œ๊ฑฐ const editableFieldsCount = React.useMemo(() => { if (templateType === 'SPREAD_ITEM') { // SPREAD_ITEM์˜ ๊ฒฝ์šฐ ๊ธฐ์กด ๋กœ์ง ์œ ์ง€ return cellMappings.filter(m => m.isEditable).length; } if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { // ๊ฐ ํ–‰๋ณ„๋กœ ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ ์ˆ˜๋ฅผ ๊ณ„์‚ฐ let totalEditableCount = 0; tableData.forEach((rowData, rowIndex) => { cellMappings.forEach(mapping => { if (mapping.dataRowIndex === rowIndex) { if (isFieldEditable(mapping.attId, rowData)) { totalEditableCount++; } } }); }); return totalEditableCount; } return cellMappings.filter(m => m.isEditable).length; }, [cellMappings, templateType, tableData, isFieldEditable]); // ๐Ÿš€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜๋“ค const setBatchValues = React.useCallback(( activeSheet: any, valuesToSet: Array<{row: number, col: number, value: any}> ) => { console.log(`๐Ÿš€ Setting ${valuesToSet.length} values in batch`); const columnGroups = new Map>(); valuesToSet.forEach(({row, col, value}) => { if (!columnGroups.has(col)) { columnGroups.set(col, []); } columnGroups.get(col)!.push({row, value}); }); columnGroups.forEach((values, col) => { values.sort((a, b) => a.row - b.row); let start = 0; while (start < values.length) { let end = start; while (end + 1 < values.length && values[end + 1].row === values[end].row + 1) { end++; } const rangeValues = values.slice(start, end + 1).map(v => v.value); const startRow = values[start].row; try { if (rangeValues.length === 1) { activeSheet.setValue(startRow, col, rangeValues[0]); } else { const dataArray = rangeValues.map(v => [v]); activeSheet.setArray(startRow, col, dataArray); } } catch (error) { for (let i = start; i <= end; i++) { try { activeSheet.setValue(values[i].row, col, values[i].value); } catch (cellError) { console.warn(`โš ๏ธ Individual value setting failed [${values[i].row}, ${col}]:`, cellError); } } } start = end + 1; } }); }, []); const createCellStyle = React.useCallback((isEditable: boolean) => { const style = new GC.Spread.Sheets.Style(); if (isEditable) { style.backColor = "#bbf7d0"; } else { style.backColor = "#e5e7eb"; style.foreColor = "#4b5563"; } return style; }, []); const setBatchStyles = React.useCallback(( activeSheet: any, stylesToSet: Array<{row: number, col: number, isEditable: boolean}> ) => { console.log(`๐ŸŽจ Setting ${stylesToSet.length} styles in batch`); const editableStyle = createCellStyle(true); const readonlyStyle = createCellStyle(false); // ๐Ÿ”ง ๊ฐœ๋ณ„ ์…€๋ณ„๋กœ ์Šคํƒ€์ผ๊ณผ ์ž ๊ธˆ ์ƒํƒœ ์„ค์ • (ํŽธ์ง‘ ๊ถŒํ•œ ๋ณด์žฅ) stylesToSet.forEach(({row, col, isEditable}) => { try { const cell = activeSheet.getCell(row, col); const style = isEditable ? editableStyle : readonlyStyle; activeSheet.setStyle(row, col, style); cell.locked(!isEditable); // ํŽธ์ง‘ ๊ฐ€๋Šฅํ•˜๋ฉด ์ž ๊ธˆ ํ•ด์ œ // ๐Ÿ†• ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์…€์— ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์—๋””ํ„ฐ ์„ค์ • if (isEditable) { const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(row, col, textCellType); } } catch (error) { console.warn(`โš ๏ธ Failed to set style for cell [${row}, ${col}]:`, error); } }); }, [createCellStyle]); const parseCellAddress = (address: string): { row: number, col: number } | null => { if (!address || address.trim() === "") return null; const match = address.match(/^([A-Z]+)(\d+)$/); if (!match) return null; const [, colStr, rowStr] = match; let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } col -= 1; const row = parseInt(rowStr) - 1; return { row, col }; }; const getCellAddress = (row: number, col: number): string => { let colStr = ''; let colNum = col; while (colNum >= 0) { colStr = String.fromCharCode((colNum % 26) + 65) + colStr; colNum = Math.floor(colNum / 26) - 1; } return colStr + (row + 1); }; const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => { if (value === undefined || value === null || value === "") { return null; } switch (columnType) { case "NUMBER": if (isNaN(Number(value))) { return "Value must be a valid number"; } break; case "LIST": if (options && !options.includes(String(value))) { return `Value must be one of: ${options.join(", ")}`; } break; case "STRING": break; default: break; } return null; }; const validateAllData = React.useCallback(() => { if (!currentSpread || !selectedTemplate) return []; const activeSheet = currentSpread.getActiveSheet(); const errors: ValidationError[] = []; cellMappings.forEach(mapping => { const columnConfig = columnsJSON.find(col => col.key === mapping.attId); if (!columnConfig) return; const cellPos = parseCellAddress(mapping.cellAddress); if (!cellPos) return; const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); if (errorMessage) { errors.push({ cellAddress: mapping.cellAddress, attId: mapping.attId, value: cellValue, expectedType: columnConfig.type, message: errorMessage }); } }); setValidationErrors(errors); return errors; }, [currentSpread, selectedTemplate, cellMappings, columnsJSON]); const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { try { console.log(`๐ŸŽฏ Setting up dropdown for ${rowCount} rows with options:`, options); const safeOptions = options .filter(opt => opt !== null && opt !== undefined && opt !== '') .map(opt => String(opt).trim()) .filter(opt => opt.length > 0) .filter((opt, index, arr) => arr.indexOf(opt) === index) .slice(0, 20); if (safeOptions.length === 0) { console.warn(`โš ๏ธ No valid options found, skipping`); return; } const optionsString = safeOptions.join(','); for (let i = 0; i < rowCount; i++) { try { const targetRow = cellPos.row + i; // ๐Ÿ”ง ๊ฐ ์…€๋งˆ๋‹ค ์ƒˆ๋กœ์šด ComboBox ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); comboBoxCellType.items(safeOptions); comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); // ๐Ÿ”ง DataValidation ์„ค์ • const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString); cellValidator.showInputMessage(false); cellValidator.showErrorMessage(false); // ComboBox์™€ Validator ์ ์šฉ activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator); // ๐Ÿš€ ์ค‘์š”: ์…€ ์ž ๊ธˆ ํ•ด์ œ ๋ฐ ํŽธ์ง‘ ๊ฐ€๋Šฅ ์„ค์ • const cell = activeSheet.getCell(targetRow, cellPos.col); cell.locked(false); console.log(`โœ… Dropdown applied to [${targetRow}, ${cellPos.col}] with ${safeOptions.length} options`); } catch (cellError) { console.warn(`โš ๏ธ Failed to apply dropdown to row ${cellPos.row + i}:`, cellError); } } console.log(`โœ… Dropdown setup completed for ${rowCount} cells`); } catch (error) { console.error('โŒ Dropdown setup failed:', error); } }, []); const getSafeActiveSheet = React.useCallback((spread: any, functionName: string = 'unknown') => { if (!spread) return null; try { let activeSheet = spread.getActiveSheet(); if (!activeSheet) { const sheetCount = spread.getSheetCount(); if (sheetCount > 0) { activeSheet = spread.getSheet(0); if (activeSheet) { spread.setActiveSheetIndex(0); } } } return activeSheet; } catch (error) { console.error(`โŒ Error getting activeSheet in ${functionName}:`, error); return null; } }, []); const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { try { if (!activeSheet) return false; const currentRowCount = activeSheet.getRowCount(); if (requiredRowCount > currentRowCount) { const newRowCount = requiredRowCount + 10; activeSheet.setRowCount(newRowCount); } return true; } catch (error) { console.error('โŒ Error in ensureRowCapacity:', error); return false; } }, []); const ensureColumnCapacity = React.useCallback((activeSheet: any, requiredColumnCount: number) => { try { if (!activeSheet) return false; const currentColumnCount = activeSheet.getColumnCount(); if (requiredColumnCount > currentColumnCount) { const newColumnCount = requiredColumnCount + 10; activeSheet.setColumnCount(newColumnCount); } return true; } catch (error) { console.error('โŒ Error in ensureColumnCapacity:', error); return false; } }, []); const setOptimalColumnWidths = React.useCallback((activeSheet: any, columns: any[], startCol: number, tableData: any[]) => { columns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; const optimalWidth = column.type === 'NUMBER' ? 100 : column.type === 'STRING' ? 150 : 120; activeSheet.setColumnWidth(targetCol, optimalWidth); }); }, []); // ๐Ÿš€ ์ตœ์ ํ™”๋œ GRD_LIST ์ƒ์„ฑ // ๐Ÿš€ ์ตœ์ ํ™”๋œ GRD_LIST ์ƒ์„ฑ (TAG_DESC ์ปฌ๋Ÿผ ํ‹€๊ณ ์ • ํฌํ•จ) const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => { console.log('๐Ÿš€ Creating optimized GRD_LIST table with TAG_DESC freeze'); const visibleColumns = columnsJSON .filter(col => col.hidden !== true) .sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); if (visibleColumns.length === 0) return []; const startCol = 1; const dataStartRow = 1; const mappings: CellMapping[] = []; ensureColumnCapacity(activeSheet, startCol + visibleColumns.length); ensureRowCapacity(activeSheet, dataStartRow + tableData.length); // ๐ŸงŠ TAG_DESC ์ปฌ๋Ÿผ ์œ„์น˜ ์ฐพ๊ธฐ (ํ‹€๊ณ ์ •์šฉ) const tagDescColumnIndex = visibleColumns.findIndex(col => col.key === 'TAG_DESC'); let freezeColumnCount = 0; if (tagDescColumnIndex !== -1) { // TAG_DESC ์ปฌ๋Ÿผ๊นŒ์ง€ ํฌํ•จํ•ด์„œ ๊ณ ์ • (startCol + tagDescColumnIndex + 1) freezeColumnCount = startCol + tagDescColumnIndex + 1; console.log(`๐ŸงŠ TAG_DESC found at column index ${tagDescColumnIndex}, freezing ${freezeColumnCount} columns`); } else { // TAG_DESC๊ฐ€ ์—†์œผ๋ฉด TAG_NO๊นŒ์ง€๋งŒ ๊ณ ์ • (์ผ๋ฐ˜์ ์œผ๋กœ ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ) const tagNoColumnIndex = visibleColumns.findIndex(col => col.key === 'TAG_NO'); if (tagNoColumnIndex !== -1) { freezeColumnCount = startCol + tagNoColumnIndex + 1; console.log(`๐ŸงŠ TAG_NO found at column index ${tagNoColumnIndex}, freezing ${freezeColumnCount} columns`); } } // ํ—ค๋” ์ƒ์„ฑ const headerStyle = new GC.Spread.Sheets.Style(); headerStyle.backColor = "#3b82f6"; headerStyle.foreColor = "#ffffff"; headerStyle.font = "bold 12px Arial"; headerStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; visibleColumns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; const cell = activeSheet.getCell(0, targetCol); cell.value(column.label); cell.locked(true); activeSheet.setStyle(0, targetCol, headerStyle); }); // ๐Ÿš€ ๋ฐ์ดํ„ฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค€๋น„ const allValues: Array<{row: number, col: number, value: any}> = []; const allStyles: Array<{row: number, col: number, isEditable: boolean}> = []; // ๐Ÿ”ง ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์…€ ์ •๋ณด ์ˆ˜์ง‘ (๋“œ๋กญ๋‹ค์šด์šฉ) const dropdownConfigs: Array<{ startRow: number; col: number; rowCount: number; options: string[]; editableRows: number[]; // ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ํ–‰๋งŒ ์ถ”์  }> = []; visibleColumns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; // ๋“œ๋กญ๋‹ค์šด ์„ค์ •์„ ์œ„ํ•œ ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ํ–‰ ์ฐพ๊ธฐ if (column.type === "LIST" && column.options) { const editableRows: number[] = []; tableData.forEach((rowData, rowIndex) => { if (isFieldEditable(column.key, rowData)) { // rowData ์ „๋‹ฌ editableRows.push(dataStartRow + rowIndex); } }); if (editableRows.length > 0) { dropdownConfigs.push({ startRow: dataStartRow, col: targetCol, rowCount: tableData.length, options: column.options, editableRows: editableRows }); } } tableData.forEach((rowData, rowIndex) => { const targetRow = dataStartRow + rowIndex; const cellEditable = isFieldEditable(column.key, rowData); // rowData ์ „๋‹ฌ const value = rowData[column.key]; mappings.push({ attId: column.key, cellAddress: getCellAddress(targetRow, targetCol), isEditable: cellEditable, dataRowIndex: rowIndex }); allValues.push({ row: targetRow, col: targetCol, value: value ?? null }); allStyles.push({ row: targetRow, col: targetCol, isEditable: cellEditable }); }); }); // ๐Ÿš€ ๋ฐฐ์น˜๋กœ ๊ฐ’๊ณผ ์Šคํƒ€์ผ ์„ค์ • setBatchValues(activeSheet, allValues); setBatchStyles(activeSheet, allStyles); // ๐ŸŽฏ ๊ฐœ์„ ๋œ ๋“œ๋กญ๋‹ค์šด ์„ค์ • (ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์…€์—๋งŒ) dropdownConfigs.forEach(({ col, options, editableRows }) => { try { console.log(`๐ŸŽฏ Setting dropdown for column ${col}, editable rows: ${editableRows.length}`); const safeOptions = options .filter(opt => opt !== null && opt !== undefined && opt !== '') .map(opt => String(opt).trim()) .filter(opt => opt.length > 0) .slice(0, 20); if (safeOptions.length === 0) return; // ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ํ–‰์—๋งŒ ๋“œ๋กญ๋‹ค์šด ์ ์šฉ editableRows.forEach(targetRow => { try { const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); comboBoxCellType.items(safeOptions); comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions.join(',')); cellValidator.showInputMessage(false); cellValidator.showErrorMessage(false); activeSheet.setCellType(targetRow, col, comboBoxCellType); activeSheet.setDataValidator(targetRow, col, cellValidator); // ๐Ÿš€ ํŽธ์ง‘ ๊ถŒํ•œ ๋ช…์‹œ์  ์„ค์ • const cell = activeSheet.getCell(targetRow, col); cell.locked(false); console.log(`โœ… Dropdown applied to editable cell [${targetRow}, ${col}]`); } catch (cellError) { console.warn(`โš ๏ธ Failed to apply dropdown to [${targetRow}, ${col}]:`, cellError); } }); } catch (error) { console.error(`โŒ Dropdown config failed for column ${col}:`, error); } }); // ๐ŸงŠ ํ‹€๊ณ ์ • ์„ค์ • if (freezeColumnCount > 0) { try { activeSheet.frozenColumnCount(freezeColumnCount); activeSheet.frozenRowCount(1); // ํ—ค๋” ํ–‰๋„ ๊ณ ์ • console.log(`๐ŸงŠ Freeze applied: ${freezeColumnCount} columns, 1 row (header)`); // ๐ŸŽจ ๊ณ ์ •๋œ ์ปฌ๋Ÿผ์— ํŠน๋ณ„ํ•œ ์Šคํƒ€์ผ ์ถ”๊ฐ€ (์„ ํƒ์‚ฌํ•ญ) for (let col = 0; col < freezeColumnCount; col++) { for (let row = 0; row <= tableData.length; row++) { try { const currentStyle = activeSheet.getStyle(row, col) || new GC.Spread.Sheets.Style(); if (row === 0) { // ํ—ค๋”๋Š” ๊ธฐ์กด ์Šคํƒ€์ผ ์œ ์ง€ continue; } else { // ๋ฐ์ดํ„ฐ ์…€์— ๊ณ ์ • ๊ตฌ๋ถ„์„  ์ถ”๊ฐ€ if (col === freezeColumnCount - 1) { currentStyle.borderRight = new GC.Spread.Sheets.LineBorder("#2563eb", GC.Spread.Sheets.LineStyle.medium); activeSheet.setStyle(row, col, currentStyle); } } } catch (styleError) { console.warn(`โš ๏ธ Failed to apply freeze border style to [${row}, ${col}]:`, styleError); } } } } catch (freezeError) { console.error('โŒ Failed to apply freeze:', freezeError); } } setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData); console.log(`โœ… Optimized GRD_LIST created with freeze:`); console.log(` - Total mappings: ${mappings.length}`); console.log(` - Editable cells: ${mappings.filter(m => m.isEditable).length}`); console.log(` - Dropdown configs: ${dropdownConfigs.length}`); console.log(` - Frozen columns: ${freezeColumnCount}`); return mappings; }, [tableData, columnsJSON, isFieldEditable, setBatchValues, setBatchStyles, ensureColumnCapacity, ensureRowCapacity, getCellAddress, setOptimalColumnWidths]); const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { console.log(`๐Ÿ›ก๏ธ Setting up protection and events for ${mappings.length} mappings`); // ๐Ÿ”ง ์‹œํŠธ ๋ณดํ˜ธ ์™„์ „ ํ•ด์ œ ํ›„ ํŽธ์ง‘ ๊ถŒํ•œ ์„ค์ • activeSheet.options.isProtected = false; // ๐Ÿ”ง ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์…€๋“ค์„ ์œ„ํ•œ ๊ฐ•ํ™”๋œ ์„ค์ • mappings.forEach((mapping) => { const cellPos = parseCellAddress(mapping.cellAddress); if (!cellPos) return; try { const cell = activeSheet.getCell(cellPos.row, cellPos.col); const columnConfig = columnsJSON.find(col => col.key === mapping.attId); if (mapping.isEditable) { // ๐Ÿš€ ํŽธ์ง‘ ๊ฐ€๋Šฅํ•œ ์…€ ์„ค์ • ๊ฐ•ํ™” cell.locked(false); if (columnConfig?.type === "LIST" && columnConfig.options) { // LIST ํƒ€์ž…: ์ƒˆ ComboBox ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ const comboBox = new GC.Spread.Sheets.CellTypes.ComboBox(); comboBox.items(columnConfig.options); comboBox.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); activeSheet.setCellType(cellPos.row, cellPos.col, comboBox); // DataValidation๋„ ์ถ”๊ฐ€ const validator = GC.Spread.Sheets.DataValidation.createListValidator(columnConfig.options.join(',')); activeSheet.setDataValidator(cellPos.row, cellPos.col, validator); } else if (columnConfig?.type === "NUMBER") { // NUMBER ํƒ€์ž…: ์ˆซ์ž ์ž…๋ ฅ ํ—ˆ์šฉ const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(cellPos.row, cellPos.col, textCellType); // ์ˆซ์ž validation ์ถ”๊ฐ€ (์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์—†์ด) const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator( GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between, -999999999, 999999999, true ); numberValidator.showInputMessage(false); numberValidator.showErrorMessage(false); activeSheet.setDataValidator(cellPos.row, cellPos.col, numberValidator); } else { // ๊ธฐ๋ณธ TEXT ํƒ€์ž…: ์ž์œ  ํ…์ŠคํŠธ ์ž…๋ ฅ const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(cellPos.row, cellPos.col, textCellType); } // ํŽธ์ง‘ ๊ฐ€๋Šฅ ์Šคํƒ€์ผ ์žฌ์ ์šฉ const editableStyle = createCellStyle(true); activeSheet.setStyle(cellPos.row, cellPos.col, editableStyle); console.log(`๐Ÿ”“ Cell [${cellPos.row}, ${cellPos.col}] ${mapping.attId} set as EDITABLE`); } else { // ์ฝ๊ธฐ ์ „์šฉ ์…€ cell.locked(true); const readonlyStyle = createCellStyle(false); activeSheet.setStyle(cellPos.row, cellPos.col, readonlyStyle); } } catch (error) { console.error(`โŒ Error processing cell ${mapping.cellAddress}:`, error); } }); // ๐Ÿ›ก๏ธ ์‹œํŠธ ๋ณดํ˜ธ ์žฌ์„ค์ • (ํŽธ์ง‘ ํ—ˆ์šฉ ๋ชจ๋“œ๋กœ) activeSheet.options.isProtected = false; activeSheet.options.protectionOptions = { allowSelectLockedCells: true, allowSelectUnlockedCells: true, allowSort: false, allowFilter: false, allowEditObjects: true, // โœ… ํŽธ์ง‘ ๊ฐ์ฒด ํ—ˆ์šฉ allowResizeRows: false, allowResizeColumns: false, allowFormatCells: false, allowInsertRows: false, allowInsertColumns: false, allowDeleteRows: false, allowDeleteColumns: false }; // ๐ŸŽฏ ๋ณ€๊ฒฝ ๊ฐ์ง€ ์ด๋ฒคํŠธ const changeEvents = [ GC.Spread.Sheets.Events.CellChanged, GC.Spread.Sheets.Events.ValueChanged, GC.Spread.Sheets.Events.ClipboardPasted ]; changeEvents.forEach(eventType => { activeSheet.bind(eventType, () => { console.log(`๐Ÿ“ ${eventType} detected`); setHasChanges(true); }); }); // ๐Ÿšซ ํŽธ์ง‘ ์‹œ์ž‘ ๊ถŒํ•œ ํ™•์ธ activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { console.log(`๐ŸŽฏ EditStarting: Row ${info.row}, Col ${info.col}`); const exactMapping = mappings.find(m => { const cellPos = parseCellAddress(m.cellAddress); return cellPos && cellPos.row === info.row && cellPos.col === info.col; }); if (!exactMapping) { console.log(`โ„น๏ธ No mapping found for [${info.row}, ${info.col}] - allowing edit`); return; // ๋งคํ•‘์ด ์—†์œผ๋ฉด ํ—ˆ์šฉ } console.log(`๐Ÿ“‹ Found mapping: ${exactMapping.attId}, isEditable: ${exactMapping.isEditable}`); if (!exactMapping.isEditable) { console.log(`๐Ÿšซ Field ${exactMapping.attId} is not editable`); toast.warning(`${exactMapping.attId} field is read-only`); info.cancel = true; return; } // SPREAD_LIST/GRD_LIST ๊ฐœ๋ณ„ ํ–‰ SHI ํ™•์ธ if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && exactMapping.dataRowIndex !== undefined) { const dataRowIndex = exactMapping.dataRowIndex; if (dataRowIndex >= 0 && dataRowIndex < tableData.length) { const rowData = tableData[dataRowIndex]; if (rowData?.shi === "OUT" || rowData?.shi === null ) { console.log(`๐Ÿšซ Row ${dataRowIndex} is in SHI mode`); toast.warning(`Row ${dataRowIndex + 1}: ${exactMapping.attId} field is read-only (SHI mode)`); info.cancel = true; return; } } } console.log(`โœ… Edit allowed for ${exactMapping.attId}`); }); // โœ… ํŽธ์ง‘ ์™„๋ฃŒ ๊ฒ€์ฆ activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => { console.log(`๐Ÿ EditEnded: Row ${info.row}, Col ${info.col}, New value: ${activeSheet.getValue(info.row, info.col)}`); const exactMapping = mappings.find(m => { const cellPos = parseCellAddress(m.cellAddress); return cellPos && cellPos.row === info.row && cellPos.col === info.col; }); if (!exactMapping) return; const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId); if (columnConfig) { const cellValue = activeSheet.getValue(info.row, info.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); const cell = activeSheet.getCell(info.row, info.col); if (errorMessage) { // ๐Ÿšจ ์—๋Ÿฌ ์Šคํƒ€์ผ ์ ์šฉ const errorStyle = new GC.Spread.Sheets.Style(); errorStyle.backColor = "#fef2f2"; errorStyle.foreColor = "#dc2626"; errorStyle.borderLeft = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); errorStyle.borderRight = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); errorStyle.borderTop = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); errorStyle.borderBottom = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); activeSheet.setStyle(info.row, info.col, errorStyle); cell.locked(!exactMapping.isEditable); // ํŽธ์ง‘ ๊ฐ€๋Šฅ ์ƒํƒœ ์œ ์ง€ toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}`, { duration: 5000 }); } else { // โœ… ์ •์ƒ ์Šคํƒ€์ผ ๋ณต์› const normalStyle = createCellStyle(exactMapping.isEditable); activeSheet.setStyle(info.row, info.col, normalStyle); cell.locked(!exactMapping.isEditable); } } setHasChanges(true); }); console.log(`๐Ÿ›ก๏ธ Protection configured. Editable cells: ${mappings.filter(m => m.isEditable).length}`); }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); // ๐Ÿš€ ์ตœ์ ํ™”๋œ initSpread const initSpread = React.useCallback(async (spread: any, template?: TemplateItem) => { const workingTemplate = template || selectedTemplate; if (!spread || !workingTemplate) { console.error('โŒ Invalid spread or template'); return; } try { console.log('๐Ÿš€ Starting optimized spread initialization...'); setIsInitializing(true); updateProgress('Initializing...', 0, 100); setCurrentSpread(spread); setHasChanges(false); setValidationErrors([]); // ๐Ÿš€ ํ•ต์‹ฌ ์ตœ์ ํ™”: ๋ชจ๋“  ๋ Œ๋”๋ง๊ณผ ์ด๋ฒคํŠธ ์ค‘๋‹จ spread.suspendPaint(); spread.suspendEvent(); spread.suspendCalcService(); updateProgress('Setting up workspace...', 10, 100); try { let activeSheet = getSafeActiveSheet(spread, 'initSpread'); if (!activeSheet) { throw new Error('Failed to get initial activeSheet'); } activeSheet.options.isProtected = false; let mappings: CellMapping[] = []; if (templateType === 'GRD_LIST') { updateProgress('Creating dynamic table...', 20, 100); spread.clearSheets(); spread.addSheet(0); const sheet = spread.getSheet(0); sheet.name('Data'); spread.setActiveSheet('Data'); updateProgress('Processing table data...', 50, 100); mappings = createGrdListTableOptimized(sheet, workingTemplate); } else { updateProgress('Loading template structure...', 20, 100); let contentJson = workingTemplate.SPR_LST_SETUP?.CONTENT || workingTemplate.SPR_ITM_LST_SETUP?.CONTENT; let dataSheets = workingTemplate.SPR_LST_SETUP?.DATA_SHEETS || workingTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS; if (!contentJson || !dataSheets) { throw new Error(`No template content found for ${workingTemplate.NAME}`); } const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; updateProgress('Loading template layout...', 40, 100); spread.fromJSON(jsonData); activeSheet = getSafeActiveSheet(spread, 'after-fromJSON'); if (!activeSheet) { throw new Error('ActiveSheet became null after loading template'); } activeSheet.options.isProtected = false; if (templateType === 'SPREAD_LIST' && tableData.length > 0) { updateProgress('Processing data rows...', 60, 100); dataSheets.forEach(dataSheet => { if (dataSheet.MAP_CELL_ATT && dataSheet.MAP_CELL_ATT.length > 0) { dataSheet.MAP_CELL_ATT.forEach((mapping: any) => { const { ATT_ID, IN } = mapping; if (!ATT_ID || !IN || IN.trim() === "") return; const cellPos = parseCellAddress(IN); if (!cellPos) return; const requiredRows = cellPos.row + tableData.length; if (!ensureRowCapacity(activeSheet, requiredRows)) return; // ๐Ÿš€ ๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ ์ค€๋น„ const valuesToSet: Array<{row: number, col: number, value: any}> = []; const stylesToSet: Array<{row: number, col: number, isEditable: boolean}> = []; tableData.forEach((rowData, index) => { const targetRow = cellPos.row + index; const cellEditable = isFieldEditable(ATT_ID, rowData); const value = rowData[ATT_ID]; mappings.push({ attId: ATT_ID, cellAddress: getCellAddress(targetRow, cellPos.col), isEditable: cellEditable, dataRowIndex: index }); valuesToSet.push({ row: targetRow, col: cellPos.col, value: value ?? null }); stylesToSet.push({ row: targetRow, col: cellPos.col, isEditable: cellEditable }); }); // ๐Ÿš€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ setBatchValues(activeSheet, valuesToSet); setBatchStyles(activeSheet, stylesToSet); // ๋“œ๋กญ๋‹ค์šด ์„ค์ • const columnConfig = columnsJSON.find(col => col.key === ATT_ID); if (columnConfig?.type === "LIST" && columnConfig.options) { const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData)); if (hasEditableRows) { setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); } } }); } }); } else if (templateType === 'SPREAD_ITEM' && selectedRow) { updateProgress('Setting up form fields...', 60, 100); dataSheets.forEach(dataSheet => { dataSheet.MAP_CELL_ATT?.forEach((mapping: any) => { const { ATT_ID, IN } = mapping; const cellPos = parseCellAddress(IN); if (cellPos) { const isEditable = isFieldEditable(ATT_ID); const value = selectedRow[ATT_ID]; mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: isEditable, dataRowIndex: 0 }); const cell = activeSheet.getCell(cellPos.row, cellPos.col); cell.value(value ?? null); const style = createCellStyle(isEditable); activeSheet.setStyle(cellPos.row, cellPos.col, style); const columnConfig = columnsJSON.find(col => col.key === ATT_ID); if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); } } }); }); } } updateProgress('Configuring interactions...', 90, 100); setCellMappings(mappings); const finalActiveSheet = getSafeActiveSheet(spread, 'setupEvents'); if (finalActiveSheet) { setupSheetProtectionAndEvents(finalActiveSheet, mappings); } updateProgress('Finalizing...', 100, 100); console.log(`โœ… Optimized initialization completed with ${mappings.length} mappings`); } finally { // ๐Ÿš€ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ์žฌ๊ฐœ spread.resumeCalcService(); spread.resumeEvent(); spread.resumePaint(); } } catch (error) { console.error('โŒ Error in optimized spread initialization:', error); if (spread?.resumeCalcService) spread.resumeCalcService(); if (spread?.resumeEvent) spread.resumeEvent(); if (spread?.resumePaint) spread.resumePaint(); toast.error(`Template loading failed: ${error.message}`); } finally { setIsInitializing(false); setLoadingProgress(null); } }, [selectedTemplate, templateType, selectedRow, tableData, updateProgress, getSafeActiveSheet, createGrdListTableOptimized, setBatchValues, setBatchStyles, setupSheetProtectionAndEvents, setCellMappings]); const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges) { toast.info("No changes to save"); return; } const errors = validateAllData(); if (errors.length > 0) { toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`); return; } try { setIsPending(true); const activeSheet = currentSpread.getActiveSheet(); if (templateType === 'SPREAD_ITEM' && selectedRow) { const dataToSave = { ...selectedRow }; cellMappings.forEach(mapping => { if (mapping.isEditable) { const cellPos = parseCellAddress(mapping.cellAddress); if (cellPos) { const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); dataToSave[mapping.attId] = cellValue; } } }); dataToSave.TAG_NO = selectedRow.TAG_NO; const { success, message } = await updateFormDataInDB( formCode, contractItemId, dataToSave ); if (!success) { toast.error(message); return; } toast.success("Changes saved successfully!"); onUpdateSuccess?.(dataToSave); } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { console.log('๐Ÿ” Starting batch save process...'); const updatedRows: GenericData[] = []; let saveCount = 0; let checkedCount = 0; for (let i = 0; i < tableData.length; i++) { const originalRow = tableData[i]; const dataToSave = { ...originalRow }; let hasRowChanges = false; console.log(`๐Ÿ” Processing row ${i} (TAG_NO: ${originalRow.TAG_NO})`); cellMappings.forEach(mapping => { if (mapping.dataRowIndex === i && mapping.isEditable) { checkedCount++; // ๐Ÿ”ง isFieldEditable๊ณผ ๋™์ผํ•œ ๋กœ์ง ์‚ฌ์šฉ const rowData = tableData[i]; const fieldEditable = isFieldEditable(mapping.attId, rowData); console.log(` ๐Ÿ“ Field ${mapping.attId}: fieldEditable=${fieldEditable}, mapping.isEditable=${mapping.isEditable}`); if (fieldEditable) { const cellPos = parseCellAddress(mapping.cellAddress); if (cellPos) { const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const originalValue = originalRow[mapping.attId]; // ๐Ÿ”ง ๊ฐœ์„ ๋œ ๊ฐ’ ๋น„๊ต (ํƒ€์ž… ๋ณ€ํ™˜ ๋ฐ null/undefined ์ฒ˜๋ฆฌ) const normalizedCellValue = cellValue === null || cellValue === undefined ? "" : String(cellValue).trim(); const normalizedOriginalValue = originalValue === null || originalValue === undefined ? "" : String(originalValue).trim(); console.log(` ๐Ÿ” ${mapping.attId}: "${normalizedOriginalValue}" -> "${normalizedCellValue}"`); if (normalizedCellValue !== normalizedOriginalValue) { dataToSave[mapping.attId] = cellValue; hasRowChanges = true; console.log(` โœ… Change detected for ${mapping.attId}`); } } } } }); if (hasRowChanges) { console.log(`๐Ÿ’พ Saving row ${i} with changes`); dataToSave.TAG_NO = originalRow.TAG_NO; try { const { success, message } = await updateFormDataInDB( formCode, contractItemId, dataToSave ); if (success) { updatedRows.push(dataToSave); saveCount++; console.log(`โœ… Row ${i} saved successfully`); } else { console.error(`โŒ Failed to save row ${i}: ${message}`); toast.error(`Failed to save row ${i + 1}: ${message}`); updatedRows.push(originalRow); // ์›๋ณธ ๋ฐ์ดํ„ฐ ์œ ์ง€ } } catch (error) { console.error(`โŒ Error saving row ${i}:`, error); toast.error(`Error saving row ${i + 1}`); updatedRows.push(originalRow); // ์›๋ณธ ๋ฐ์ดํ„ฐ ์œ ์ง€ } } else { updatedRows.push(originalRow); console.log(`โ„น๏ธ No changes in row ${i}`); } } console.log(`๐Ÿ“Š Save summary: ${saveCount} saved, ${checkedCount} fields checked`); if (saveCount > 0) { toast.success(`${saveCount} rows saved successfully!`); onUpdateSuccess?.(updatedRows); } else { console.warn(`โš ๏ธ No changes detected despite hasChanges=${hasChanges}`); toast.warning("No actual changes were found to save. Please check if the values were properly edited."); } } setHasChanges(false); setValidationErrors([]); } catch (error) { console.error("Error saving changes:", error); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } }, [ currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON, validateAllData, isFieldEditable // ๐Ÿ”ง ์˜์กด์„ฑ ์ถ”๊ฐ€ ]); if (!isOpen) return null; const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0; const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length; return ( SEDP Template - {formCode}
{availableTemplates.length > 1 && (
Template:
)} {selectedTemplate && (
Template Type: { templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' : 'Grid List View (GRD_LIST)' } {templateType === 'SPREAD_ITEM' && selectedRow && ( โ€ข Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'} )} {(templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && ( โ€ข {dataCount} rows )} {hasChanges && ( โ€ข Unsaved changes )} {validationErrors.length > 0 && ( {validationErrors.length} validation errors )}
)}
Editable fields Read-only fields Validation errors {cellMappings.length > 0 && ( {editableFieldsCount} of {cellMappings.length} fields editable )}
{/* ๐Ÿ†• ๋กœ๋”ฉ ํ”„๋กœ๊ทธ๋ ˆ์Šค ์˜ค๋ฒ„๋ ˆ์ด */} {selectedTemplate && isClient && isDataValid ? ( ) : (
{!isClient ? ( <> Loading... ) : !selectedTemplate ? ( "No template available" ) : !isDataValid ? ( `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available` ) : ( "Template not ready" )}
)}
{hasChanges && ( )} {validationErrors.length > 0 && ( )}
); }