[st] - fix: 메뉴 권한체크 로직 수정

This commit is contained in:
2026-05-18 12:06:56 +09:00
parent b508568e86
commit 50ed22b4bc
8 changed files with 122 additions and 71 deletions
@@ -82,13 +82,14 @@ export default function WithBaseLayout({
const activeId = getTabStoreState().activeTabId;
if (activeId) {
setActiveTab(activeId);
const activeTab = appTabs.find((t) => t.id === activeId);
const gridActiveId = activeTab?.menuItem.menuEngNm;
if (gridActiveId) {
getGridTabStoreState().setActiveTab(gridActiveId);
}
}
}, [menuList, tabs]);
}, [menuList, tabs, setActiveTab]);
useEffect(() => {
if (!pathname) return;
@@ -103,7 +104,7 @@ export default function WithBaseLayout({
setActiveTabForGrid(gridActiveId);
}
}
}, [pathname, setActiveTab, setActiveTabForGrid]);
}, [pathname, setActiveTab, setActiveTabForGrid, tabs.length, activeTabId]);
// 활성 탭의 메뉴를 찾아서 urlPath를 가져옴. 탭이 전부 닫히면 ''를 넘겨 LNB에서 메뉴 강조 제거
const currentPath = useMemo(() => {
@@ -466,7 +466,7 @@ export function ExceptionProcessingHistoryDialog({
gridRef: MutableRefObject<RealGridReact | null>,
) => {
if (!loading && rows.length === 0) {
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground h-[65vh] flex justify-center items-center">{emptyMessage}</p>;
}
return (
@@ -380,7 +380,7 @@ export function VerificationResultDialog({
</DialogHeader>
<div className="max-h-[85vh] space-y-5 overflow-auto">
{errorMessage && (
<p className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[1.2rem] text-destructive">
<p className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-[1.2rem] text-destructive h-[65vh] flex justify-center items-center">
{errorMessage}
</p>
)}
@@ -468,7 +468,7 @@ export function VerificationResultHistoryDialog({
const gridRef = gridRefByTab[tab];
if (!loading && rows.length === 0) {
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground">{emptyMessage}</p>;
return <p className="p-6 text-center text-[1.3rem] text-muted-foreground h-[65vh] flex justify-center items-center">{emptyMessage}</p>;
}
return (
@@ -4,7 +4,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { RealGridReact } from "realgrid-react";
import useSWR, { useSWRConfig } from "swr";
import { useAlertStore, useStatisticsInputStatusConfirmGroupGate } from "@/shared/lib";
import { useActiveTabRegAuthorityAllowed, useAlertStore } from "@/shared/lib";
import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service";
import { StatisticsInputStatusApi } from "@/entities/statistics/api/input/statistics-input-status-service";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
@@ -62,7 +62,7 @@ const StatsInputStatusInp = () => {
ExceptionProcessingHistoryQueryContext | null
>(null);
const [isApproveLoading, setIsApproveLoading] = useState(false);
const confirmGroupToolbarAllowed = useStatisticsInputStatusConfirmGroupGate();
const regAuthorityToolbarAllowed = useActiveTabRegAuthorityAllowed();
const swrKey = searchParams
? (["stats-input-status", searchParams] as const)
@@ -214,6 +214,10 @@ const StatsInputStatusInp = () => {
const openExceptionHistoryForRow = useCallback(
(row: Record<string, string | number>) => {
if (!regAuthorityToolbarAllowed) {
alert("권한이 없습니다.");
return;
}
const ctx = lastSearchedValues;
if (!ctx?.year || !ctx?.sido || !ctx?.bsnmCd) {
alert("먼저 조회를 진행해주세요.");
@@ -233,7 +237,7 @@ const StatsInputStatusInp = () => {
});
setExceptionHistoryOpen(true);
},
[alert, lastSearchedValues],
[alert, lastSearchedValues, regAuthorityToolbarAllowed],
);
const handleExceptionSubmitHistoryCellClick = useCallback(
@@ -266,7 +270,7 @@ const StatsInputStatusInp = () => {
gugunOkOrNotDoList &&
inputPeriodCurrentRef.current &&
!allRowsLaeSbmsnApproved &&
confirmGroupToolbarAllowed;
regAuthorityToolbarAllowed;
return (
<>
+2 -1
View File
@@ -6,7 +6,8 @@ export { useGlobalProgressStore } from "./use-global-progress-store";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
export {
useStatisticsInputStatusConfirmGroupGate,
isRegAuthorityAllowed,
useActiveTabRegAuthorityAllowed,
useStatisticsInputToolbarGroupGate,
} from "./use-statistics-input-toolbar-group-gate";
export { usePrompt } from "./use-prompt";
@@ -1,68 +1,28 @@
"use client";
import { useEffect, useState } from "react";
import { useTabStore } from "@/store/tab-store";
import {
checkStatisticsInputStatusConfirmGroupMembership,
checkStatisticsInputToolbarGroupMembership,
} from "@/entities/group-user/lib/check-statistics-input-toolbar-group-membership";
export function isRegAuthorityAllowed(
pageMenuAuthority: { regAuthrtYn: string } | null | undefined,
): boolean {
return pageMenuAuthority?.regAuthrtYn === "Y";
}
import { useUserStore } from "./use-user-store";
/**
* 활성 탭의 `getPageMenuAuthority` 결과에서 `regAuthrtYn === "Y"`일 때만 true.
* 권한은 탭 추가·활성화 시 tab-store에서 조회합니다.
*/
export function useActiveTabRegAuthorityAllowed(): boolean {
const activeTabId = useTabStore((s) => s.activeTabId);
const pageMenuAuthority = useTabStore((s) => {
if (!activeTabId) return undefined;
return s.tabs.find((tab) => tab.id === activeTabId)?.pageMenuAuthority;
});
/** 통계 입력 상단 툴바(저장·제출·검증결과 등) — 그룹 멤버십 만족 시에만 true */
return isRegAuthorityAllowed(pageMenuAuthority);
}
/** 통계 입력 화면 상단 툴바용 — {@link useActiveTabRegAuthorityAllowed}와 동일 */
export function useStatisticsInputToolbarGroupGate(): boolean {
const userInfo = useUserStore((s) => s.userInfo);
const [allowed, setAllowed] = useState(false);
useEffect(() => {
let cancelled = false;
const name = userInfo?.userName?.trim() ?? "";
const uid = userInfo?.userUnqId?.trim() ?? "";
if (!name || !uid) {
setAllowed(false);
return;
}
void (async () => {
try {
const ok = await checkStatisticsInputToolbarGroupMembership(name, uid);
if (!cancelled) setAllowed(ok);
} catch {
if (!cancelled) setAllowed(false);
}
})();
return () => {
cancelled = true;
};
}, [userInfo?.userName, userInfo?.userUnqId]);
return allowed;
}
/** 통계 입력 현황 — `GRP_ST_CONFIRM` / `GRP_ADMIN` 멤버일 때만 true */
export function useStatisticsInputStatusConfirmGroupGate(): boolean {
const userInfo = useUserStore((s) => s.userInfo);
const [allowed, setAllowed] = useState(false);
useEffect(() => {
let cancelled = false;
const name = userInfo?.userName?.trim() ?? "";
const uid = userInfo?.userUnqId?.trim() ?? "";
if (!name || !uid) {
setAllowed(false);
return;
}
void (async () => {
try {
const ok = await checkStatisticsInputStatusConfirmGroupMembership(name, uid);
if (!cancelled) setAllowed(ok);
} catch {
if (!cancelled) setAllowed(false);
}
})();
return () => {
cancelled = true;
};
}, [userInfo?.userName, userInfo?.userUnqId]);
return allowed;
return useActiveTabRegAuthorityAllowed();
}
+85
View File
@@ -2,6 +2,8 @@ import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import type { MenuItem } from "@/entities/menu";
import { MenuAuthorityApi } from "@/entities/menu-authority";
import type { PageMenuAuthority } from "@/entities/menu-authority/model/menu-authority-interface";
export interface Tab {
id: string; // 고유 ID (urlPath)
@@ -10,6 +12,69 @@ export interface Tab {
urlPath: string; // url 경로
parentEngNm: string; // 부모 menuEngNm
menuItem: MenuItem; // 메뉴 정보
/** undefined: 미조회, null: 조회했으나 없음/실패 */
pageMenuAuthority?: PageMenuAuthority | null;
}
async function fetchTabPageMenuAuthority(
menuCd: number,
apply: (authority: PageMenuAuthority | null) => void,
): Promise<void> {
try {
const response = await MenuAuthorityApi.getPageMenuAuthority(menuCd);
apply(response?.[0] ?? null);
} catch {
apply(null);
}
}
function scheduleTabPageMenuAuthorityFetch(
get: () => TabStore,
set: (
partial:
| Partial<TabStore>
| ((state: TabStore) => Partial<TabStore>),
) => void,
tabId: string,
menuCd: number,
): void {
void fetchTabPageMenuAuthority(menuCd, (authority) => {
const { tabs } = get();
if (!tabs.some((tab) => tab.id === tabId)) return;
set((state) => ({
tabs: state.tabs.map((tab) =>
tab.id === tabId ? { ...tab, pageMenuAuthority: authority } : tab,
),
}));
});
}
type TabStoreSetState = (
partial:
| Partial<TabStore>
| ((state: TabStore) => Partial<TabStore>),
) => void;
const tabStoreSetState: TabStoreSetState = (partial) => {
useTabStoreHook.setState(partial);
};
function ensureTabPageMenuAuthority(
get: () => TabStore,
set: TabStoreSetState,
tab: Tab,
): void {
if (tab.pageMenuAuthority !== undefined) return;
scheduleTabPageMenuAuthorityFetch(get, set, tab.id, tab.menuItem.menuCd);
}
/** persist 복원·새로고침 후 활성 탭 메뉴 권한이 없으면 조회 */
export function hydrateActiveTabPageMenuAuthority(): void {
const { tabs, activeTabId } = getTabStoreState();
if (!activeTabId) return;
const tab = tabs.find((t) => t.id === activeTabId);
if (!tab) return;
ensureTabPageMenuAuthority(getTabStoreState, tabStoreSetState, tab);
}
interface TabStore {
@@ -32,6 +97,7 @@ function tabForPersist(tab: Tab): Tab {
return {
...tab,
menuItem: { ...tab.menuItem, children: null },
pageMenuAuthority: undefined,
};
}
@@ -71,6 +137,7 @@ const useTabStoreHook = create<TabStore>()(
if (existingTab) {
set({ activeTabId: tabId });
ensureTabPageMenuAuthority(get, set, existingTab);
return true;
}
@@ -85,6 +152,7 @@ const useTabStoreHook = create<TabStore>()(
(m) => m.menuCd === menuItem.parentId,
)?.menuEngNm || "",
menuItem,
pageMenuAuthority: undefined,
};
set({
@@ -92,6 +160,13 @@ const useTabStoreHook = create<TabStore>()(
activeTabId: tabId,
});
scheduleTabPageMenuAuthorityFetch(
get,
set,
tabId,
menuItem.menuCd,
);
return true;
}
return false;
@@ -122,9 +197,15 @@ const useTabStoreHook = create<TabStore>()(
tabs: newTabs,
activeTabId: newActiveTabId,
});
if (newActiveTabId) {
const nextActive = get().tabs.find((t) => t.id === newActiveTabId);
if (nextActive) ensureTabPageMenuAuthority(get, set, nextActive);
}
},
setActiveTab: (tabId: string) => {
set({ activeTabId: tabId });
const tab = get().tabs.find((t) => t.id === tabId);
if (tab) ensureTabPageMenuAuthority(get, set, tab);
},
setMenuList: (menuList: MenuItem[] | null) => {
set({ menuList });
@@ -173,6 +254,10 @@ const useTabStoreHook = create<TabStore>()(
activeTabId,
};
},
onRehydrateStorage: () => (state, error) => {
if (error || !state) return;
queueMicrotask(() => hydrateActiveTabPageMenuAuthority());
},
},
),
);