diff --git a/apps/st/app/(main)/(with-base-layout)/layout.tsx b/apps/st/app/(main)/(with-base-layout)/layout.tsx index 1ba6133ea..f4648a8b0 100644 --- a/apps/st/app/(main)/(with-base-layout)/layout.tsx +++ b/apps/st/app/(main)/(with-base-layout)/layout.tsx @@ -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(() => { diff --git a/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx b/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx index 483346a6c..ff72a51ef 100644 --- a/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx +++ b/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx @@ -466,7 +466,7 @@ export function ExceptionProcessingHistoryDialog({ gridRef: MutableRefObject, ) => { if (!loading && rows.length === 0) { - return

{emptyMessage}

; + return

{emptyMessage}

; } return ( diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx index 7311e184d..1141348aa 100644 --- a/apps/st/src/components/module/VerificationResultDialog.tsx +++ b/apps/st/src/components/module/VerificationResultDialog.tsx @@ -380,7 +380,7 @@ export function VerificationResultDialog({
{errorMessage && ( -

+

{errorMessage}

)} diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index cd599665b..e85d481b0 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -468,7 +468,7 @@ export function VerificationResultHistoryDialog({ const gridRef = gridRefByTab[tab]; if (!loading && rows.length === 0) { - return

{emptyMessage}

; + return

{emptyMessage}

; } return ( diff --git a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx index db04bc957..2cf6f2261 100644 --- a/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx +++ b/apps/st/src/components/statistics/input/StatsInputStatusInp.tsx @@ -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) => { + 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 ( <> diff --git a/apps/st/src/shared/lib/index.ts b/apps/st/src/shared/lib/index.ts index ec5104865..b2175d7c7 100644 --- a/apps/st/src/shared/lib/index.ts +++ b/apps/st/src/shared/lib/index.ts @@ -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"; diff --git a/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts b/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts index e8337f496..57f4a0782 100644 --- a/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts +++ b/apps/st/src/shared/lib/use-statistics-input-toolbar-group-gate.ts @@ -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(); } diff --git a/apps/st/src/store/tab-store.ts b/apps/st/src/store/tab-store.ts index 640cada97..6909ed26b 100644 --- a/apps/st/src/store/tab-store.ts +++ b/apps/st/src/store/tab-store.ts @@ -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 { + try { + const response = await MenuAuthorityApi.getPageMenuAuthority(menuCd); + apply(response?.[0] ?? null); + } catch { + apply(null); + } +} + +function scheduleTabPageMenuAuthorityFetch( + get: () => TabStore, + set: ( + partial: + | Partial + | ((state: TabStore) => Partial), + ) => 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 + | ((state: TabStore) => Partial), +) => 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()( if (existingTab) { set({ activeTabId: tabId }); + ensureTabPageMenuAuthority(get, set, existingTab); return true; } @@ -85,6 +152,7 @@ const useTabStoreHook = create()( (m) => m.menuCd === menuItem.parentId, )?.menuEngNm || "", menuItem, + pageMenuAuthority: undefined, }; set({ @@ -92,6 +160,13 @@ const useTabStoreHook = create()( activeTabId: tabId, }); + scheduleTabPageMenuAuthorityFetch( + get, + set, + tabId, + menuItem.menuCd, + ); + return true; } return false; @@ -122,9 +197,15 @@ const useTabStoreHook = create()( 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()( activeTabId, }; }, + onRehydrateStorage: () => (state, error) => { + if (error || !state) return; + queueMicrotask(() => hydrateActiveTabPageMenuAuthority()); + }, }, ), );