user(); // 로그인한 사용자의 tenant_id만 사용 (session 값 무시) $tenantId = $user?->tenant_id ?? 1; // 테넌트의 모든 활성 메뉴 조회 $allMenus = Menu::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('is_active', true) ->where('hidden', false) ->orderBy('sort_order') ->get(); // 슈퍼관리자는 모든 메뉴 표시 if ($user && $user->is_super_admin) { return $this->buildMenuTree($allMenus); } // 일반 사용자: 부서 권한 기반 메뉴 ID 조회 $permittedMenuIds = $this->getPermittedMenuIds($user, $tenantId); // 역할 기반 필터링 + 부서 권한 필터링 $filteredMenus = $allMenus->filter(function ($menu) use ($user, $permittedMenuIds) { $requiredRole = $menu->getRequiresRole(); // super_admin 역할 필요시 슈퍼관리자만 if ($requiredRole === 'super_admin') { return $user && $user->is_super_admin; } // 기타 역할 체크 if ($requiredRole && ! ($user && $user->hasRole($requiredRole))) { return false; } // 부서 권한 체크: 허용된 메뉴 ID만 표시 return in_array($menu->id, $permittedMenuIds); }); return $this->buildMenuTree($filteredMenus); } /** * 사용자가 접근 가능한 메뉴 ID 목록 조회 (부서 권한 기반) */ private function getPermittedMenuIds(?User $user, int $tenantId): array { if (! $user) { return []; } // 사용자의 부서 ID 조회 $departmentIds = DB::table('department_user') ->where('user_id', $user->id) ->pluck('department_id') ->toArray(); if (empty($departmentIds)) { return []; } $now = now(); // permission_overrides 테이블에서 부서에 ALLOW된 menu:*.view 권한 조회 $permittedMenuIds = DB::table('permission_overrides as po') ->join('permissions as p', 'p.id', '=', 'po.permission_id') ->where('po.model_type', Department::class) ->whereIn('po.model_id', $departmentIds) ->where('po.tenant_id', $tenantId) ->where('po.effect', 1) // ALLOW ->where('p.name', 'like', 'menu:%.view') ->whereNull('po.deleted_at') ->where(function ($query) use ($now) { $query->whereNull('po.effective_from') ->orWhere('po.effective_from', '<=', $now); }) ->where(function ($query) use ($now) { $query->whereNull('po.effective_to') ->orWhere('po.effective_to', '>=', $now); }) ->pluck('p.name') ->map(function ($name) { // menu:{id}.view에서 id 추출 if (preg_match('/^menu:(\d+)\.view$/', $name, $matches)) { return (int) $matches[1]; } return null; }) ->filter() ->unique() ->toArray(); return $permittedMenuIds; } /** * 섹션별 메뉴 조회 (main, tools, labs) */ public function getMenusBySection(?User $user = null): array { $menuTree = $this->getUserMenuTree($user); return [ 'main' => $menuTree->filter(fn ($m) => $m->getSection() === 'main')->values(), 'tools' => $menuTree->filter(fn ($m) => $m->getSection() === 'tools')->values(), 'labs' => $menuTree->filter(fn ($m) => $m->getSection() === 'labs')->values(), ]; } /** * 메뉴 트리 구성 */ private function buildMenuTree(Collection $menus, ?int $parentId = null): Collection { return $menus->where('parent_id', $parentId) ->map(function ($menu) use ($menus) { $menu->menuChildren = $this->buildMenuTree($menus, $menu->id); return $menu; }) ->values(); } /** * 현재 라우트가 메뉴와 일치하는지 확인 */ public function isMenuActive(Menu $menu): bool { $routeName = $menu->getRouteName(); if ($routeName) { // 라우트 패턴 매칭 (예: pm.* → pm.index, pm.projects.index 등) if (str_ends_with($routeName, '.*')) { $prefix = substr($routeName, 0, -2); return request()->routeIs($prefix.'*'); } return request()->routeIs($routeName); } // URL 매칭 if ($menu->url) { $currentPath = '/'.ltrim(request()->path(), '/'); // /boards/{id}/posts 패턴인 경우 게시판 기반 메뉴 매칭 $boardMenuUrl = $this->getBoardMenuUrl(); if ($boardMenuUrl) { return $menu->url === $boardMenuUrl; } // 정확한 URL 매칭 if ($currentPath === $menu->url) { return true; } // prefix 매칭: 다른 메뉴가 현재 URL에 정확히 매칭되면 prefix 매칭 비활성화 // (예: /esign/create 접속 시, /esign 메뉴가 prefix로 잘못 활성화되는 것 방지) if (str_starts_with($currentPath, $menu->url.'/')) { return !self::hasExactMenuMatch($currentPath); } return false; } return false; } /** * 현재 /boards/{code}/posts 경로인 경우 해당 게시판의 메뉴 URL 반환 */ private function getBoardMenuUrl(): ?string { static $cachedUrl = null; static $calculated = false; if ($calculated) { return $cachedUrl; } $calculated = true; $currentPath = request()->path(); // /boards/{code}/posts 패턴 확인 (code는 영문자로 시작하는 문자열) if (! preg_match('#^boards/([a-zA-Z][a-zA-Z0-9_-]*)(/posts.*)?$#', $currentPath, $matches)) { return null; } $boardCode = $matches[1]; // 게시판 조회 (board_code로) $board = Board::withoutGlobalScopes()->where('board_code', $boardCode)->first(); if (! $board) { // 게시판 없으면 게시판 관리 메뉴 $cachedUrl = '/boards'; return $cachedUrl; } // 시스템 게시판: /customer-center/{board_code} // 테넌트 게시판: /boards/{board_code} if ($board->is_system) { $expectedUrl = '/customer-center/'.$board->board_code; } else { $expectedUrl = '/boards/'.$board->board_code; } // 해당 URL의 메뉴가 존재하는지 확인 $menuExists = Menu::withoutGlobalScopes() ->where('url', $expectedUrl) ->where('is_active', true) ->exists(); if ($menuExists) { $cachedUrl = $expectedUrl; } else { // 메뉴 없으면 게시판 관리 메뉴 $cachedUrl = '/boards'; } return $cachedUrl; } /** * 현재 URL에 정확히 매칭되는 메뉴가 존재하는지 확인 (캐시) */ private static ?bool $exactMatchCache = null; private static function hasExactMenuMatch(string $currentPath): bool { if (self::$exactMatchCache !== null) { return self::$exactMatchCache; } $tenantId = auth()->user()?->tenant_id ?? 1; self::$exactMatchCache = Menu::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('is_active', true) ->where('url', $currentPath) ->exists(); return self::$exactMatchCache; } /** * 메뉴 또는 자식 메뉴가 활성 상태인지 확인 */ public function isMenuOrChildActive(Menu $menu): bool { if ($this->isMenuActive($menu)) { return true; } // 자식 메뉴 중 활성 상태가 있는지 확인 if (isset($menu->menuChildren) && $menu->menuChildren->isNotEmpty()) { foreach ($menu->menuChildren as $child) { if ($this->isMenuOrChildActive($child)) { return true; } } } return false; } }