'use client'; import { createContext, useContext, useEffect, useState, useCallback } from 'react'; import { usePathname } from 'next/navigation'; import { getRolePermissionMatrix, getPermissionMenuUrlMap } from '@/lib/permissions/actions'; import { buildMenuIdToUrlMap, convertMatrixToPermissionMap, findMatchingUrl, mergePermissionMaps } from '@/lib/permissions/utils'; import { ALL_DENIED_PERMS } from '@/lib/permissions/types'; import type { PermissionMap, PermissionAction } from '@/lib/permissions/types'; import { AccessDenied } from '@/components/common/AccessDenied'; import { stripLocalePrefix } from '@/lib/utils/locale'; interface PermissionContextType { permissionMap: PermissionMap | null; isLoading: boolean; /** URL 지정 권한 체크 (특수 케이스용) */ can: (url: string, action: PermissionAction) => boolean; /** 권한 데이터 다시 로드 (설정 변경 후 호출) */ reloadPermissions: () => void; } const PermissionContext = createContext({ permissionMap: null, isLoading: true, can: () => true, reloadPermissions: () => {}, }); export function PermissionProvider({ children }: { children: React.ReactNode }) { const [permissionMap, setPermissionMap] = useState(null); const [isLoading, setIsLoading] = useState(true); const loadPermissions = useCallback(async () => { const userData = getUserData(); if (!userData || userData.roleIds.length === 0) { setIsLoading(false); return; } const { roleIds, menuIdToUrl } = userData; setIsLoading(true); try { // 사이드바 메뉴에 없는 권한 메뉴의 URL 매핑 보완 // (기준정보 관리, 공정관리 등 사이드바 미등록 메뉴 대응) const [permMenuUrlMap, ...results] = await Promise.all([ getPermissionMenuUrlMap(), ...roleIds.map(id => getRolePermissionMatrix(id)), ]); // 권한 메뉴 URL을 베이스로, 사이드바 메뉴 URL로 덮어쓰기 (사이드바 우선) const mergedMenuIdToUrl = { ...permMenuUrlMap, ...menuIdToUrl }; const maps = results .filter(r => r.success && r.data?.permissions) .map(r => convertMatrixToPermissionMap(r.data.permissions, mergedMenuIdToUrl)); if (maps.length > 0) { const merged = mergePermissionMaps(maps); // 권한 메뉴에 등록되어 있지만 매트릭스 응답에 없는 메뉴 처리 // (모든 권한 OFF → API가 해당 menuId를 생략 → "all denied"로 보완) for (const [, url] of Object.entries(permMenuUrlMap)) { if (url && !merged[url]) { merged[url] = { ...ALL_DENIED_PERMS }; } } setPermissionMap(merged); } else { setPermissionMap(null); } } catch (error) { console.error('[Permission] 권한 로드 실패:', error); setPermissionMap(null); } setIsLoading(false); }, []); // 마운트 시 1회 로드 useEffect(() => { loadPermissions(); }, [loadPermissions]); const can = useCallback((url: string, action: PermissionAction): boolean => { if (!permissionMap) return true; const matchedUrl = findMatchingUrl(url, permissionMap); if (!matchedUrl) return true; const perms = permissionMap[matchedUrl]; return perms?.[action] ?? true; }, [permissionMap]); return ( {children} ); } /** * 자기 잠금(self-lockout) 방지: 권한 설정 페이지는 항상 접근 허용 */ const BYPASS_PATHS = ['/settings/permissions']; function isGateBypassed(pathname: string): boolean { const pathWithoutLocale = stripLocalePrefix(pathname); return BYPASS_PATHS.some(bp => pathWithoutLocale.startsWith(bp)); } /** * PermissionGate: 레이아웃에 배치하여 모든 페이지 자동 보호 */ export function PermissionGate({ children }: { children: React.ReactNode }) { const pathname = usePathname(); const { permissionMap, isLoading } = useContext(PermissionContext); if (isLoading) return null; if (!permissionMap) { return <>{children}; } if (isGateBypassed(pathname)) return <>{children}; const matchedUrl = findMatchingUrl(pathname, permissionMap); if (!matchedUrl) { return <>{children}; } const perms = permissionMap[matchedUrl]; const canView = perms?.view ?? true; if (!canView) { return ; } return <>{children}; } /** localStorage 'user' 키에서 역할 ID 배열 + menuId→URL 매핑 추출 */ function getUserData(): { roleIds: number[]; menuIdToUrl: Record } | null { if (typeof window === 'undefined') return null; try { const raw = localStorage.getItem('user'); if (!raw) return null; const parsed = JSON.parse(raw); const roleIds = Array.isArray(parsed.roles) ? parsed.roles.map((r: { id: number }) => r.id).filter(Boolean) : []; const menuIdToUrl = Array.isArray(parsed.menu) ? buildMenuIdToUrlMap(parsed.menu) : {}; return { roleIds, menuIdToUrl }; } catch { return null; } } export const usePermissionContext = () => useContext(PermissionContext);