Files
sam-manage/app/Services/SidebarMenuService.php

252 lines
7.7 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use App\Models\Boards\Board;
use App\Models\Commons\Menu;
use App\Models\Tenants\Department;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class SidebarMenuService
{
/**
* 현재 사용자가 접근 가능한 메뉴 트리 조회
*/
public function getUserMenuTree(?User $user = null): Collection
{
$user = $user ?? auth()->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;
}
return $currentPath === $menu->url || str_starts_with($currentPath, $menu->url.'/');
}
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;
}
/**
* 메뉴 또는 자식 메뉴가 활성 상태인지 확인
*/
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;
}
}