Merge branch 'iipbs/st/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/st/dev

This commit is contained in:
2026-02-12 17:00:45 +09:00
14 changed files with 843 additions and 244 deletions
+57 -12
View File
@@ -221,7 +221,7 @@ function createOnSortingHandler(
const mergeCellCol =
mergeCellCols.find((c) => c.fieldName === "gugun") || mergeCellCols[0];
const groupField = mergeCellCol?.fieldName || null;
const orderByField = "rgnNm"; // 고정값 또는 설정 가능하게 할 수 있음
const orderByField = "displayYear"; // 고정값 또는 설정 가능하게 할 수 있음
const hasMergeCells = mergeCellCols.length > 0;
return function onSorting(
@@ -412,6 +412,7 @@ const DataGrid = ({
showHeaderCheckbox = false,
rowData,
setRowData,
getGridDataRef,
onRowClick,
}: DataGridProps) => {
const { columns, columnLayout } = columnInfo;
@@ -1030,7 +1031,46 @@ const DataGrid = ({
}
});
}, 200);
}, [columns, columnLayout, fixedColumns, rowData, showHeaderCheckbox]);
if (getGridDataRef) {
const fieldNames = columns
.map((c) => c.fieldName)
.filter(Boolean) as string[];
getGridDataRef.current = () => {
const gv = gridRef.current?.gridView as
| {
getDataSource?: () => {
getRowCount(): number;
getValue(r: number, f: string): unknown;
};
}
| undefined;
if (!gv?.getDataSource) return [];
const ds = gv.getDataSource();
if (!ds) return [];
const cnt = ds.getRowCount();
const rows: Record<string, string | number>[] = [];
for (let i = 0; i < cnt; i++) {
const r: Record<string, string | number> = {};
for (const f of fieldNames)
r[f] = ds.getValue(i, f) as string | number;
rows.push(r);
}
return rows;
};
}
return () => {
if (getGridDataRef) getGridDataRef.current = null;
};
}, [
columns,
columnLayout,
fixedColumns,
rowData,
showHeaderCheckbox,
getGridDataRef,
]);
// 행 클릭 이벤트 설정
useEffect(() => {
@@ -1117,20 +1157,25 @@ const DataGrid = ({
sortDirRef,
originalRowsRef,
);
const dp = gridRef.current?.dataProvider as
const dataProvider = gridRef.current
?.dataProvider as unknown as
| {
setFields?: (fields: unknown[]) => void;
setRows?: (
rows: Record<string, string | number>[],
) => void;
setFields?(f: unknown[]): void;
setRows?(
r: Record<string, string | number>[],
): void;
getJsonRow?(
r: number,
): Record<string, unknown>;
}
| undefined;
if (dp?.setFields) {
dp.setFields(buildFieldsFromColumns(columns));
if (dataProvider?.setFields) {
dataProvider.setFields(
buildFieldsFromColumns(columns),
);
}
// setFields 호출 직후 행 데이터 적용 (realgrid-react의 setRows 타이밍 이슈 방지)
if (dp?.setRows && rowData?.length) {
dp.setRows(rowData);
if (dataProvider?.setRows && rowData?.length) {
dataProvider.setRows(rowData);
}
}}
>
+122 -18
View File
@@ -162,7 +162,7 @@ function createOnSortingHandler(
const mergeCellCols = columns.filter((c) => c.mergeCells === true && c.fieldName);
const mergeCellCol = mergeCellCols.find((c) => c.fieldName === 'gugun') || mergeCellCols[0];
const groupField = mergeCellCol?.fieldName || null;
const orderByField = 'rgnNm'; // 고정값 또는 설정 가능하게 할 수 있음
const orderByField = 'displayYear'; // 고정값 또는 설정 가능하게 할 수 있음
const hasMergeCells = mergeCellCols.length > 0;
return function onSorting(g: unknown, fields: unknown[], _directions: unknown[]) {
@@ -306,7 +306,7 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string
};
}
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap }: TabDataGridProps) => {
const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, rows = [], showHeaderCheckbox = false, rowDataMap, setRowDataMap, activeTab: activeTabProp, onActiveTabChange }: TabDataGridProps) => {
// 각 탭별로 그리드 ref와 데이터를 관리
const gridRefs = useRef<Map<string, RealGridReact>>(new Map());
const fileInputRefs = useRef<Map<string, HTMLInputElement>>(new Map());
@@ -316,11 +316,14 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
const { setMessage: alert } = useAlertStore((state) => state);
const activeTabId = useTabStore((state) => state.activeTabId);
const menuList = useTabStore((state) => state.menuList);
const [activeTab, setActiveTab] = useState<string>(() => {
// 초기 활성 탭을 첫 번째 tabKey로 설정
const [internalActiveTab, setInternalActiveTab] = useState<string>(() => {
const firstTab = columnInfo[0];
return firstTab?.tabKey || '';
});
const isControlled = activeTabProp !== undefined;
const activeTab = isControlled ? activeTabProp : internalActiveTab;
const setActiveTab = isControlled ? (onActiveTabChange ?? (() => {})) : setInternalActiveTab;
const exportExcel = (tabKey: string) => {
const currentRowData = rowDataMap.get(tabKey) || [];
@@ -653,22 +656,115 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
}, [rowDataMap, columnInfo]);
// 활성 탭이 변경될 때마다 fitStyle 재적용 (그리드가 보일 때)
// 탭 전환 시 그리드 레이아웃·푸터·포맷 재적용 (여러 시점 + ResizeObserver)
const appliedTabRef = useRef<string | null>(null);
useEffect(() => {
const gridRef = gridRefs.current.get(activeTab);
if (!gridRef?.gridView) return;
appliedTabRef.current = null;
const currentInfo = columnInfo.find(info => info.tabKey === activeTab);
const columns = currentInfo?.columns;
const columnLayout = currentInfo?.columnLayout;
if (!columns?.length || !columnLayout) return;
const gridView = gridRef.gridView;
// 약간의 지연을 두고 설정 (그리드 렌더링 완료 후)
const timer = setTimeout(() => {
gridView.setDisplayOptions({
fitStyle: GridFitStyle.EVEN
const tryApply = () => {
if (appliedTabRef.current === activeTab) return;
const gridRef = gridRefs.current.get(activeTab);
if (!gridRef?.gridView || !gridRef?.dataProvider) return;
const gridView = gridRef.gridView;
const dp = gridRef.dataProvider as { setFields?: (fields: unknown[]) => void; setRows?: (rows: Record<string, string | number>[]) => void } | undefined;
const tabRowData = rowDataMap.get(activeTab) || [];
try {
if (dp?.setFields) dp.setFields(buildFieldsFromColumns(columns));
gridView.setDisplayOptions({ fitStyle: GridFitStyle.EVEN });
gridView.setColumnLayout?.(columnLayout as ColumnLayoutItem[]);
const hasFooter = columns.some(col => col.footer);
if (hasFooter) {
const hasArrayFooter = columns.some(col => col.footer && Array.isArray(col.footer));
const hasMultiRowFooter = columns.some(col => {
const f = col.footer;
return f && !Array.isArray(f) && 'multiRow' in f && (f as { multiRow?: boolean }).multiRow === true;
});
if (hasArrayFooter || hasMultiRowFooter) {
const lengths = columns.filter(col => col.footer && Array.isArray(col.footer)).map(col => (col.footer as unknown[]).length);
const maxLen = lengths.length ? Math.max(...lengths) : 2;
gridView.setFooters?.(Array.from({ length: maxLen }, () => ({ height: 40 })));
} else {
gridView.footer.height = 40;
}
}
gridView.setOptions?.({ summaryMode: "aggregate" });
if (columns.some(col => col.footer && Array.isArray(col.footer))) {
const columnsWithFooter: RealGridColumnInfo[] = columns.map(col => {
const c: RealGridColumnInfo = {
name: col.name,
fieldName: col.fieldName,
header: col.header,
editable: col.editable,
valueType: col.valueType,
dataFormat: col.dataFormat,
...(col.styleName != null && { styleName: col.styleName }),
...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }),
};
if (col.footer) {
if (Array.isArray(col.footer)) c.footer = col.footer;
else c.footer = { text: col.footer.text, expression: col.footer.expression, numberFormat: col.footer.numberFormat };
}
return c;
});
(gridView as GridViewWithSetColumns).setColumns?.(columnsWithFooter);
columns.forEach(col => {
if (!col.footer || !Array.isArray(col.footer)) return;
try {
const column = gridView.columnByName(col.name);
const footerArr = column?.footer as unknown[] | undefined;
if (footerArr && Array.isArray(footerArr))
col.footer.forEach((item, i) => {
if (item.valueCallback && footerArr[i])
(footerArr[i] as FooterItemWithCallback).valueCallback = item.valueCallback;
});
} catch (_) {}
});
}
columns.forEach(col => {
if (!col.footer || Array.isArray(col.footer)) return;
try {
const column = gridView.columnByName(col.name);
const fo = col.footer as { valueCallback?: (g: unknown, col: unknown, fi: number, cf: unknown, v: unknown) => unknown };
if (column?.footer && fo.valueCallback)
(column.footer as { valueCallback?: typeof fo.valueCallback }).valueCallback = fo.valueCallback;
} catch (_) {}
});
if (dp?.setRows && tabRowData.length) dp.setRows(tabRowData);
gridView.refresh();
appliedTabRef.current = activeTab;
} catch (e) {
console.warn('TabDataGrid activeTab layout re-apply:', e);
}
};
const delays = [200, 500, 1000, 1500];
const timers = delays.map(d => setTimeout(tryApply, d));
const container = document.querySelector(`[data-tab-key="${activeTab}"]`);
let observer: ResizeObserver | null = null;
if (container && typeof ResizeObserver !== 'undefined') {
observer = new ResizeObserver(() => {
if ((container as HTMLElement).offsetWidth > 0) tryApply();
});
gridView.refresh();
}, 50);
observer.observe(container);
}
return () => clearTimeout(timer);
}, [activeTab]);
return () => {
timers.forEach(t => clearTimeout(t));
observer?.disconnect();
};
}, [activeTab, columnInfo, rowDataMap]);
if (columnInfo.length === 0) {
return <div>컬럼 정보가 없습니다.</div>;
@@ -788,7 +884,9 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
}
</div>
<div className="h-[60vh]" data-tab-key={tabKey}>
{activeTab === tabKey ? (
<RealGridReact
key={tabKey}
ref={(el) => {
if (el) {
gridRefs.current.set(tabKey, el);
@@ -803,10 +901,15 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
const columns = currentInfo.columns;
const dp = el.dataProvider as { setFields?: (fields: unknown[]) => void } | undefined;
const dp = el.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 타이밍 이슈 방지)
const tabRowData = rowDataMap.get(tabKey) || [];
if (dp?.setRows && tabRowData.length) {
dp.setRows(tabRowData);
}
gridView.setDisplayOptions({
fitStyle: GridFitStyle.EVEN
@@ -1018,7 +1121,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
const onSortingHandler = createOnSortingHandler(columns, setRowData, sortDirRef, originalRowsRef, findGridElement);
(gridView as { onSorting?: (g: unknown, f: unknown[], d: unknown[]) => boolean }).onSorting = onSortingHandler;
}, 0);
}, 150);
}
} else {
gridRefs.current.delete(tabKey);
@@ -1053,6 +1156,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
/>
))}
</RealGridReact>
) : null}
</div>
</div>
</TabsContent>