st: fix: 하수관로 검토 컬럼 수정 & 관리자 계정 판별 로직 추가

This commit is contained in:
2026-02-25 17:04:27 +09:00
parent 5600ce01bd
commit 690bca4c06
6 changed files with 61 additions and 18 deletions
@@ -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}
/>
))}
@@ -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<boolean>(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<string, string | number>[]) | 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 = () => {
<>
<div className="relative">
<SearchForm onSubmit={handleSearch} onReset={handleReset} onValuesChange={setSearchValues} />
{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) && (
<div className="card !py-0">
<div className="flex justify-end items-center gap-[0.6rem] mx-[0.6rem]">
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
<SubmitButton submitFunc={submitFunc} />
{
// 최고 관리자일땐 저장 가능하게
<SaveButton saveFunc={saveFunc} />
}
{isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) && (
<>
<SubmitExceptionButton SubmitExceptionFunc={SubmitExceptionFunc} />
<LastYearDataButton getLastYearDataFunc={() => getLastYearDataFunc(searchValues ?? {})} />
<SubmitButton submitFunc={submitFunc} />
</>
)}
{(isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) || isAdminInfo) && <SaveButton saveFunc={saveFunc} />}
</div>
</div>
}
)}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>}
<DataGrid
key={isEnterable ? 'enterable' : 'readonly'}
key={(isEnterable || isAdminInfo) ? 'enterable' : 'readonly'}
columnInfo={columnInfo}
isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)}
rowStartNum={3}
+3 -1
View File
@@ -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;
@@ -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',
]
},
@@ -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<string, React.ComponentType>());
@@ -26,6 +28,10 @@ const StatisticsPage = () => {
});
}, [tabs]);
useEffect(() => {
fetchAdminInfo();
}, [fetchAdminInfo]);
// 탭이 없으면 Dashboard 표시
if (tabs.length === 0) {
return (
+29
View File
@@ -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<void>;
}
const useAdminStoreHook = create<AdminStore>((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();