where('is_active', (bool) $params['is_active']); } // 필터: 숨김 상태 if (isset($params['hidden'])) { $query->where('hidden', (bool) $params['hidden']); } // 필터: 상위 메뉴 if (array_key_exists('parent_id', $params)) { $query->where('parent_id', $params['parent_id']); } return $query ->orderByRaw('COALESCE(parent_id, 0)') ->orderBy('sort_order') ->get(); } /** * 글로벌 메뉴 트리 구조로 조회 */ public function tree(): Collection { $menus = Menu::global() ->orderBy('sort_order') ->get(); return $this->buildTree($menus); } /** * 글로벌 메뉴 단건 조회 */ public function show(int $id): ?Menu { return Menu::global() ->with('children') ->find($id); } /** * 글로벌 메뉴 생성 */ public function store(array $data): Menu { return Menu::create([ 'tenant_id' => null, // 글로벌 메뉴 'parent_id' => $data['parent_id'] ?? null, 'name' => $data['name'], 'url' => $data['url'] ?? null, 'icon' => $data['icon'] ?? null, 'sort_order' => $data['sort_order'] ?? 0, 'is_active' => $data['is_active'] ?? true, 'hidden' => $data['hidden'] ?? false, 'is_external' => $data['is_external'] ?? false, 'external_url' => $data['external_url'] ?? null, 'created_by' => $this->apiUserId(), 'updated_by' => $this->apiUserId(), ]); } /** * 글로벌 메뉴 수정 */ public function update(int $id, array $data): ?Menu { $menu = Menu::global()->find($id); if (! $menu) { return null; } $updateData = array_filter([ 'parent_id' => $data['parent_id'] ?? null, 'name' => $data['name'] ?? null, 'url' => $data['url'] ?? null, 'icon' => $data['icon'] ?? null, 'sort_order' => $data['sort_order'] ?? null, 'is_active' => isset($data['is_active']) ? (bool) $data['is_active'] : null, 'hidden' => isset($data['hidden']) ? (bool) $data['hidden'] : null, 'is_external' => isset($data['is_external']) ? (bool) $data['is_external'] : null, 'external_url' => $data['external_url'] ?? null, ], fn ($v) => ! is_null($v)); $updateData['updated_by'] = $this->apiUserId(); $menu->update($updateData); return $menu->fresh(); } /** * 글로벌 메뉴 삭제 * - 연결된 테넌트 메뉴의 global_menu_id를 NULL로 변경 */ public function destroy(int $id): bool { $menu = Menu::global()->find($id); if (! $menu) { return false; } return DB::transaction(function () use ($menu) { // 연결된 테넌트 메뉴의 global_menu_id를 NULL로 변경 Menu::withoutGlobalScopes() ->where('global_menu_id', $menu->id) ->update(['global_menu_id' => null]); // 하위 메뉴도 삭제 Menu::global() ->where('parent_id', $menu->id) ->delete(); $menu->deleted_by = $this->apiUserId(); $menu->save(); $menu->delete(); return true; }); } /** * 글로벌 메뉴 순서 변경 */ public function reorder(array $items): bool { return DB::transaction(function () use ($items) { foreach ($items as $item) { if (! isset($item['id'], $item['sort_order'])) { continue; } Menu::global() ->where('id', $item['id']) ->update([ 'sort_order' => (int) $item['sort_order'], 'updated_by' => $this->apiUserId(), ]); } return true; }); } /** * 특정 글로벌 메뉴를 모든 테넌트에 동기화 * - 이미 복제된 테넌트는 건너뜀 */ public function syncToAllTenants(int $globalMenuId): array { $globalMenu = Menu::global()->find($globalMenuId); if (! $globalMenu) { return ['synced' => 0, 'skipped' => 0, 'details' => []]; } // 모든 테넌트 ID 조회 $tenantIds = Menu::withoutGlobalScopes() ->whereNotNull('tenant_id') ->distinct() ->pluck('tenant_id'); $synced = 0; $skipped = 0; $details = []; foreach ($tenantIds as $tenantId) { // 이미 복제된 경우 건너뜀 $exists = Menu::withoutGlobalScopes() ->where('tenant_id', $tenantId) ->where('global_menu_id', $globalMenuId) ->exists(); if ($exists) { $skipped++; $details[] = [ 'tenant_id' => $tenantId, 'action' => 'skipped', 'reason' => 'already_exists', ]; continue; } // 복제 $newMenu = MenuBootstrapService::cloneSingleMenu($globalMenuId, $tenantId); if ($newMenu) { $synced++; $details[] = [ 'tenant_id' => $tenantId, 'action' => 'created', 'tenant_menu_id' => $newMenu->id, ]; } } return [ 'synced' => $synced, 'skipped' => $skipped, 'details' => $details, ]; } /** * 글로벌 메뉴 통계 조회 */ public function stats(): array { $total = Menu::global()->count(); $active = Menu::global()->where('is_active', true)->count(); $inactive = Menu::global()->where('is_active', false)->count(); $hidden = Menu::global()->where('hidden', true)->count(); // 테넌트별 사용 현황 $tenantUsage = Menu::withoutGlobalScopes() ->whereNotNull('global_menu_id') ->groupBy('global_menu_id') ->selectRaw('global_menu_id, COUNT(DISTINCT tenant_id) as tenant_count') ->pluck('tenant_count', 'global_menu_id') ->toArray(); return [ 'total' => $total, 'active' => $active, 'inactive' => $inactive, 'hidden' => $hidden, 'tenant_usage' => $tenantUsage, ]; } /** * 메뉴 컬렉션을 트리 구조로 변환 */ private function buildTree(Collection $menus, ?int $parentId = null): Collection { return $menus ->filter(fn ($menu) => $menu->parent_id === $parentId) ->map(function ($menu) use ($menus) { $menu->children = $this->buildTree($menus, $menu->id); return $menu; }) ->values(); } }