diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-19 07:51:27 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-19 07:51:27 +0000 |
| commit | 9ecdfb23fe3df6a5df86782385002c562dfc1198 (patch) | |
| tree | 4188cb7e6bf2c862d9c86a59d79946bd41217227 /components/form-data/spreadJS-dialog copy 2.tsx | |
| parent | b67861fbb424c7ad47ad1538f75e2945bd8890c5 (diff) | |
(대표님) rfq 히스토리, swp 등
Diffstat (limited to 'components/form-data/spreadJS-dialog copy 2.tsx')
| -rw-r--r-- | components/form-data/spreadJS-dialog copy 2.tsx | 1002 |
1 files changed, 0 insertions, 1002 deletions
diff --git a/components/form-data/spreadJS-dialog copy 2.tsx b/components/form-data/spreadJS-dialog copy 2.tsx deleted file mode 100644 index 520362ff..00000000 --- a/components/form-data/spreadJS-dialog copy 2.tsx +++ /dev/null @@ -1,1002 +0,0 @@ -"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"; - -// SpreadSheets를 동적으로 import (SSR 비활성화) -const SpreadSheets = dynamic( - () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), - { - ssr: false, - loading: () => ( - <div className="flex items-center justify-center h-full"> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Loading SpreadSheets... - </div> - ) - } -); - -// 라이센스 키 설정을 클라이언트에서만 실행 -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<string>; - 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<string>; - ATTS: Array<{}>; - }; - SPR_ITM_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - 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; // SPREAD_ITEM용 - tableData?: GenericData[]; // SPREAD_LIST용 - formCode: string; - columnsJSON: DataTableColumnJSON[] - contractItemId: number; - editableFieldsMap?: Map<string, string[]>; - onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void; -} - -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<any>(null); - const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]); - const [isClient, setIsClient] = React.useState(false); - const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null); - const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]); - const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); - const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]); - - // 클라이언트 사이드에서만 렌더링되도록 보장 - 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]; - } - - // CONTENT가 있는 템플릿들 필터링 - const validTemplates = templates.filter(template => { - const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT; - const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT; - const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM"; - - return isValidType && (hasSpreadListContent || hasSpreadItemContent); - }); - - setAvailableTemplates(validTemplates); - - // 첫 번째 유효한 템플릿을 기본으로 선택 - if (validTemplates.length > 0 && !selectedTemplateId) { - setSelectedTemplateId(validTemplates[0].TMPL_ID); - setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); - } - }, [templateData, selectedTemplateId]); - - // 선택된 템플릿 변경 처리 - const handleTemplateChange = (templateId: string) => { - const template = availableTemplates.find(t => t.TMPL_ID === templateId); - if (template) { - setSelectedTemplateId(templateId); - setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); - setHasChanges(false); - setValidationErrors([]); - - // SpreadSheets 재초기화 - if (currentSpread) { - const template = availableTemplates.find(t => t.TMPL_ID === templateId); - if (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인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리 - if (templateType === 'SPREAD_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이면 편집 불가 - } - - // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우) - if (attId === "TAG_NO" || attId === "TAG_DESC") { - return true; - } - - // SPREAD_ITEM인 경우: editableFields 체크 - if (templateType === 'SPREAD_ITEM') { - return editableFields.includes(attId); - } - - // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려 - if (templateType === 'SPREAD_LIST') { - // 기본적으로 editableFields에 포함되어야 함 - if (!editableFields.includes(attId)) { - return false; - } - - // rowData가 제공된 경우 해당 행의 shi 상태도 확인 - if (rowData && rowData.shi === true) { - return false; - } - - return true; - } - - // 기본적으로는 editableFields 체크 - // return editableFields.includes(attId); - return true; - }, [templateType, columnsJSON, editableFields]); - - // 편집 가능한 필드 개수 계산 - const editableFieldsCount = React.useMemo(() => { - return cellMappings.filter(m => m.isEditable).length; - }, [cellMappings]); - - // 셀 주소를 행과 열로 변환하는 함수 - 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 validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => { - if (value === undefined || value === null || value === "") { - return null; // 빈 값은 별도 required 검증에서 처리 - } - - 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": - // 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; - - 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') { - // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 - 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, templateType]); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 🛠️ 헬퍼 함수들 - // ═══════════════════════════════════════════════════════════════════════════════ - - // 🎨 셀 스타일 생성 - 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; - }, []); - - -// 🎯 간소화된 드롭다운 설정 - setupSimpleValidation 완전 제거 - -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 !== '') - .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; - } - - 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 인스턴스 생성 - 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); - - // ComboBox + DataValidation 둘 다 적용 - 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}]`); - - } catch (cellError) { - console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError); - } - } - - console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`); - - } catch (error) { - console.error('❌ Dropdown setup failed:', error); - } -}, []); - // 🚀 행 용량 확보 - 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`); - } - }, []); - - // 🛡️ 시트 보호 및 이벤트 설정 - 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, - allowResizeRows: false, - allowResizeColumns: 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} at ${exactMapping.cellAddress}`); - - // 기본 편집 권한 확인 - 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 개별 행 SHI 확인 - if (templateType === 'SPREAD_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) { - 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; - } - } 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}`); - - // ✅ 정확한 매핑 찾기 - 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; - } - - 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"; - 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}. Please correct the value.`, { duration: 5000 }); - } else { - console.log(`✅ Validation passed`); - - // ✅ 정상 스타일 복원 - const normalStyle = createCellStyle(exactMapping.isEditable); - activeSheet.setStyle(info.row, info.col, normalStyle); - cell.locked(!exactMapping.isEditable); - } - } - }); - - console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`); - }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 🏗️ 메인 SpreadSheets 초기화 함수 - // ═══════════════════════════════════════════════════════════════════════════════ - - const initSpread = React.useCallback((spread: any, template?: TemplateItem) => { - const workingTemplate = template || selectedTemplate; - if (!spread || !workingTemplate) return; - - try { - // 🔄 초기 설정 - setCurrentSpread(spread); - setHasChanges(false); - setValidationErrors([]); - - // 📋 템플릿 콘텐츠 및 데이터 시트 추출 - 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); - } - - if (!contentJson) { - console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); - return; - } - - // 🏗️ SpreadSheets 초기화 - const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - - // 성능을 위한 렌더링 일시 중단 - spread.suspendPaint(); - - try { - // 템플릿 구조 로드 - spread.fromJSON(jsonData); - const activeSheet = spread.getActiveSheet(); - - // 시트 보호 해제 (편집을 위해) - activeSheet.options.isProtected = false; - - // 📊 셀 매핑 및 데이터 처리 - if (dataSheets && dataSheets.length > 0) { - const mappings: CellMapping[] = []; - - // 🔄 각 데이터 시트의 매핑 정보 처리 - dataSheets.forEach(dataSheet => { - if (dataSheet.MAP_CELL_ATT) { - dataSheet.MAP_CELL_ATT.forEach(mapping => { - 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})`); - } - } - }); - } - }); - - // 💾 매핑 정보 저장 및 이벤트 설정 - setCellMappings(mappings); - setupSheetProtectionAndEvents(activeSheet, mappings); - } - - } finally { - // 렌더링 재개 - spread.resumePaint(); - } - - } catch (error) { - console.error('❌ Error initializing spread:', error); - toast.error('Failed to load template'); - if (spread?.resumePaint) { - spread.resumePaint(); - } - } - }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]); - - // 변경사항 저장 함수 - 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' && 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; // 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 ( - <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent - className="w-[80%] max-w-none h-[80vh] flex flex-col" - style={{ maxWidth: "80vw" }} - > - <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> - <Select value={selectedTemplateId} onValueChange={handleTemplateChange}> - <SelectTrigger className="w-64"> - <SelectValue placeholder="Select a template" /> - </SelectTrigger> - <SelectContent> - {availableTemplates.map(template => ( - <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> - {template.NAME} ({template.TMPL_TYPE}) - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - )} - - {/* 템플릿 정보 */} - {selectedTemplate && ( - <div className="flex items-center gap-4 text-sm"> - <span className="font-medium text-blue-600"> - Template Type: {selectedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'} - </span> - {templateType === 'SPREAD_ITEM' && selectedRow && ( - <span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span> - )} - {templateType === 'SPREAD_LIST' && ( - <span>• {dataCount} rows</span> - )} - {hasChanges && ( - <span className="text-orange-600 font-medium"> - • Unsaved changes - </span> - )} - {validationErrors.length > 0 && ( - <span className="text-red-600 font-medium flex items-center"> - <AlertTriangle className="w-4 h-4 mr-1" /> - {validationErrors.length} validation errors - </span> - )} - </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> - Editable fields - </span> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span> - Read-only fields - </span> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-red-100 border border-red-300 mr-1"></span> - Validation errors - </span> - {cellMappings.length > 0 && ( - <span className="text-blue-600"> - {editableFieldsCount} of {cellMappings.length} fields editable - </span> - )} - </div> - </div> - </DialogDescription> - </DialogHeader> - - {/* SpreadSheets 컴포넌트 영역 */} - <div className="flex-1 overflow-hidden"> - {selectedTemplate && isClient && isDataValid ? ( - <SpreadSheets - key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} - workbookInitialized={initSpread} - hostStyle={hostStyle} - /> - ) : ( - <div className="flex items-center justify-center h-full text-muted-foreground"> - {!isClient ? ( - <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Loading... - </> - ) : !selectedTemplate ? ( - "No template available" - ) : !isDataValid ? ( - `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available` - ) : ( - "Template not ready" - )} - </div> - )} - </div> - - <DialogFooter className="flex-shrink-0"> - <div className="flex items-center gap-2"> - <Button variant="outline" onClick={onClose}> - Close - </Button> - - {hasChanges && ( - <Button - variant="default" - onClick={handleSaveChanges} - disabled={isPending || validationErrors.length > 0} - > - {isPending ? ( - <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Saving... - </> - ) : ( - <> - <Save className="mr-2 h-4 w-4" /> - Save Changes - </> - )} - </Button> - )} - - {validationErrors.length > 0 && ( - <Button - variant="outline" - onClick={validateAllData} - className="text-red-600 border-red-300 hover:bg-red-50" - > - <AlertTriangle className="mr-2 h-4 w-4" /> - Check Errors ({validationErrors.length}) - </Button> - )} - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -}
\ No newline at end of file |
