diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index a7e0e3383..71d94fad3 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -656,22 +656,115 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r }, [rowDataMap, columnInfo]); - // 활성 탭이 변경될 때마다 fitStyle 재적용 (그리드가 보일 때) + // 탭 전환 시 그리드 레이아웃·푸터·포맷 재적용 (여러 시점 + ResizeObserver) + const appliedTabRef = useRef(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[]) => 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
컬럼 정보가 없습니다.
; @@ -791,7 +884,9 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r }
+ {activeTab === tabKey ? ( { 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 /> ))} + ) : null}
diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 70d4f10ae..9333b92cb 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -89,6 +89,7 @@ const SeweragePenetrationRateInp = () => { ...d, sido: commonInfo.sido, speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr, + piplSupprt: commonInfo.piplSupprt, })); setRowData(toGridRows(convertedData)); diff --git a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx index 1ba9d048f..34fe00910 100644 --- a/apps/st/src/components/statistics/review/SewagePipeRvw.tsx +++ b/apps/st/src/components/statistics/review/SewagePipeRvw.tsx @@ -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[]>([]); + const [rowDataMap, setRowDataMap] = useState[]>>(new Map()); const [isLoading, setIsLoading] = useState(false); const [activeTab, setActiveTab] = useState('a'); + const searchParamRef = useRef(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(); - - setRowDataMap() - - newRowDataMap.set(activeTab, toGridRows(rawList)); - + const newRowDataMap = new Map[]>(); + 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 (
@@ -104,7 +139,7 @@ const SewagePipeRvw = () => { rowDataMap={rowDataMap} setRowDataMap={setRowDataMap} activeTab={activeTab} - onActiveTabChange={setActiveTab} + onActiveTabChange={handleActiveTabChange} />
);