summaryrefslogtreecommitdiff
path: root/components/form-data/spreadJS-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data/spreadJS-dialog.tsx')
-rw-r--r--components/form-data/spreadJS-dialog.tsx736
1 files changed, 456 insertions, 280 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 1d0796fe..11d37911 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -119,7 +119,7 @@ export function TemplateViewDialog({
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 [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null>(null);
const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]);
const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]);
@@ -140,21 +140,35 @@ export function TemplateViewDialog({
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";
+ 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);
+ return isValidType && (hasSpreadListContent || hasSpreadItemContent || hasGrdListSetup);
});
setAvailableTemplates(validTemplates);
// 첫 번째 유효한 템플릿을 기본으로 선택
if (validTemplates.length > 0 && !selectedTemplateId) {
- setSelectedTemplateId(validTemplates[0].TMPL_ID);
- setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
+ const firstTemplate = validTemplates[0];
+ 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]);
@@ -163,7 +177,16 @@ export function TemplateViewDialog({
const template = availableTemplates.find(t => t.TMPL_ID === templateId);
if (template) {
setSelectedTemplateId(templateId);
- setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
+
+ // 템플릿 타입 결정
+ 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([]);
@@ -192,8 +215,8 @@ export function TemplateViewDialog({
return editableFieldsMap.get(selectedRow.TAG_NO) || [];
}
- // SPREAD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리
- if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
+ // 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) || [];
@@ -221,8 +244,8 @@ export function TemplateViewDialog({
return editableFields.includes(attId);
}
- // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려
- if (templateType === 'SPREAD_LIST') {
+ // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려
+ if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') {
// 기본적으로 editableFields에 포함되어야 함
if (!editableFields.includes(attId)) {
return false;
@@ -236,8 +259,6 @@ export function TemplateViewDialog({
return true;
}
- // 기본적으로는 editableFields 체크
- // return editableFields.includes(attId);
return true;
}, [templateType, columnsJSON, editableFields]);
@@ -266,6 +287,17 @@ export function TemplateViewDialog({
return { row, col };
};
+ // 행과 열을 셀 주소로 변환하는 함수 (GRD_LIST용)
+ 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 === "") {
@@ -322,7 +354,7 @@ export function TemplateViewDialog({
message: errorMessage
});
}
- } else if (templateType === 'SPREAD_LIST') {
+ } else if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') {
// 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴
const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
@@ -359,164 +391,282 @@ export function TemplateViewDialog({
return style;
}, []);
- // 📋 최적화된 LIST 드롭다운 설정
-const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
- try {
- console.log(`🎯 Setting up DataValidation dropdown for ${rowCount} rows with options:`, options);
-
- // 🚨 성능 임계점 확인
- if (rowCount > 100) {
- console.warn(`⚡ Large dataset (${rowCount} rows): Using simple validation only`);
- setupSimpleValidation(activeSheet, cellPos, options, rowCount);
- return;
- }
-
- // ✅ 1단계: options 철저하게 정규화 (이것이 에러 방지의 핵심!)
- let safeOptions;
+ // 🎯 드롭다운 설정
+ const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
try {
- safeOptions = options
- .filter(opt => opt !== null && opt !== undefined && opt !== '') // null, undefined, 빈값 제거
- .map(opt => {
- // 모든 값을 안전한 문자열로 변환
- let str = String(opt);
- // 특수 문자나 문제 있는 문자 처리
- str = str.replace(/[\r\n\t]/g, ' '); // 줄바꿈, 탭을 공백으로
- str = str.replace(/[^\x20-\x7E\u00A1-\uFFFF]/g, ''); // 제어 문자 제거
- return str.trim();
- })
- .filter(opt => opt.length > 0 && opt.length < 255) // 빈값과 너무 긴 값 제거
- .filter((opt, index, arr) => arr.indexOf(opt) === index) // 중복 제거
- .slice(0, 100); // 최대 100개로 제한
-
- console.log(`📋 Original options:`, options);
- console.log(`📋 Safe options:`, safeOptions);
- } catch (filterError) {
- console.error('❌ Options filtering failed:', filterError);
- safeOptions = ['Option1', 'Option2']; // 안전한 폴백 옵션
- }
+ 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;
+ }
- if (safeOptions.length === 0) {
- console.warn(`⚠️ No valid options found, using fallback`);
- safeOptions = ['Please Select'];
- }
+ console.log(`📋 Safe options:`, safeOptions);
- // ✅ 2단계: DataValidation 생성 (엑셀 스타일 드롭다운)
- let validator;
- try {
- validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions);
- console.log(`✅ DataValidation validator created successfully`);
- } catch (validatorError) {
- console.error('❌ Failed to create validator:', validatorError);
- return;
- }
+ // ✅ DataValidation용 문자열 준비
+ const optionsString = safeOptions.join(',');
- // ✅ 3단계: 셀/범위에 적용
- try {
- if (rowCount > 1) {
- // 범위에 적용
- const range = activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1);
- range.dataValidator(validator);
- console.log(`✅ DataValidation applied to range [${cellPos.row}, ${cellPos.col}, ${rowCount}, 1]`);
- } else {
- // 단일 셀에 적용
- activeSheet.setDataValidator(cellPos.row, cellPos.col, validator);
- console.log(`✅ DataValidation applied to single cell [${cellPos.row}, ${cellPos.col}]`);
- }
- } catch (applicationError) {
- console.error('❌ Failed to apply DataValidation:', applicationError);
-
- // 폴백: 개별 셀에 하나씩 적용
- console.log('🔄 Trying individual cell application...');
- for (let i = 0; i < Math.min(rowCount, 50); i++) {
+ // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성!
+ for (let i = 0; i < rowCount; i++) {
try {
- const individualValidator = GC.Spread.Sheets.DataValidation.createListValidator([...safeOptions]);
- activeSheet.setDataValidator(cellPos.row + i, cellPos.col, individualValidator);
- console.log(`✅ Individual DataValidation set for row ${cellPos.row + i}`);
- } catch (individualError) {
- console.warn(`⚠️ Failed individual cell ${cellPos.row + i}:`, individualError);
+ 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(`✅ DataValidation dropdown setup completed`);
+ console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`);
- } catch (error) {
- console.error('❌ DataValidation setup failed completely:', error);
- console.error('Error stack:', error.stack);
- console.log('🔄 Falling back to no validation');
- }
-}, []);
+ } 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 setupSimpleValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
- try {
- console.log(`⚡ Setting up simple validation (no dropdown UI) for ${rowCount} rows`);
+ // 🆕 GRD_LIST용 동적 테이블 생성 함수
+ const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => {
+ console.log('🏗️ Creating GRD_LIST table');
- const safeOptions = options
- .filter(opt => opt != null && opt !== '')
- .map(opt => String(opt).trim())
- .filter(opt => opt.length > 0);
+ // 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;
+ });
- if (safeOptions.length === 0) return;
+ console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`));
- const validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions);
+ if (visibleColumns.length === 0) {
+ console.warn('❌ No visible columns found in columnsJSON');
+ return [];
+ }
+
+ // 테이블 생성 시작
+ const mappings: CellMapping[] = [];
+ const startCol = 1; // A열 제외하고 B열부터 시작
- // 범위로 적용 시도
- try {
- activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1).dataValidator(validator);
- console.log(`✅ Simple validation applied to range`);
- } catch (rangeError) {
- console.warn('Range validation failed, trying individual cells');
- // 폴백: 개별 적용
- for (let i = 0; i < Math.min(rowCount, 100); i++) {
- try {
- activeSheet.setDataValidator(cellPos.row + i, cellPos.col, validator);
- } catch (individualError) {
- // 개별 실패해도 계속
+ // 🔍 그룹 헤더 분석
+ 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}`);
+ });
}
-
- } catch (error) {
- console.error('❌ Simple validation failed:', error);
- }
-}, []);
-
-// ═══════════════════════════════════════════════════════════════════════════════
-// 🔍 디버깅용: 에러 발생 원인 추적
-// ═══════════════════════════════════════════════════════════════════════════════
-
-const debugDropdownError = (options: any[], attId: string) => {
- console.group(`🔍 Debugging dropdown for ${attId}`);
-
- console.log('Original options type:', typeof options);
- console.log('Is array:', Array.isArray(options));
- console.log('Length:', options?.length);
- console.log('Raw options:', options);
-
- if (Array.isArray(options)) {
- options.forEach((opt, index) => {
- console.log(`[${index}] Type: ${typeof opt}, Value: "${opt}", String: "${String(opt)}"`);
+
+ // 데이터 행 생성
+ const dataRowCount = tableData.length;
+ ensureRowCapacity(activeSheet, dataStartRow + dataRowCount);
+
+ tableData.forEach((rowData, rowIndex) => {
+ const targetRow = dataStartRow + rowIndex;
- // 문제 있는 값 체크
- if (opt === null) console.warn(` ⚠️ NULL value at index ${index}`);
- if (opt === undefined) console.warn(` ⚠️ UNDEFINED value at index ${index}`);
- if (typeof opt === 'object') console.warn(` ⚠️ OBJECT value at index ${index}:`, opt);
- if (typeof opt === 'string' && opt.includes('\n')) console.warn(` ⚠️ NEWLINE in string at index ${index}`);
- if (typeof opt === 'string' && opt.length === 0) console.warn(` ⚠️ EMPTY STRING at index ${index}`);
+ visibleColumns.forEach((column, colIndex) => {
+ const targetCol = startCol + colIndex;
+ const cellAddress = getCellAddress(targetRow, targetCol);
+ const isEditable = isFieldEditable(column.key, rowData);
+
+ // 매핑 정보 추가
+ mappings.push({
+ attId: column.key,
+ cellAddress: cellAddress,
+ isEditable: isEditable,
+ 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'})`);
+
+ // LIST 타입 드롭다운 설정
+ if (column.type === "LIST" && column.options && isEditable) {
+ setupOptimizedListValidation(activeSheet, { row: targetRow, col: targetCol }, column.options, 1);
+ }
+ });
});
- }
-
- console.groupEnd();
-};
- // 🚀 행 용량 확보
- 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`);
+ 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[];
+ }> = [];
+
+ 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]
+ });
+ i++;
+ continue;
+ }
+
+ // 같은 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++;
+ }
+
+ // 그룹 추가
+ groups.push({
+ head: groupHead,
+ isGroup: groupColumns.length > 1,
+ columns: groupColumns
+ });
+
+ i = j; // 다음 그룹으로 이동
}
+
+ return { groups };
}, []);
// 🛡️ 시트 보호 및 이벤트 설정
@@ -574,8 +724,8 @@ const debugDropdownError = (options: any[], attId: string) => {
return;
}
- // SPREAD_LIST 개별 행 SHI 확인
- if (templateType === 'SPREAD_LIST' && exactMapping.dataRowIndex !== undefined) {
+ // 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}`);
@@ -663,149 +813,167 @@ const debugDropdownError = (options: any[], attId: string) => {
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[] = [];
+ let mappings: CellMapping[] = [];
+
+ // 🆕 GRD_LIST 처리
+ if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) {
+ console.log('🏗️ Processing GRD_LIST template');
- // 🔄 각 데이터 시트의 매핑 정보 처리
- 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);
- }
+ // 기본 워크북 설정
+ spread.clearSheets();
+ spread.addSheet(0);
+ const sheet = spread.getSheet(0);
+ sheet.name('Data');
+ spread.setActiveSheet('Data');
+
+ // 동적 테이블 생성
+ mappings = createGrdListTable(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);
+ }
- } 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);
+ if (!contentJson) {
+ console.warn('❌ No CONTENT found in template:', workingTemplate.NAME);
+ return;
+ }
+
+ // 🏗️ SpreadSheets 초기화
+ const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
+
+ // 템플릿 구조 로드
+ spread.fromJSON(jsonData);
+
+ // 📊 셀 매핑 및 데이터 처리
+ if (dataSheets && dataSheets.length > 0) {
+
+ // 🔄 각 데이터 시트의 매핑 정보 처리
+ 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: targetCellAddress, // 각 행마다 다른 주소
- isEditable: cellEditable,
- dataRowIndex: index // 원본 데이터 인덱스
+ cellAddress: IN,
+ isEditable: isEditable,
+ dataRowIndex: 0
});
-
- 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];
+ const cell = activeSheet.getCell(cellPos.row, cellPos.col);
+ const value = selectedRow[ATT_ID];
// 값 설정
cell.value(value ?? null);
- console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`);
+
+ // 🎨 스타일 및 편집 권한 설정
+ 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`);
- // 편집 권한 및 스타일 설정
- const cellEditable = isFieldEditable(ATT_ID, rowData);
- cell.locked(!cellEditable);
- const style = createCellStyle(cellEditable);
- activeSheet.setStyle(targetRow, cellPos.col, style);
- });
- }
+ // 🚀 행 확장 (필요시)
+ 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'})`);
+ });
- console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`);
- }
- }
- });
- }
- });
+ // 📋 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);
+ }
+ }
- // 💾 매핑 정보 저장 및 이벤트 설정
- setCellMappings(mappings);
- setupSheetProtectionAndEvents(activeSheet, mappings);
+ // 🎨 개별 셀 데이터 및 스타일 설정
+ 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();
@@ -818,7 +986,7 @@ const debugDropdownError = (options: any[], attId: string) => {
spread.resumePaint();
}
}
- }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]);
+ }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable]);
// 변경사항 저장 함수
const handleSaveChanges = React.useCallback(async () => {
@@ -869,8 +1037,8 @@ const debugDropdownError = (options: any[], attId: string) => {
toast.success("Changes saved successfully!");
onUpdateSuccess?.(dataToSave);
- } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
- // 복수 행 저장
+ } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) {
+ // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리)
const updatedRows: GenericData[] = [];
let saveCount = 0;
@@ -966,7 +1134,11 @@ const debugDropdownError = (options: any[], attId: string) => {
<SelectContent>
{availableTemplates.map(template => (
<SelectItem key={template.TMPL_ID} value={template.TMPL_ID}>
- {template.NAME} ({template.TMPL_TYPE})
+ {template.NAME} ({
+ template.GRD_LST_SETUP && columnsJSON.length > 0
+ ? 'GRD_LIST'
+ : template.TMPL_TYPE
+ })
</SelectItem>
))}
</SelectContent>
@@ -978,12 +1150,16 @@ const debugDropdownError = (options: any[], attId: string) => {
{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)'}
+ Template Type: {
+ templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' :
+ templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' :
+ 'Grid List View (GRD_LIST)'
+ }
</span>
{templateType === 'SPREAD_ITEM' && selectedRow && (
<span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
)}
- {templateType === 'SPREAD_LIST' && (
+ {(templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && (
<span>• {dataCount} rows</span>
)}
{hasChanges && (
@@ -1028,7 +1204,7 @@ const debugDropdownError = (options: any[], attId: string) => {
<div className="flex-1 overflow-hidden">
{selectedTemplate && isClient && isDataValid ? (
<SpreadSheets
- key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
+ key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
workbookInitialized={initSpread}
hostStyle={hostStyle}
/>