withTrashed(); // 테넌트 필터링 if ($tenantId) { // 특정 테넌트 선택 시: 해당 테넌트의 메뉴만 $query->where('tenant_id', $tenantId); } else { // 전체 선택 시: tenant_id가 NULL인 마스터 메뉴만 $query->whereNull('tenant_id'); } // Soft Delete 필터 if (isset($filters['trashed'])) { if ($filters['trashed'] === 'only') { $query->onlyTrashed(); } elseif ($filters['trashed'] === 'with') { $query->withTrashed(); } } // 검색 필터 if (! empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('url', 'like', "%{$search}%"); }); } // 활성 상태 필터 if (isset($filters['is_active'])) { $query->where('is_active', $filters['is_active']); } // 부모 메뉴 필터 (트리 구조) if (isset($filters['parent_id'])) { if ($filters['parent_id'] === 'null') { $query->whereNull('parent_id'); } else { $query->where('parent_id', $filters['parent_id']); } } // 모든 메뉴 가져오기 (트리 구조 정렬을 위해) $allMenus = $query->with(['parent', 'tenant'])->orderBy('sort_order')->orderBy('id')->get(); // 트리 구조로 정렬 후 플랫한 배열로 변환 $flattenedMenus = $this->flattenMenuTree($allMenus); // 수동 페이지네이션 $currentPage = request()->input('page', 1); $offset = ($currentPage - 1) * $perPage; $items = $flattenedMenus->slice($offset, $perPage)->values(); return new \Illuminate\Pagination\LengthAwarePaginator( $items, $flattenedMenus->count(), $perPage, $currentPage, ['path' => request()->url(), 'query' => request()->query()] ); } /** * 트리 구조를 플랫한 배열로 변환 (depth 정보 포함) */ private function flattenMenuTree(Collection $menus, ?int $parentId = null, int $depth = 0): Collection { $result = collect(); $filteredMenus = $menus->where('parent_id', $parentId)->sortBy('sort_order'); foreach ($filteredMenus as $menu) { $menu->depth = $depth; // 자식 메뉴 존재 여부 확인 $menu->has_children = $menus->where('parent_id', $menu->id)->count() > 0; $result->push($menu); // 자식 메뉴 재귀적으로 추가 $children = $this->flattenMenuTree($menus, $menu->id, $depth + 1); $result = $result->merge($children); } return $result; } /** * 메뉴 상세 조회 */ public function getMenuById(int $id): ?Menu { return Menu::with(['parent', 'children'])->find($id); } /** * 메뉴 트리 구조로 조회 (전체) */ public function getMenuTree(?int $tenantId = null): Collection { $tenantId = $tenantId ?? session('selected_tenant_id'); $query = Menu::query() ->where('is_active', true) ->orderBy('sort_order') ->orderBy('id'); if ($tenantId) { // 특정 테넌트 선택 시: 해당 테넌트의 메뉴만 $query->where('tenant_id', $tenantId); } else { // 전체 선택 시: tenant_id가 NULL인 마스터 메뉴만 $query->whereNull('tenant_id'); } $allMenus = $query->get(); // 부모 메뉴만 필터링하고 자식 메뉴를 재귀적으로 연결 return $allMenus->where('parent_id', null)->map(function ($menu) use ($allMenus) { $menu->children = $this->buildChildren($menu, $allMenus); return $menu; }); } /** * 재귀적으로 자식 메뉴 구성 */ private function buildChildren(Menu $parent, Collection $allMenus): Collection { $children = $allMenus->where('parent_id', $parent->id); return $children->map(function ($child) use ($allMenus) { $child->children = $this->buildChildren($child, $allMenus); return $child; }); } /** * 부모 메뉴 목록 조회 (드롭다운용) */ public function getParentMenus(?int $tenantId = null): Collection { $tenantId = $tenantId ?? session('selected_tenant_id'); $query = Menu::query() ->where('is_active', true) ->orderBy('sort_order') ->orderBy('name'); if ($tenantId) { // 특정 테넌트 선택 시: 해당 테넌트의 메뉴만 $query->where('tenant_id', $tenantId); } else { // 전체 선택 시: tenant_id가 NULL인 마스터 메뉴만 $query->whereNull('tenant_id'); } return $query->get(); } /** * 메뉴 생성 */ public function createMenu(array $data): Menu { $tenantId = session('selected_tenant_id'); // is_active 처리 $data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1'; // hidden 처리 $data['hidden'] = isset($data['hidden']) && $data['hidden'] == '1'; // is_external 처리 $data['is_external'] = isset($data['is_external']) && $data['is_external'] == '1'; // 생성자 정보 $data['created_by'] = auth()->id(); // 테넌트 정보 if ($tenantId) { $data['tenant_id'] = $tenantId; } // parent_id null 처리 if (empty($data['parent_id'])) { $data['parent_id'] = null; } return Menu::create($data); } /** * 메뉴 수정 */ public function updateMenu(int $id, array $data): bool { $menu = $this->getMenuById($id); if (! $menu) { return false; } // is_active 처리 $data['is_active'] = isset($data['is_active']) && $data['is_active'] == '1'; // hidden 처리 $data['hidden'] = isset($data['hidden']) && $data['hidden'] == '1'; // is_external 처리 $data['is_external'] = isset($data['is_external']) && $data['is_external'] == '1'; // 수정자 정보 $data['updated_by'] = auth()->id(); // parent_id null 처리 if (empty($data['parent_id'])) { $data['parent_id'] = null; } // 자기 자신을 부모로 설정하는 것 방지 if (isset($data['parent_id']) && $data['parent_id'] == $id) { return false; } return $menu->update($data); } /** * 메뉴 삭제 (Soft Delete) */ public function deleteMenu(int $id): bool { $menu = $this->getMenuById($id); if (! $menu) { return false; } // 자식 메뉴가 있는 경우 삭제 불가 if ($menu->children()->count() > 0) { return false; } $menu->deleted_by = auth()->id(); $menu->save(); return $menu->delete(); } /** * 메뉴 복원 */ public function restoreMenu(int $id): bool { $menu = Menu::onlyTrashed()->findOrFail($id); return $menu->restore(); } /** * 메뉴 영구 삭제 (슈퍼관리자 전용) */ public function forceDeleteMenu(int $id): bool { $menu = Menu::withTrashed()->findOrFail($id); // 자식 메뉴가 있는 경우 영구 삭제 불가 if ($menu->children()->withTrashed()->count() > 0) { return false; } return $menu->forceDelete(); } /** * 메뉴 활성 상태 토글 */ public function toggleActive(int $id): bool { $menu = $this->getMenuById($id); if (! $menu) { return false; } $menu->is_active = ! $menu->is_active; $menu->updated_by = auth()->id(); return $menu->save(); } /** * 메뉴 숨김 상태 토글 */ public function toggleHidden(int $id): bool { $menu = $this->getMenuById($id); if (! $menu) { return false; } $menu->hidden = ! $menu->hidden; $menu->updated_by = auth()->id(); return $menu->save(); } }