Merge remote-tracking branch 'origin/iipbs/st/dev' into feature/review-skeleton-sein
This commit is contained in:
@@ -3,7 +3,7 @@ import { ValueType } from "realgrid";
|
||||
|
||||
import RealGrid, { GridFitStyle, type DisplayOptions } from "realgrid";
|
||||
import { RealGridReact, RGDataColumn } from "realgrid-react";
|
||||
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback } from "../type/DataGrid.type";
|
||||
import type { DataGridProps, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, ColumnLayoutItem } from "../type/DataGrid.type";
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks";
|
||||
import JSZip from "jszip";
|
||||
import * as XLSX from "xlsx";
|
||||
@@ -315,12 +315,94 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport
|
||||
.find((s) => s.menuEngNm === activeTabId)
|
||||
?.menuKornNm || 'export';
|
||||
|
||||
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const originalHeaders: Map<string, string> = new Map();
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header) {
|
||||
const originalText = gridCol.header.text || '';
|
||||
originalHeaders.set(col.name, originalText);
|
||||
const headerWithFieldName = `${originalText} (${col.fieldName})`;
|
||||
gridCol.header.text = headerWithFieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
|
||||
const normalizeLayoutForExport = (layout: ColumnLayoutItem[]): ColumnLayoutItem[] => {
|
||||
return layout.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
// { column: string, footerUserSpans?: ... } 형태인 경우
|
||||
if ('column' in item && typeof item.column === 'string') {
|
||||
const colItem = item as { column: string; footerUserSpans?: unknown[] };
|
||||
// col 속성 추가 (RealGrid가 exportGrid에서 필요로 함)
|
||||
return {
|
||||
...colItem,
|
||||
col: gridView.columnByName?.(colItem.column) || null,
|
||||
} as ColumnLayoutItem;
|
||||
}
|
||||
// { name: string, items: ... } 형태인 경우
|
||||
if ('items' in item && Array.isArray(item.items)) {
|
||||
return {
|
||||
...item,
|
||||
items: normalizeLayoutForExport(item.items as ColumnLayoutItem[]),
|
||||
};
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const originalLayout = columnLayout;
|
||||
let normalizedLayout: ColumnLayoutItem[] | undefined;
|
||||
let layoutRestored = false;
|
||||
|
||||
// footerUserSpans가 있는 경우에만 정규화
|
||||
const checkFooterUserSpans = (layout: ColumnLayoutItem[]): boolean => {
|
||||
return layout.some(item => {
|
||||
if (typeof item === 'string') return false;
|
||||
if (item && typeof item === 'object' && 'footerUserSpans' in item) return true;
|
||||
if (item && typeof item === 'object' && 'items' in item && Array.isArray(item.items)) {
|
||||
return checkFooterUserSpans(item.items as ColumnLayoutItem[]);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
if (checkFooterUserSpans(columnLayout)) {
|
||||
normalizedLayout = normalizeLayoutForExport(columnLayout);
|
||||
// 정규화된 layout을 임시로 설정
|
||||
gridView.setColumnLayout?.(normalizedLayout);
|
||||
}
|
||||
|
||||
gridView.exportGrid({
|
||||
type: 'excel',
|
||||
target: 'local',
|
||||
fileName: `${menuName}.xlsx`,
|
||||
footer: 'hidden',
|
||||
done: () => {
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header && originalHeaders.has(col.name)) {
|
||||
gridCol.header.text = originalHeaders.get(col.name) || '';
|
||||
}
|
||||
}
|
||||
});
|
||||
// columnLayout 원복 (footerUserSpans 유지)
|
||||
if (normalizedLayout && !layoutRestored) {
|
||||
layoutRestored = true;
|
||||
setTimeout(() => {
|
||||
if (gridRef.current?.gridView && originalLayout) {
|
||||
gridRef.current.gridView.setColumnLayout?.(originalLayout);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
alert('내보내기가 완료되었습니다.');
|
||||
},
|
||||
});
|
||||
@@ -390,19 +472,27 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport
|
||||
const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow;
|
||||
const dataStartRow = maxHeaderRowIndex + 1;
|
||||
|
||||
// 헤더 텍스트를 fieldName으로 매핑하는 맵 생성
|
||||
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
|
||||
const headerToFieldNameMap = new Map<string, string>();
|
||||
headers.forEach((headerText) => {
|
||||
// columns에서 header.text가 일치하는 컬럼 찾기
|
||||
const matchedColumn = columns.find(
|
||||
(col) => col.header?.text === headerText || col.fieldName === headerText
|
||||
);
|
||||
if (matchedColumn?.fieldName) {
|
||||
headerToFieldNameMap.set(headerText, matchedColumn.fieldName);
|
||||
} else {
|
||||
// 매칭되지 않으면 원본 헤더 텍스트 사용 (하위 호환성)
|
||||
headerToFieldNameMap.set(headerText, headerText);
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
const extractedFieldName = fieldNameMatch[1];
|
||||
if (columns.find(c => c.fieldName === extractedFieldName)) {
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
|
||||
// 데이터 행 읽기
|
||||
@@ -692,10 +782,14 @@ const DataGrid = ({ columnInfo, rowStartNum, isExcelImport = true, isExcelExport
|
||||
rows={rowData}
|
||||
onInitialized={(grid) => {
|
||||
(grid as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef);
|
||||
const dp = gridRef.current?.dataProvider as { setFields?: (fields: unknown[]) => void } | undefined;
|
||||
const dp = gridRef.current?.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record<string, string | number>[]) => void } | undefined;
|
||||
if (dp?.setFields) {
|
||||
dp.setFields(buildFieldsFromColumns(columns));
|
||||
}
|
||||
// setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지)
|
||||
if (dp?.setRows && rowData?.length) {
|
||||
dp.setRows(rowData);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ValueType } from "realgrid"
|
||||
|
||||
import RealGrid, { GridFitStyle } from "realgrid"
|
||||
import { RealGridReact, RGDataColumn } from "realgrid-react"
|
||||
import type { TabDataGridProps, ColumnInfo, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback } from "../type/DataGrid.type"
|
||||
import type { TabDataGridProps, ColumnInfo, RealGridColumnInfo, GridViewWithSetColumns, FooterItemWithCallback, ColumnLayoutItem } from "../type/DataGrid.type"
|
||||
import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"
|
||||
import JSZip from "jszip"
|
||||
import * as XLSX from "xlsx"
|
||||
@@ -151,18 +151,18 @@ function updateHeaderSortIndicator(
|
||||
/** onSorting: mergeCells: true인 컬럼이 있으면 그룹 내 순서 유지 정렬, 없으면 RealGrid 기본 정렬 */
|
||||
function createOnSortingHandler(
|
||||
columns: { fieldName?: string; name: string; mergeCells?: boolean; sortdisable?: boolean; valueType?: ValueType }[],
|
||||
setRowData: (updater: React.SetStateAction<Record<string, string | number>[]>) => void,
|
||||
sortDirRef: React.MutableRefObject<Record<string, "asc" | "desc" | "none">>,
|
||||
originalRowsRef: React.MutableRefObject<Record<string, Record<string, string | number>[]>>,
|
||||
setRowData: React.Dispatch<React.SetStateAction<Record<string, string | number>[]>>,
|
||||
sortDirRef: { current: Record<string, "asc" | "desc" | "none"> },
|
||||
originalRowsRef: { current: Record<string, Record<string, string | number>[]> },
|
||||
findGridElement?: () => HTMLElement | null
|
||||
) {
|
||||
const fieldNames = [...new Set(columns.map((c) => c.fieldName).filter((x): x is string => typeof x === "string"))];
|
||||
// mergeCells: true인 컬럼 찾기 (시군구 등) - 그룹 필드로 사용
|
||||
// 시군구를 우선으로 찾고, 없으면 첫 번째 mergeCells 컬럼 사용
|
||||
const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName);
|
||||
const mergeCellCol = mergeCellCols.find((c) => c.fieldName === '시군구') || mergeCellCols[0];
|
||||
const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0];
|
||||
const groupField = mergeCellCol?.fieldName || null;
|
||||
const orderByField = '구분'; // 고정값 또는 설정 가능하게 할 수 있음
|
||||
const orderByField = 'rgnNm'; // 고정값 또는 설정 가능하게 할 수 있음
|
||||
const hasMergeCells = mergeCellCols.length > 0;
|
||||
|
||||
return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) {
|
||||
@@ -341,13 +341,101 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
?.menuKornNm || 'export';
|
||||
|
||||
const columnInfoItem = columnInfo.find(info => info.tabKey === tabKey);
|
||||
const tabLabel = columnInfoItem?.tabLabel || tabKey;
|
||||
if (!columnInfoItem) return;
|
||||
|
||||
const tabLabel = columnInfoItem.tabLabel || tabKey;
|
||||
const columns = columnInfoItem.columns || [];
|
||||
const columnLayout = columnInfoItem.columnLayout || [];
|
||||
|
||||
// 엑셀 import 시 매칭을 위해 헤더에 fieldName 포함 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const originalHeaders: Map<string, string> = new Map();
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header) {
|
||||
const originalText = gridCol.header.text || '';
|
||||
originalHeaders.set(col.name, originalText);
|
||||
const headerWithFieldName = `${originalText} (${col.fieldName})`;
|
||||
gridCol.header.text = headerWithFieldName;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// exportGrid 호출 전에 columnLayout 정규화 (col 속성 추가)
|
||||
const normalizeLayoutForExport = (layout: ColumnLayoutItem[]): ColumnLayoutItem[] => {
|
||||
return layout.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
// { column: string, footerUserSpans?: ... } 형태인 경우
|
||||
if ('column' in item && typeof item.column === 'string') {
|
||||
const colItem = item as { column: string; footerUserSpans?: unknown[] };
|
||||
// col 속성 추가 (RealGrid가 exportGrid에서 필요로 함)
|
||||
return {
|
||||
...colItem,
|
||||
col: gridView.columnByName?.(colItem.column) || null,
|
||||
} as ColumnLayoutItem;
|
||||
}
|
||||
// { name: string, items: ... } 형태인 경우
|
||||
if ('items' in item && Array.isArray(item.items)) {
|
||||
return {
|
||||
...item,
|
||||
items: normalizeLayoutForExport(item.items as ColumnLayoutItem[]),
|
||||
};
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
const originalLayout = columnLayout;
|
||||
let normalizedLayout: ColumnLayoutItem[] | undefined;
|
||||
let layoutRestored = false;
|
||||
|
||||
// footerUserSpans가 있는 경우에만 정규화
|
||||
const checkFooterUserSpans = (layout: ColumnLayoutItem[]): boolean => {
|
||||
return layout.some(item => {
|
||||
if (typeof item === 'string') return false;
|
||||
if (item && typeof item === 'object' && 'footerUserSpans' in item) return true;
|
||||
if (item && typeof item === 'object' && 'items' in item && Array.isArray(item.items)) {
|
||||
return checkFooterUserSpans(item.items as ColumnLayoutItem[]);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
if (checkFooterUserSpans(columnLayout)) {
|
||||
normalizedLayout = normalizeLayoutForExport(columnLayout);
|
||||
// 정규화된 layout을 임시로 설정
|
||||
gridView.setColumnLayout?.(normalizedLayout);
|
||||
}
|
||||
|
||||
gridView.exportGrid({
|
||||
type: 'excel',
|
||||
target: 'local',
|
||||
fileName: `${menuName}_${tabLabel}.xlsx`,
|
||||
footer: 'hidden',
|
||||
done: () => {
|
||||
// 헤더 원복
|
||||
columns.forEach((col) => {
|
||||
if (col.fieldName && gridView.columnByName) {
|
||||
const gridCol = gridView.columnByName(col.name);
|
||||
if (gridCol && gridCol.header && originalHeaders.has(col.name)) {
|
||||
gridCol.header.text = originalHeaders.get(col.name) || '';
|
||||
}
|
||||
}
|
||||
});
|
||||
// columnLayout 원복 (footerUserSpans 유지)
|
||||
if (normalizedLayout && !layoutRestored) {
|
||||
layoutRestored = true;
|
||||
setTimeout(() => {
|
||||
const currentGridRef = gridRefs.current.get(tabKey);
|
||||
if (currentGridRef?.gridView && originalLayout) {
|
||||
currentGridRef.gridView.setColumnLayout?.(originalLayout);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
alert('내보내기가 완료되었습니다.');
|
||||
},
|
||||
});
|
||||
@@ -423,20 +511,28 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
|
||||
const maxHeaderRowIndex = headerRowIndexes.length > 0 ? Math.max(...headerRowIndexes) : headerStartRow;
|
||||
const dataStartRow = maxHeaderRowIndex + 1;
|
||||
|
||||
// 헤더 텍스트를 fieldName으로 매핑하는 맵 생성
|
||||
// 엑셀 컬럼 인덱스 → 그리드 fieldName 매핑
|
||||
const headerToFieldNameMap = new Map<string, string>();
|
||||
const columns = columnInfoItem.columns || [];
|
||||
headers.forEach((headerText) => {
|
||||
// columns에서 header.text가 일치하는 컬럼 찾기
|
||||
const matchedColumn = columns.find(
|
||||
(col) => col.header?.text === headerText || col.fieldName === headerText
|
||||
);
|
||||
if (matchedColumn?.fieldName) {
|
||||
headerToFieldNameMap.set(headerText, matchedColumn.fieldName);
|
||||
} else {
|
||||
// 매칭되지 않으면 원본 헤더 텍스트 사용 (하위 호환성)
|
||||
headerToFieldNameMap.set(headerText, headerText);
|
||||
headers.forEach((headerText, colIndex) => {
|
||||
let fieldName: string | null = null;
|
||||
|
||||
// 헤더에 fieldName이 포함된 경우 (예: "합계 (swgeProcZoneInnerSm)")
|
||||
const fieldNameMatch = headerText.match(/\(([a-zA-Z_][a-zA-Z0-9_]*)\)$/);
|
||||
if (fieldNameMatch && fieldNameMatch[1]) {
|
||||
const extractedFieldName = fieldNameMatch[1];
|
||||
if (columns.find(c => c.fieldName === extractedFieldName)) {
|
||||
fieldName = extractedFieldName;
|
||||
}
|
||||
}
|
||||
|
||||
// 매칭 실패 시 원본 헤더 텍스트 사용
|
||||
if (!fieldName) {
|
||||
console.warn(`엑셀 컬럼 ${colIndex + 1} (헤더: "${headerText}") 매칭 실패 → 헤더 텍스트를 fieldName으로 사용`);
|
||||
fieldName = headerText;
|
||||
}
|
||||
|
||||
headerToFieldNameMap.set(headerText, fieldName);
|
||||
});
|
||||
|
||||
// 데이터 행 읽기
|
||||
|
||||
@@ -35,7 +35,16 @@ export const SeweragePenetrationRateInpColumn = {
|
||||
{ name: 'popltnSm', fieldName: 'popltnSm', header: { text: '총인구(명)' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0' },
|
||||
|
||||
// 총면적
|
||||
{ name: 'arSm', fieldName: 'arSm', header: { text: '총면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' },
|
||||
{
|
||||
name: 'arSm',
|
||||
fieldName: 'arSm',
|
||||
header: { text: '총면적(㎢)' },
|
||||
footer: footerDecimal3,
|
||||
valueType: ValueType.NUMBER,
|
||||
dataFormat: '#,##0.000',
|
||||
computed: { fields: ['swgeProcZoneInnerAr', 'swgeProcZoneOuterAr'], type: 'sum' as const },
|
||||
styleName: 'rg-cell-total',
|
||||
},
|
||||
|
||||
// 하수처리구역 내 > 합계
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user