diff --git a/apps/st/src/components/module/TabDataGrid.tsx b/apps/st/src/components/module/TabDataGrid.tsx index 96aec2a94..b188ce3fe 100644 --- a/apps/st/src/components/module/TabDataGrid.tsx +++ b/apps/st/src/components/module/TabDataGrid.tsx @@ -708,7 +708,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r valueType: col.valueType, dataFormat: col.dataFormat, ...(col.styleName != null && { styleName: col.styleName }), - ...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }), + ...(col.mergeCells && { mergeRule: { criteria: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }), }; if (col.footer) { if (Array.isArray(col.footer)) c.footer = col.footer; @@ -993,7 +993,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r valueType: col.valueType, dataFormat: col.dataFormat, ...(col.styleName != null && { styleName: col.styleName }), - ...(col.mergeCells && { mergeRule: { criteria: 'value' as const } }), + ...(col.mergeCells && { mergeRule: { criteria: col.mergeGroupField ? `values['${col.mergeGroupField}'] + value` : 'value' as const } }), }; if (col.footer) { @@ -1151,7 +1151,7 @@ const TabDataGrid = ({ columnInfo, isExcelImport = true, isExcelExport = true, r expression: (column.footer as { expression?: 'sum' | 'avg' | 'count' | 'max' | 'min' }).expression, numberFormat: (column.footer as { numberFormat?: string }).numberFormat, } : undefined} - mergeRule={column.mergeCells ? { criteria: 'value' } : undefined} + mergeRule={column.mergeCells ? { criteria: column.mergeGroupField ? `values['${column.mergeGroupField}'] + value` : 'value' } : undefined} sortable={!column.sortdisable} /> ))} diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 13249fdf8..cf7e52e27 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -12,6 +12,7 @@ import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import SubmitDailog from "./SubmitDailog"; import { useAlertStore } from "@/shared/lib"; +import { useAdminStore } from "@/store/admin-store"; import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import type { @@ -60,6 +61,8 @@ const SeweragePenetrationRateInp = () => { const [showDailog, setShowDailog] = useState(false); const { confirm } = useConfirm(); + const { isAdminInfo } = useAdminStore(); + const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number; refArea?: number } | null>(null); const getGridDataRef = useRef<(() => Record[]) | null>(null); @@ -74,7 +77,7 @@ const SeweragePenetrationRateInp = () => { // 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요. const columnInfo = useMemo(() => { const { columns, columnLayout } = SeweragePenetrationRateInpColumn; - const base = isEnterable ? columns : columns.map((col) => ({ ...col, editable: false as const })); + const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const })); return { columnLayout, columns: base.map((col) => { @@ -85,7 +88,7 @@ const SeweragePenetrationRateInp = () => { return col; }), }; - }, [isEnterable, footerFromApi]); + }, [isEnterable, isAdminInfo, footerFromApi]); const handleReset = () => { setRowData([]); @@ -389,26 +392,27 @@ const SeweragePenetrationRateInp = () => { <>
- {rowData.length > 0 && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && + {rowData.length > 0 && (isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && (
- - getLastYearDataFunc(searchValues ?? {})} /> - - { - // 최고 관리자일땐 저장 가능하게 - - } + {isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && ( + <> + + getLastYearDataFunc(searchValues ?? {})} /> + + + )} + {(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && }
- } + )} {isLoading &&
} 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} rowStartNum={3} diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index 69d82b6d2..0106251d5 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -12,7 +12,7 @@ export type RealGridColumnInfo = { valueType?: import("realgrid").ValueType; dataFormat?: string; styleName?: string; - mergeRule?: { criteria: "value" }; + mergeRule?: { criteria: "value" | string }; sortable?: boolean; footer?: | { @@ -135,6 +135,8 @@ export type ColumnConfig = { /** 셀에 적용할 CSS 클래스명 (예: 'rg-cell-readonly', 'rg-cell-total') */ styleName?: string; mergeCells?: boolean; + /** 병합 시 그룹 기준 필드(예: 시군구). 지정 시 같은 그룹 내에서만 value 기준 병합 */ + mergeGroupField?: string; sortdisable?: boolean; sortPreserveGroupOrder?: SortPreserveGroupOrder; valueType?: ValueType; diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index cc86bd201..1020ce3bc 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -218,7 +218,8 @@ const rawList = [ ] }, { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, mergeCells: true, sortdisable: true }, - { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명칭' }, mergeCells: true }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, mergeCells: true, sortdisable: true }, + { name: 'swrmtFcltyNm', fieldName: 'swrmtFcltyNm', header: { text: '하수처리시설 명' }, mergeCells: true, mergeGroupField: 'admdstNm' }, { name: 'displayYear', fieldName: 'displayYear', @@ -276,8 +277,9 @@ const rawList = [ name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }, { colspan: 3 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, 'sggNm', + 'admdstNm', 'swrmtFcltyNm', ] }, diff --git a/apps/st/src/pages/statistics/ui/statistics-page.tsx b/apps/st/src/pages/statistics/ui/statistics-page.tsx index 7513b0ffe..a228a4d46 100644 --- a/apps/st/src/pages/statistics/ui/statistics-page.tsx +++ b/apps/st/src/pages/statistics/ui/statistics-page.tsx @@ -3,6 +3,7 @@ import { Suspense, useRef, useEffect } from "react"; import dynamic from "next/dynamic"; import { useTabStore } from "@/store/tab-store"; +import { useAdminStore } from "@/store/admin-store"; import Dashboard from "@/components/Dashboard"; import { SkeletonPage } from "@/shared/ui/skeleton-page"; @@ -10,6 +11,7 @@ import "@rsone/components/index.css"; const StatisticsPage = () => { const { tabs, activeTabId } = useTabStore(); + const fetchAdminInfo = useAdminStore((state) => state.fetchAdminInfo); // 컴포넌트 캐시를 useRef로 유지 (재렌더링 시에도 유지됨) const componentCache = useRef(new Map()); @@ -26,6 +28,10 @@ const StatisticsPage = () => { }); }, [tabs]); + useEffect(() => { + fetchAdminInfo(); + }, [fetchAdminInfo]); + // 탭이 없으면 Dashboard 표시 if (tabs.length === 0) { return ( diff --git a/apps/st/src/store/admin-store.ts b/apps/st/src/store/admin-store.ts new file mode 100644 index 000000000..1b9937d4f --- /dev/null +++ b/apps/st/src/store/admin-store.ts @@ -0,0 +1,29 @@ +import { create } from "zustand"; +import { UserApi } from "@/entities/user"; +import { getUserInfo } from "../../../../packages/common/components/src/api"; + +interface AdminStore { + /** 현재 사용자가 관리자 목록에 있는지 */ + isAdminInfo: boolean; + setAdminInfo: (value: boolean) => void; + /** API로 관리자 목록 조회 후 현재 사용자 일치 여부 설정 */ + fetchAdminInfo: () => Promise; +} + +const useAdminStoreHook = create((set) => ({ + isAdminInfo: false, + setAdminInfo: (value) => set({ isAdminInfo: value }), + fetchAdminInfo: async () => { + try { + const userInfo = await getUserInfo(); + const adminList = await UserApi.findPageAdminUser({ userName: userInfo.userName }); + const matched = adminList.content?.filter((a) => a.userName === userInfo.userName) ?? []; + set({ isAdminInfo: matched.length > 0 }); + } catch { + set({ isAdminInfo: false }); + } + }, +})); + +export const useAdminStore = useAdminStoreHook; +export const getAdminStoreState = () => useAdminStoreHook.getState();