diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-13 11:05:09 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-13 11:05:09 +0000 |
| commit | 33be47506f0aa62b969d82521580a29e95080268 (patch) | |
| tree | 6b7e232f2d78ef8775944ea085a36b3ccbce7d95 /components/form-data | |
| parent | 2ac95090157c355ea1bd0b8eb1e1e5e2bd56faf4 (diff) | |
(대표님) 입찰, 법무검토, EDP 변경사항 대응, dolce 개선, form-data 개선, 정규업체 등록관리 추가
(최겸) pq 미사용 컴포넌트 및 페이지 제거, 파일 라우트에 pq 적용
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/add-formTag-dialog.tsx | 4 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 25 | ||||
| -rw-r--r-- | components/form-data/sedp-compare-dialog.tsx | 21 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 1157 | ||||
| -rw-r--r-- | components/form-data/update-form-sheet.tsx | 6 |
5 files changed, 671 insertions, 542 deletions
diff --git a/components/form-data/add-formTag-dialog.tsx b/components/form-data/add-formTag-dialog.tsx index 9d80de8c..5a462d2b 100644 --- a/components/form-data/add-formTag-dialog.tsx +++ b/components/form-data/add-formTag-dialog.tsx @@ -95,6 +95,7 @@ interface AddFormTagDialogProps { formCode: string; formName?: string; contractItemId: number; + packageCode: string; open?: boolean; onOpenChange?: (open: boolean) => void; } @@ -104,6 +105,7 @@ export function AddFormTagDialog({ formCode, formName, contractItemId, + packageCode, open: externalOpen, onOpenChange: externalOnOpenChange }: AddFormTagDialogProps) { @@ -361,7 +363,7 @@ export function AddFormTagDialog({ }; try { - const res = await createTagInForm(tagData, contractItemId, formCode); + const res = await createTagInForm(tagData, contractItemId, formCode, packageCode); if ("error" in res) { failedTags.push({ tag: row.tagNo, error: res.error }); } else { diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index be37de7a..b2fadacf 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -33,6 +33,8 @@ import { } from "lucide-react"; import { toast } from "sonner"; import { + getPackageCodeById, + getProjectById, getProjectCodeById, getReportTempList, sendFormDataToSEDP, @@ -220,6 +222,8 @@ export default function DynamicTable({ // SEDP compare dialog state const [sedpCompareOpen, setSedpCompareOpen] = React.useState(false); const [projectCode, setProjectCode] = React.useState<string>(''); + const [projectType, setProjectType] = React.useState<string>('plant'); + const [packageCode, setPackageCode] = React.useState<string>(''); // 새로 추가된 Template 다이얼로그 상태 const [templateDialogOpen, setTemplateDialogOpen] = React.useState(false); @@ -255,12 +259,26 @@ export default function DynamicTable({ getTempCount(); }, [contractItemId, formId, tempUpDialog]); + React.useEffect(() => { + const getPackageCode = async () => { + try { + const packageCode = await getPackageCodeById(contractItemId); + setPackageCode(packageCode || ''); // 빈 문자열이나 다른 기본값 + } catch (error) { + console.error('패키지 조회 실패:', error); + setPackageCode(''); + } + }; + + getPackageCode(); + }, [contractItemId]) // Get project code when component mounts React.useEffect(() => { const getProjectCode = async () => { try { - const code = await getProjectCodeById(projectId); - setProjectCode(code); + const project = await getProjectById(projectId); + setProjectCode(project.code); + setProjectType(project.type); } catch (error) { console.error("Error fetching project code:", error); toast.error("Failed to fetch project code"); @@ -911,6 +929,7 @@ export default function DynamicTable({ formCode={formCode} formName={`Form ${formCode}`} contractItemId={contractItemId} + packageCode={packageCode} open={addTagDialogOpen} onOpenChange={setAddTagDialogOpen} /> @@ -982,6 +1001,8 @@ export default function DynamicTable({ projectCode={projectCode} formCode={formCode} fetchTagDataFromSEDP={fetchTagDataFromSEDP} + projectType={projectType} + packageCode={packageCode} /> {/* Other dialogs */} diff --git a/components/form-data/sedp-compare-dialog.tsx b/components/form-data/sedp-compare-dialog.tsx index 1a9938bd..9a6c8098 100644 --- a/components/form-data/sedp-compare-dialog.tsx +++ b/components/form-data/sedp-compare-dialog.tsx @@ -22,6 +22,8 @@ interface SEDPCompareDialogProps { projectCode: string; formCode: string; fetchTagDataFromSEDP: (projectCode: string, formCode: string) => Promise<any>; + projectType:string; + packageCode:string; } interface ComparisonResult { @@ -67,6 +69,8 @@ export function SEDPCompareDialog({ projectCode, formCode, fetchTagDataFromSEDP, + projectType, + packageCode }: SEDPCompareDialogProps) { const params = useParams() || {} @@ -256,7 +260,22 @@ export function SEDPCompareDialog({ // Create a map of SEDP data by TAG_NO for quick lookup const sedpTagMap = new Map(); - sedpTagEntries.forEach((entry: any) => { + + const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074"; + + + const tagEntries = sedpTagEntries.filter(entry => { + if (Array.isArray(entry.ATTRIBUTES)) { + const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId); + if (packageCodeAttr && packageCodeAttr.VALUE === packageCode) { + return true; + } + } + return false; + }); + + + tagEntries.forEach((entry: any) => { const tagNo = entry.TAG_NO; const attributesMap = new Map(); diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 11d37911..54a70d9d 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -13,7 +13,6 @@ 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"; -// SpreadSheets를 동적으로 import (SSR 비활성화) const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), { @@ -27,7 +26,6 @@ const SpreadSheets = dynamic( } ); -// 라이센스 키 설정을 클라이언트에서만 실행 if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } @@ -88,8 +86,8 @@ interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; templateData: TemplateItem[] | any; - selectedRow?: GenericData; // SPREAD_ITEM용 - tableData?: GenericData[]; // SPREAD_LIST용 + selectedRow?: GenericData; + tableData?: GenericData[]; formCode: string; columnsJSON: DataTableColumnJSON[] contractItemId: number; @@ -97,6 +95,44 @@ interface TemplateViewDialogProps { onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void; } +// 🚀 로딩 프로그레스 컴포넌트 +interface LoadingProgressProps { + phase: string; + progress: number; + total: number; + isVisible: boolean; +} + +const LoadingProgress: React.FC<LoadingProgressProps> = ({ phase, progress, total, isVisible }) => { + const percentage = total > 0 ? Math.round((progress / total) * 100) : 0; + + if (!isVisible) return null; + + return ( + <div className="absolute inset-0 bg-white/90 flex items-center justify-center z-50"> + <div className="bg-white rounded-lg shadow-lg border p-6 min-w-[300px]"> + <div className="flex items-center space-x-3 mb-4"> + <Loader className="h-5 w-5 animate-spin text-blue-600" /> + <span className="font-medium text-gray-900">Loading Template</span> + </div> + + <div className="space-y-2"> + <div className="text-sm text-gray-600">{phase}</div> + <div className="w-full bg-gray-200 rounded-full h-2"> + <div + className="bg-blue-600 h-2 rounded-full transition-all duration-300 ease-out" + style={{ width: `${percentage}%` }} + /> + </div> + <div className="text-xs text-gray-500 text-right"> + {progress.toLocaleString()} / {total.toLocaleString()} ({percentage}%) + </div> + </div> + </div> + </div> + ); +}; + export function TemplateViewDialog({ isOpen, onClose, @@ -123,13 +159,41 @@ export function TemplateViewDialog({ const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]); const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]); + + // 🆕 로딩 상태 추가 + 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; @@ -140,134 +204,158 @@ export function TemplateViewDialog({ templates = [templateData as TemplateItem]; } - // 유효한 템플릿들 필터링 - const validTemplates = templates.filter(template => { - const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT; - const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT; - const hasGrdListSetup = template.GRD_LST_SETUP && columnsJSON.length > 0; // GRD_LIST 조건: GRD_LST_SETUP 존재 + columnsJSON 있음 - - const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || - template.TMPL_TYPE === "SPREAD_ITEM" || - template.TMPL_TYPE === "GRD_LIST"; // GRD_LIST 타입 추가 - - return isValidType && (hasSpreadListContent || hasSpreadItemContent || hasGrdListSetup); - }); - + const validTemplates = templates.filter(isValidTemplate); setAvailableTemplates(validTemplates); - // 첫 번째 유효한 템플릿을 기본으로 선택 if (validTemplates.length > 0 && !selectedTemplateId) { const firstTemplate = validTemplates[0]; + const templateTypeToSet = determineTemplateType(firstTemplate); setSelectedTemplateId(firstTemplate.TMPL_ID); - - // 템플릿 타입 결정 - let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST'; - if (firstTemplate.GRD_LST_SETUP && columnsJSON.length > 0) { - templateTypeToSet = 'GRD_LIST'; - } else { - templateTypeToSet = firstTemplate.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'; - } - setTemplateType(templateTypeToSet); } - }, [templateData, selectedTemplateId]); + }, [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); - - // 템플릿 타입 결정 - let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST'; - if (template.GRD_LST_SETUP && columnsJSON.length > 0) { - templateTypeToSet = 'GRD_LIST'; - } else { - templateTypeToSet = template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'; - } - setTemplateType(templateTypeToSet); setHasChanges(false); setValidationErrors([]); - // SpreadSheets 재초기화 - if (currentSpread) { - const template = availableTemplates.find(t => t.TMPL_ID === templateId); - if (template) { - initSpread(currentSpread, template); - } + 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인 경우: selectedRow의 TAG_NO로 확인 if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) { if (!editableFieldsMap.has(selectedRow.TAG_NO)) { return []; } return editableFieldsMap.get(selectedRow.TAG_NO) || []; } - - // SPREAD_LIST 또는 GRD_LIST인 경우: 첫 번째 행의 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) => { - // columnsJSON에서 해당 attId의 shi 값 확인 const columnConfig = columnsJSON.find(col => col.key === attId); if (columnConfig?.shi === true) { - return false; // columnsJSON에서 shi가 true이면 편집 불가 + return false; } - // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우) - if (attId === "TAG_NO" || attId === "TAG_DESC") { - return true; + if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") { + return false; } - // SPREAD_ITEM인 경우: editableFields 체크 - if (templateType === 'SPREAD_ITEM') { - return editableFields.includes(attId); - } - - // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려 if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { - // 기본적으로 editableFields에 포함되어야 함 - if (!editableFields.includes(attId)) { - return false; - } - - // rowData가 제공된 경우 해당 행의 shi 상태도 확인 if (rowData && rowData.shi === true) { return false; } - return true; } - + return true; - }, [templateType, columnsJSON, editableFields]); + }, [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<number, Array<{row: number, value: any}>>(); + + 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 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; @@ -283,11 +371,9 @@ export function TemplateViewDialog({ col -= 1; const row = parseInt(rowStr) - 1; - return { row, col }; }; - // 행과 열을 셀 주소로 변환하는 함수 (GRD_LIST용) const getCellAddress = (row: number, col: number): string => { let colStr = ''; let colNum = col; @@ -298,10 +384,9 @@ export function TemplateViewDialog({ return colStr + (row + 1); }; - // 데이터 타입 검증 함수 const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => { if (value === undefined || value === null || value === "") { - return null; // 빈 값은 별도 required 검증에서 처리 + return null; } switch (columnType) { @@ -316,17 +401,14 @@ export function TemplateViewDialog({ } break; case "STRING": - // STRING 타입은 대부분의 값을 허용 break; default: - // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음 break; } return null; }; - // 전체 데이터 검증 함수 const validateAllData = React.useCallback(() => { if (!currentSpread || !selectedTemplate) return []; @@ -340,68 +422,44 @@ export function TemplateViewDialog({ const cellPos = parseCellAddress(mapping.cellAddress); if (!cellPos) return; - if (templateType === 'SPREAD_ITEM') { - // 단일 행 검증 - 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 - }); - } - } else if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { - // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 - const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); - const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); + 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 - }); - } + 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, templateType]); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 🛠️ 헬퍼 함수들 - // ═══════════════════════════════════════════════════════════════════════════════ + }, [currentSpread, selectedTemplate, cellMappings, columnsJSON]); - // 🎨 셀 스타일 생성 const createCellStyle = React.useCallback((isEditable: boolean) => { const style = new GC.Spread.Sheets.Style(); if (isEditable) { - style.backColor = "#f0fdf4"; // 연한 초록 (편집 가능) + style.backColor = "#f0fdf4"; } else { - style.backColor = "#f9fafb"; // 연한 회색 (읽기 전용) + style.backColor = "#f9fafb"; style.foreColor = "#6b7280"; } return style; }, []); - // 🎯 드롭다운 설정 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); - // ✅ options 정규화 const safeOptions = options - .filter(opt => opt !== null && opt !== undefined && opt !== '') + .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) + .filter((opt, index, arr) => arr.indexOf(opt) === index) .slice(0, 20); if (safeOptions.length === 0) { @@ -409,280 +467,333 @@ export function TemplateViewDialog({ return; } - console.log(`📋 Safe options:`, safeOptions); - - // ✅ DataValidation용 문자열 준비 const optionsString = safeOptions.join(','); - // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성! for (let i = 0; i < rowCount; i++) { try { const targetRow = cellPos.row + i; - // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성 + // 🔧 각 셀마다 새로운 ComboBox 인스턴스 생성 const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(safeOptions); // 배열로 전달 + comboBoxCellType.items(safeOptions); comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - - // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성 + + // 🔧 DataValidation 설정 const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString); - - // ComboBox + DataValidation 둘 다 적용 + 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(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`); - + + console.log(`✅ Dropdown applied to [${targetRow}, ${cellPos.col}] with ${safeOptions.length} options`); + } catch (cellError) { - console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError); + console.warn(`⚠️ Failed to apply dropdown to row ${cellPos.row + i}:`, cellError); } } - console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`); + 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) => { - const currentRowCount = activeSheet.getRowCount(); - if (requiredRowCount > currentRowCount) { - activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가 - console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`); + 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; } }, []); - // 🆕 GRD_LIST용 동적 테이블 생성 함수 - const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => { - console.log('🏗️ Creating GRD_LIST table'); - - // columnsJSON의 visible 컬럼들을 seq 순서로 정렬하여 사용 - const visibleColumns = columnsJSON - .filter(col => col.hidden !== true) // hidden이 true가 아닌 것들만 - .sort((a, b) => { - // seq가 없는 경우 999999로 처리하여 맨 뒤로 보냄 - const seqA = a.seq !== undefined ? a.seq : 999999; - const seqB = b.seq !== undefined ? b.seq : 999999; - return seqA - seqB; - }); + 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; + } + }, []); - console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`)); + 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); + }); + }, []); - if (visibleColumns.length === 0) { - console.warn('❌ No visible columns found in columnsJSON'); - return []; - } + // 🚀 최적화된 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[] = []; - const startCol = 1; // A열 제외하고 B열부터 시작 - - // 🔍 그룹 헤더 분석 - const groupInfo = analyzeColumnGroups(visibleColumns); - const hasGroups = groupInfo.groups.length > 0; - - // 헤더 행 계산: 그룹이 있으면 2행, 없으면 1행 - const groupHeaderRow = 0; - const columnHeaderRow = hasGroups ? 1 : 0; - const dataStartRow = hasGroups ? 2 : 1; - - // 🎨 헤더 스타일 생성 - const groupHeaderStyle = new GC.Spread.Sheets.Style(); - groupHeaderStyle.backColor = "#1e40af"; // 더 진한 파란색 - groupHeaderStyle.foreColor = "#ffffff"; - groupHeaderStyle.font = "bold 13px Arial"; - groupHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; - groupHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center; - - const columnHeaderStyle = new GC.Spread.Sheets.Style(); - columnHeaderStyle.backColor = "#3b82f6"; // 기본 파란색 - columnHeaderStyle.foreColor = "#ffffff"; - columnHeaderStyle.font = "bold 12px Arial"; - columnHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; - columnHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center; - - let currentCol = startCol; - - // 🏗️ 그룹 헤더 및 컬럼 헤더 생성 - if (hasGroups) { - // 그룹 헤더가 있는 경우 - groupInfo.groups.forEach(group => { - if (group.isGroup) { - // 그룹 헤더 생성 및 병합 - const groupStartCol = currentCol; - const groupEndCol = currentCol + group.columns.length - 1; - - // 그룹 헤더 셀 설정 - const groupHeaderCell = activeSheet.getCell(groupHeaderRow, groupStartCol); - groupHeaderCell.value(group.head); - - // 그룹 헤더 병합 - if (group.columns.length > 1) { - activeSheet.addSpan(groupHeaderRow, groupStartCol, 1, group.columns.length); - } - - // 그룹 헤더 스타일 적용 - for (let col = groupStartCol; col <= groupEndCol; col++) { - activeSheet.setStyle(groupHeaderRow, col, groupHeaderStyle); - activeSheet.getCell(groupHeaderRow, col).locked(true); - } - - console.log(`📝 Group Header [${groupHeaderRow}, ${groupStartCol}-${groupEndCol}]: ${group.head}`); - - // 그룹 내 개별 컬럼 헤더 생성 - group.columns.forEach((column, index) => { - const colIndex = groupStartCol + index; - const columnHeaderCell = activeSheet.getCell(columnHeaderRow, colIndex); - columnHeaderCell.value(column.label); - activeSheet.setStyle(columnHeaderRow, colIndex, columnHeaderStyle); - columnHeaderCell.locked(true); - - console.log(`📝 Column Header [${columnHeaderRow}, ${colIndex}]: ${column.label}`); - }); - - currentCol += group.columns.length; - } else { - // 그룹이 아닌 단일 컬럼 - const column = group.columns[0]; - - // 그룹 헤더 행에는 빈 셀 (개별 컬럼이므로) - const groupHeaderCell = activeSheet.getCell(groupHeaderRow, currentCol); - groupHeaderCell.value(""); - activeSheet.setStyle(groupHeaderRow, currentCol, groupHeaderStyle); - groupHeaderCell.locked(true); - - // 컬럼 헤더 생성 - const columnHeaderCell = activeSheet.getCell(columnHeaderRow, currentCol); - columnHeaderCell.value(column.label); - activeSheet.setStyle(columnHeaderRow, currentCol, columnHeaderStyle); - columnHeaderCell.locked(true); - - console.log(`📝 Single Column [${columnHeaderRow}, ${currentCol}]: ${column.label}`); - currentCol++; - } - }); - } else { - // 그룹이 없는 경우 - 기존 로직 - visibleColumns.forEach((column, colIndex) => { - const headerCol = startCol + colIndex; - const headerCell = activeSheet.getCell(columnHeaderRow, headerCol); - headerCell.value(column.label); - activeSheet.setStyle(columnHeaderRow, headerCol, columnHeaderStyle); - headerCell.locked(true); - - console.log(`📝 Header [${columnHeaderRow}, ${headerCol}]: ${column.label}`); - }); - } - // 데이터 행 생성 - const dataRowCount = tableData.length; - ensureRowCapacity(activeSheet, dataStartRow + dataRowCount); + 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[]; // 편집 가능한 행만 추적 + }> = []; - tableData.forEach((rowData, rowIndex) => { - const targetRow = dataStartRow + rowIndex; + visibleColumns.forEach((column, colIndex) => { + const targetCol = startCol + colIndex; - visibleColumns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - const cellAddress = getCellAddress(targetRow, targetCol); - const isEditable = isFieldEditable(column.key, rowData); + // 드롭다운 설정을 위한 편집 가능한 행 찾기 + 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: cellAddress, - isEditable: isEditable, + cellAddress: getCellAddress(targetRow, targetCol), + isEditable: cellEditable, dataRowIndex: rowIndex }); - // 셀 값 설정 - const cell = activeSheet.getCell(targetRow, targetCol); - const value = rowData[column.key]; - cell.value(value ?? null); - - // 스타일 적용 - const style = createCellStyle(isEditable); - activeSheet.setStyle(targetRow, targetCol, style); - cell.locked(!isEditable); - - console.log(`📝 Data [${targetRow}, ${targetCol}]: ${column.key} = "${value}" (${isEditable ? 'Editable' : 'ReadOnly'})`); + allValues.push({ + row: targetRow, + col: targetCol, + value: value ?? null + }); - // LIST 타입 드롭다운 설정 - if (column.type === "LIST" && column.options && isEditable) { - setupOptimizedListValidation(activeSheet, { row: targetRow, col: targetCol }, column.options, 1); - } + allStyles.push({ + row: targetRow, + col: targetCol, + isEditable: cellEditable + }); }); }); - console.log(`🏗️ GRD_LIST table created with ${mappings.length} mappings, hasGroups: ${hasGroups}`); - return mappings; - }, [tableData, columnsJSON, isFieldEditable, createCellStyle, ensureRowCapacity, setupOptimizedListValidation]); - - // 🔍 컬럼 그룹 분석 함수 - const analyzeColumnGroups = React.useCallback((columns: DataTableColumnJSON[]) => { - const groups: Array<{ - head: string; - isGroup: boolean; - columns: DataTableColumnJSON[]; - }> = []; + // 🚀 배치로 값과 스타일 설정 + setBatchValues(activeSheet, allValues); + setBatchStyles(activeSheet, allStyles); - let i = 0; - while (i < columns.length) { - const currentCol = columns[i]; - - // head가 없거나 빈 문자열인 경우 단일 컬럼으로 처리 - if (!currentCol.head || !currentCol.head.trim()) { - groups.push({ - head: '', - isGroup: false, - columns: [currentCol] + // 🎯 개선된 드롭다운 설정 (편집 가능한 셀에만) + 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); + } }); - i++; - continue; + } catch (error) { + console.error(`❌ Dropdown config failed for column ${col}:`, error); } + }); - // 같은 head를 가진 연속된 컬럼들을 찾기 - const groupHead = currentCol.head.trim(); - const groupColumns: DataTableColumnJSON[] = [currentCol]; - let j = i + 1; - - while (j < columns.length && columns[j].head && columns[j].head.trim() === groupHead) { - groupColumns.push(columns[j]); - j++; - } + 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]); - // 그룹 추가 - groups.push({ - head: groupHead, - isGroup: groupColumns.length > 1, - columns: groupColumns - }); + const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { + console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); - i = j; // 다음 그룹으로 이동 - } + // 🔧 시트 보호 완전 해제 후 편집 권한 설정 + activeSheet.options.isProtected = false; - return { groups }; - }, []); + // 🔧 편집 가능한 셀들을 위한 강화된 설정 + 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); + } + }); - // 🛡️ 시트 보호 및 이벤트 설정 - const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { - console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); - - // 시트 보호 설정 + // 🛡️ 시트 보호 재설정 (편집 허용 모드로) activeSheet.options.isProtected = true; activeSheet.options.protectionOptions = { allowSelectLockedCells: true, allowSelectUnlockedCells: true, allowSort: false, allowFilter: false, - allowEditObjects: false, + allowEditObjects: true, // ✅ 편집 객체 허용 allowResizeRows: false, - allowResizeColumns: false + allowResizeColumns: false, + allowFormatCells: false, + allowInsertRows: false, + allowInsertColumns: false, + allowDeleteRows: false, + allowDeleteColumns: false }; // 🎯 변경 감지 이벤트 @@ -699,11 +810,10 @@ export function TemplateViewDialog({ }); }); - // 🚫 편집 시작 권한 확인 (수정됨) + // 🚫 편집 시작 권한 확인 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; @@ -711,12 +821,11 @@ export function TemplateViewDialog({ if (!exactMapping) { console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - allowing edit`); - return; // 매핑이 없으면 허용 (템플릿 영역 밖) + return; // 매핑이 없으면 허용 } - console.log(`📋 Found mapping: ${exactMapping.attId} at ${exactMapping.cellAddress}`); + 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`); @@ -724,12 +833,9 @@ export function TemplateViewDialog({ return; } - // SPREAD_LIST 또는 GRD_LIST 개별 행 SHI 확인 + // SPREAD_LIST/GRD_LIST 개별 행 SHI 확인 if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && exactMapping.dataRowIndex !== undefined) { const dataRowIndex = exactMapping.dataRowIndex; - - console.log(`🔍 Checking SHI for data row ${dataRowIndex}`); - if (dataRowIndex >= 0 && dataRowIndex < tableData.length) { const rowData = tableData[dataRowIndex]; if (rowData?.shi === true) { @@ -738,41 +844,31 @@ export function TemplateViewDialog({ info.cancel = true; return; } - } else { - console.warn(`⚠️ Invalid dataRowIndex: ${dataRowIndex} (tableData.length: ${tableData.length})`); } } 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}`); - - // ✅ 정확한 매핑 찾기 + 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) { - console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - skipping validation`); - return; - } + if (!exactMapping) return; const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId); if (columnConfig) { const cellValue = activeSheet.getValue(info.row, info.col); - console.log(`🔍 Validating ${exactMapping.attId}: "${cellValue}"`); - const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); const cell = activeSheet.getCell(info.row, info.col); if (errorMessage) { - console.log(`❌ Validation failed: ${errorMessage}`); - - // 🚨 에러 스타일 적용 (편집 가능 상태 유지) + // 🚨 에러 스타일 적용 const errorStyle = new GC.Spread.Sheets.Style(); errorStyle.backColor = "#fef2f2"; errorStyle.foreColor = "#dc2626"; @@ -780,222 +876,221 @@ export function TemplateViewDialog({ 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}. Please correct the value.`, { duration: 5000 }); + cell.locked(!exactMapping.isEditable); // 편집 가능 상태 유지 + toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}`, { duration: 5000 }); } else { - console.log(`✅ Validation passed`); - // ✅ 정상 스타일 복원 const normalStyle = createCellStyle(exactMapping.isEditable); activeSheet.setStyle(info.row, info.col, normalStyle); cell.locked(!exactMapping.isEditable); } } + + setHasChanges(true); }); - console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`); + console.log(`🛡️ Protection configured. Editable cells: ${mappings.filter(m => m.isEditable).length}`); }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); - // ═══════════════════════════════════════════════════════════════════════════════ - // 🏗️ 메인 SpreadSheets 초기화 함수 - // ═══════════════════════════════════════════════════════════════════════════════ - - const initSpread = React.useCallback((spread: any, template?: TemplateItem) => { + // 🚀 최적화된 initSpread + const initSpread = React.useCallback(async (spread: any, template?: TemplateItem) => { const workingTemplate = template || selectedTemplate; - if (!spread || !workingTemplate) return; + 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 { - const activeSheet = spread.getActiveSheet(); - - // 시트 보호 해제 (편집을 위해) - activeSheet.options.isProtected = false; + let activeSheet = getSafeActiveSheet(spread, 'initSpread'); + if (!activeSheet) { + throw new Error('Failed to get initial activeSheet'); + } + activeSheet.options.isProtected = false; let mappings: CellMapping[] = []; - // 🆕 GRD_LIST 처리 - if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) { - console.log('🏗️ Processing GRD_LIST template'); + 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'); - // 동적 테이블 생성 - mappings = createGrdListTable(sheet, workingTemplate); + updateProgress('Processing table data...', 50, 100); + mappings = createGrdListTableOptimized(sheet, workingTemplate); } else { - // 기존 SPREAD_LIST 및 SPREAD_ITEM 처리 - let contentJson = null; - let dataSheets = null; - - // SPR_LST_SETUP.CONTENT 우선 사용 - if (workingTemplate.SPR_LST_SETUP?.CONTENT) { - contentJson = workingTemplate.SPR_LST_SETUP.CONTENT; - dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS; - console.log('✅ Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME); - } - // SPR_ITM_LST_SETUP.CONTENT 대안 사용 - else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) { - contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT; - dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; - console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME); - } + 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) { - console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); - return; + if (!contentJson || !dataSheets) { + throw new Error(`No template content found for ${workingTemplate.NAME}`); } - // 🏗️ SpreadSheets 초기화 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 (dataSheets && dataSheets.length > 0) { + 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.forEach(mapping => { + if (dataSheet.MAP_CELL_ATT && dataSheet.MAP_CELL_ATT.length > 0) { + dataSheet.MAP_CELL_ATT.forEach((mapping: any) => { const { ATT_ID, IN } = mapping; - - if (IN && IN.trim() !== "") { - const cellPos = parseCellAddress(IN); - if (cellPos) { - const columnConfig = columnsJSON.find(col => col.key === ATT_ID); - - // 🎯 템플릿 타입별 데이터 처리 - if (templateType === 'SPREAD_ITEM' && selectedRow) { - // 📝 단일 행 처리 (SPREAD_ITEM) - const isEditable = isFieldEditable(ATT_ID); - - // 매핑 정보 저장 - mappings.push({ - attId: ATT_ID, - cellAddress: IN, - isEditable: isEditable, - dataRowIndex: 0 - }); - - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - const value = selectedRow[ATT_ID]; - - // 값 설정 - cell.value(value ?? null); - - // 🎨 스타일 및 편집 권한 설정 - cell.locked(!isEditable); - const style = createCellStyle(isEditable); - activeSheet.setStyle(cellPos.row, cellPos.col, style); - - // 📋 LIST 타입 드롭다운 설정 - if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); - } - - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨 - console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - - // 🚀 행 확장 (필요시) - ensureRowCapacity(activeSheet, cellPos.row + tableData.length); - - // 📋 각 행마다 개별 매핑 생성 - tableData.forEach((rowData, index) => { - const targetRow = cellPos.row + index; - const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`; - const cellEditable = isFieldEditable(ATT_ID, rowData); - - // 개별 매핑 추가 - mappings.push({ - attId: ATT_ID, - cellAddress: targetCellAddress, // 각 행마다 다른 주소 - isEditable: cellEditable, - dataRowIndex: index // 원본 데이터 인덱스 - }); - - console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`); - }); - - // 📋 LIST 타입 드롭다운 설정 (조건부) - if (columnConfig?.type === "LIST" && columnConfig.options) { - // 편집 가능한 행이 하나라도 있으면 드롭다운 설정 - const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData)); - if (hasEditableRows) { - setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); - } - } - - // 🎨 개별 셀 데이터 및 스타일 설정 - tableData.forEach((rowData, index) => { - const targetRow = cellPos.row + index; - const cell = activeSheet.getCell(targetRow, cellPos.col); - const value = rowData[ATT_ID]; - - // 값 설정 - cell.value(value ?? null); - // console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`); - - // 편집 권한 및 스타일 설정 - const cellEditable = isFieldEditable(ATT_ID, rowData); - cell.locked(!cellEditable); - const style = createCellStyle(cellEditable); - activeSheet.setStyle(targetRow, cellPos.col, style); - }); - } - - // console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`); + 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); - setupSheetProtectionAndEvents(activeSheet, 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 initializing spread:', error); - toast.error('Failed to load template'); - if (spread?.resumePaint) { - spread.resumePaint(); - } + 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, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable]); + }, [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.`); @@ -1004,11 +1099,9 @@ export function TemplateViewDialog({ try { setIsPending(true); - const activeSheet = currentSpread.getActiveSheet(); if (templateType === 'SPREAD_ITEM' && selectedRow) { - // 단일 행 저장 const dataToSave = { ...selectedRow }; cellMappings.forEach(mapping => { @@ -1038,7 +1131,6 @@ export function TemplateViewDialog({ onUpdateSuccess?.(dataToSave); } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { - // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리) const updatedRows: GenericData[] = []; let saveCount = 0; @@ -1047,7 +1139,6 @@ export function TemplateViewDialog({ const dataToSave = { ...originalRow }; let hasRowChanges = false; - // 각 매핑에 대해 해당 행의 값 확인 cellMappings.forEach(mapping => { if (mapping.dataRowIndex === i && mapping.isEditable) { const columnConfig = columnsJSON.find(col => col.key === mapping.attId); @@ -1058,8 +1149,6 @@ export function TemplateViewDialog({ 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; @@ -1069,10 +1158,8 @@ export function TemplateViewDialog({ } }); - // 변경사항이 있는 행만 저장 if (hasRowChanges) { - dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록 - + dataToSave.TAG_NO = originalRow.TAG_NO; const { success } = await updateFormDataInDB( formCode, contractItemId, @@ -1084,7 +1171,7 @@ export function TemplateViewDialog({ saveCount++; } } else { - updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지 + updatedRows.push(originalRow); } } @@ -1109,21 +1196,18 @@ export function TemplateViewDialog({ if (!isOpen) return null; - // 데이터 유효성 검사 const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0; const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length; - + return ( <Dialog open={isOpen} onOpenChange={onClose}> <DialogContent - className="w-[80%] max-w-none h-[80vh] flex flex-col" - style={{ maxWidth: "80vw" }} + className="w-[90vw] max-w-[1400px] h-[85vh] flex flex-col fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] z-50" > <DialogHeader className="flex-shrink-0"> <DialogTitle>SEDP Template - {formCode}</DialogTitle> <DialogDescription> <div className="space-y-3"> - {/* 템플릿 선택 */} {availableTemplates.length > 1 && ( <div className="flex items-center gap-4"> <span className="text-sm font-medium">Template:</span> @@ -1134,11 +1218,7 @@ export function TemplateViewDialog({ <SelectContent> {availableTemplates.map(template => ( <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> - {template.NAME} ({ - template.GRD_LST_SETUP && columnsJSON.length > 0 - ? 'GRD_LIST' - : template.TMPL_TYPE - }) + {template.NAME} ({template.TMPL_TYPE}) </SelectItem> ))} </SelectContent> @@ -1146,14 +1226,13 @@ export function TemplateViewDialog({ </div> )} - {/* 템플릿 정보 */} {selectedTemplate && ( <div className="flex items-center gap-4 text-sm"> <span className="font-medium text-blue-600"> Template Type: { - templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : - templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' : - 'Grid List View (GRD_LIST)' + templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : + templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' : + 'Grid List View (GRD_LIST)' } </span> {templateType === 'SPREAD_ITEM' && selectedRow && ( @@ -1176,7 +1255,6 @@ export function TemplateViewDialog({ </div> )} - {/* 범례 */} <div className="flex items-center gap-4 text-xs"> <span className="text-muted-foreground"> <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span> @@ -1200,8 +1278,15 @@ export function TemplateViewDialog({ </DialogDescription> </DialogHeader> - {/* SpreadSheets 컴포넌트 영역 */} - <div className="flex-1 overflow-hidden"> + <div className="flex-1 overflow-hidden relative"> + {/* 🆕 로딩 프로그레스 오버레이 */} + <LoadingProgress + phase={loadingProgress?.phase || ''} + progress={loadingProgress?.progress || 0} + total={loadingProgress?.total || 100} + isVisible={isInitializing && !!loadingProgress} + /> + {selectedTemplate && isClient && isDataValid ? ( <SpreadSheets key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} diff --git a/components/form-data/update-form-sheet.tsx b/components/form-data/update-form-sheet.tsx index 91cb7a07..6de68a1a 100644 --- a/components/form-data/update-form-sheet.tsx +++ b/components/form-data/update-form-sheet.tsx @@ -104,8 +104,10 @@ export function UpdateTagSheet({ const isFieldEditable = React.useCallback((column: DataTableColumnJSON) => { if (column.shi === true) return false; // SHI‑only - if (column.key === "TAG_NO" || column.key === "TAG_DESC") return true; - return editableFields.includes(column.key); + if (column.key === "TAG_NO" || column.key === "TAG_DESC") return false; + if (column.key === "status") return false; + // return editableFields.includes(column.key); + return true }, [editableFields]); const isFieldReadOnly = React.useCallback((column: DataTableColumnJSON) => !isFieldEditable(column), [isFieldEditable]); |
