st: fix & refactor: TabDataGrid에서 텝 전환시 컬럼 포맷 및 footer 사라지는 오류 수정, 통계 검토 - 하수 관로 검색 로직 추가

This commit is contained in:
2026-02-12 16:40:03 +09:00
parent 12bed29eb9
commit 2873d1b0b9
3 changed files with 168 additions and 36 deletions
+109 -13
View File
@@ -656,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>;
@@ -791,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);
@@ -1026,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);
@@ -1061,6 +1156,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r
/>
))}
</RealGridReact>
) : null}
</div>
</div>
</TabsContent>
@@ -89,6 +89,7 @@ const SeweragePenetrationRateInp = () => {
...d,
sido: commonInfo.sido,
speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr,
piplSupprt: commonInfo.piplSupprt,
}));
setRowData(toGridRows(convertedData));
@@ -1,4 +1,4 @@
import { useState, useMemo, act } from "react";
import { useState, useMemo, useRef } from "react";
import TabDataGrid from "@/components/module/TabDataGrid";
import SearchForm from "@/components/module/SearchForm";
import SaveButton from "@/components/button/SaveButton";
@@ -10,20 +10,23 @@ import { LoadingWithAnimation } from "@common/components";
import { useAlertStore } from "@/shared/lib";
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
import { SewagePipeColumnInfoListRvw } from "@/components/util/column/review/SewagePipeRvwColumn";
import type { ColumnInfoList, ColumnConfig } from "@/components/type/DataGrid.type";
import type { SearchFormValues } from "@/components/type/SearchForm.type";
import { toGridRows } from "@/components/util/grid/gridFunction";
// 하수도 통계 검토 02.하수관로
const SewagePipeRvw = () => {
const { setMessage: alert } = useAlertStore((state) => state);
const [rowDataMap, setRowDataMap] = useState<Record<string, string | number>[]>([]);
const [rowDataMap, setRowDataMap] = useState<Map<string, Record<string, string | number>[]>>(new Map());
const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>('a');
const searchParamRef = useRef<SearchFormValues | null>(null);
const columnInfo = useMemo(() => {
return SewagePipeColumnInfoListRvw.map((tab: any) => ({
return SewagePipeColumnInfoListRvw.map((tab: ColumnInfoList) => ({
...tab,
columns: tab.columns.map((col: any) => ({
columns: (tab.columns ?? []).map((col: ColumnConfig) => ({
...col,
editable: false as const,
styleName: col.styleName ?? ("rg-cell-readonly" as const),
@@ -31,7 +34,10 @@ const SewagePipeRvw = () => {
}));
}, []);
const handleReset = () => setRowDataMap([]);
const handleReset = () => {
setRowDataMap(new Map());
setActiveTab('a');
};
const handleSearch = async (values: SearchFormValues) => {
if (!values.sido || values.sido === 'ALL') {
@@ -39,6 +45,10 @@ const SewagePipeRvw = () => {
return;
}
setActiveTab('a');
setRowDataMap(new Map());
try {
setIsLoading(true);
@@ -47,37 +57,62 @@ const SewagePipeRvw = () => {
gugun: values.gugun === 'ALL' ? '' : values.gugun,
bsnmCd: values.bsnmCd,
yr: values.year,
}
};
let response;
if (activeTab === 'a') {
response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam);
} else if (activeTab === 'b') {
response = await statisticsManagementApi.getSewagePipeFacilityList(searchParam);
}
searchParamRef.current = searchParam;
const response = await statisticsManagementApi.getSewagePipeReviewRegionalList(searchParam);
if (response?.status === 'success' && response.data) {
const rawList = response.data;
const newRowDataMap = new Map<string, any[]>();
setRowDataMap()
newRowDataMap.set(activeTab, toGridRows(rawList));
const newRowDataMap = new Map<string, Record<string, string | number>[]>();
newRowDataMap.set('a', toGridRows(response.data as object[]));
setRowDataMap(newRowDataMap);
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch (error) {
console.error("조회 에러:", error);
alert('데이터 조회 중 오류가 발생했습니다.');
handleReset();
} finally {
setIsLoading(false);
}
};
const handleActiveTabChange = async (tabKey: string) => {
setActiveTab(tabKey);
const existingAData = rowDataMap.get('a');
const activeTabData = rowDataMap.get(tabKey);
const hasAData = Array.isArray(existingAData) && existingAData.length > 0;
const hasActiveTabData = Array.isArray(activeTabData) && activeTabData.length > 0;
if (hasAData && !hasActiveTabData && searchParamRef.current) {
try {
setIsLoading(true);
let response;
if (tabKey === 'b') {
response = await statisticsManagementApi.getSewagePipeFacilityList(searchParamRef.current);
}
if (response?.status === 'success' && response.data) {
setRowDataMap(prev => {
const next = new Map(prev);
next.set(tabKey, toGridRows(response.data as object[]));
return next;
});
} else {
alert(response?.message || "데이터가 없습니다.");
}
} catch {
alert('데이터 조회 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}
};
return (
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
@@ -104,7 +139,7 @@ const SewagePipeRvw = () => {
rowDataMap={rowDataMap}
setRowDataMap={setRowDataMap}
activeTab={activeTab}
onActiveTabChange={setActiveTab}
onActiveTabChange={handleActiveTabChange}
/>
</div>
);