"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(() => {
if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) {
if (!editableFieldsMap.has(selectedRow.TAG_NO)) {
return [];
}
return editableFieldsMap.get(selectedRow.TAG_NO) || [];
}
if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) {
const firstRowTagNo = tableData[0]?.TAG_NO;
if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) {
return editableFieldsMap.get(firstRowTagNo) || [];
}
}
return [];
}, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]);
const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => {
const columnConfig = columnsJSON.find(col => col.key === attId);
if (columnConfig?.shi === true) {
return false;
}
if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") {
return false;
}
if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') {
if (rowData && rowData.shi === true) {
return false;
}
return true;
}
return true;
}, [templateType, columnsJSON]);
const editableFieldsCount = React.useMemo(() => {
return cellMappings.filter(m => m.isEditable).length;
}, [cellMappings]);
// π λ°°μΉ μ²λ¦¬ ν¨μλ€
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 = "#f0fdf4";
} else {
style.backColor = "#f9fafb";
style.foreColor = "#6b7280";
}
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 μμ±
const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => {
console.log('π Creating optimized GRD_LIST table');
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);
// ν€λ μμ±
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)) {
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);
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);
}
});
setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData);
console.log(`β
Optimized GRD_LIST created:`);
console.log(` - Total mappings: ${mappings.length}`);
console.log(` - Editable cells: ${mappings.filter(m => m.isEditable).length}`);
console.log(` - Dropdown configs: ${dropdownConfigs.length}`);
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 === true) {
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) {
const updatedRows: GenericData[] = [];
let saveCount = 0;
for (let i = 0; i < tableData.length; i++) {
const originalRow = tableData[i];
const dataToSave = { ...originalRow };
let hasRowChanges = false;
cellMappings.forEach(mapping => {
if (mapping.dataRowIndex === i && mapping.isEditable) {
const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
const isColumnEditable = columnConfig?.shi !== true;
const isRowEditable = originalRow.shi !== true;
if (isColumnEditable && isRowEditable) {
const cellPos = parseCellAddress(mapping.cellAddress);
if (cellPos) {
const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
if (cellValue !== originalRow[mapping.attId]) {
dataToSave[mapping.attId] = cellValue;
hasRowChanges = true;
}
}
}
}
});
if (hasRowChanges) {
dataToSave.TAG_NO = originalRow.TAG_NO;
const { success } = await updateFormDataInDB(
formCode,
contractItemId,
dataToSave
);
if (success) {
updatedRows.push(dataToSave);
saveCount++;
}
} else {
updatedRows.push(originalRow);
}
}
if (saveCount > 0) {
toast.success(`${saveCount} rows saved successfully!`);
onUpdateSuccess?.(updatedRows);
} else {
toast.info("No changes to save");
}
}
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]);
if (!isOpen) return null;
const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0;
const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length;
return (
);
}