['admin' => [...], 'user' => [...]], * 'types' => ['admin', 'user'], * 'default_type' => 'admin', * ] */ public function getPermissionTree(): array { // 최상위 권한(모듈)부터 재귀적으로 자식 로드 $rootPermissions = $this->permissionRepository->getRootsWithChildren(); // 코어 권한을 항상 먼저 표시하기 위해 PHP에서 정렬 $rootPermissions = $rootPermissions->sortBy(function ($permission) { // 코어는 0, 나머지는 1로 우선순위 부여 후 order로 정렬 $priority = $permission->extension_type === ExtensionOwnerType::Core ? 0 : 1; return [$priority, $permission->order]; })->values(); // 전체 트리를 먼저 빌드 $fullTree = []; foreach ($rootPermissions as $permission) { $fullTree[] = $this->formatPermissionNode($permission); } // 타입별로 트리를 재귀적으로 필터링하여 그룹화 $groupedByType = []; $types = PermissionType::values(); foreach ($types as $type) { $groupedByType[$type] = [ 'label' => PermissionType::from($type)->label(), 'icon' => PermissionType::from($type)->icon(), 'permissions' => $this->filterTreeByType($fullTree, $type), ]; } $result = [ 'permissions' => $groupedByType, 'types' => $types, 'default_type' => PermissionType::Admin->value, 'scope_options' => [ ['value' => null, 'label' => __('auth.scope_type_all')], ['value' => 'role', 'label' => __('auth.scope_type_role')], ['value' => 'self', 'label' => __('auth.scope_type_self')], ], ]; // 필터 훅 - 결과 데이터 변형 (확장성 확보) $result = HookManager::applyFilters('core.permission.filter_tree_result', $result); return $result; } /** * 단일 권한 노드를 포맷팅합니다 (재귀). * * @param Permission $permission 권한 모델 * @return array 포맷된 권한 노드 */ private function formatPermissionNode(Permission $permission): array { $node = [ 'id' => $permission->id, 'identifier' => $permission->identifier, 'name' => $permission->getLocalizedName(), 'name_raw' => $permission->name, 'description' => $permission->getLocalizedDescription(), 'description_raw' => $permission->description, 'extension_type' => $permission->extension_type?->value, 'extension_identifier' => $permission->extension_identifier, 'type' => $permission->type?->value, 'is_assignable' => $permission->children->isEmpty(), 'resource_route_key' => $permission->resource_route_key, 'owner_key' => $permission->owner_key, 'children' => [], 'leaf_count' => $this->countLeafNodes($permission), ]; if ($permission->children->isNotEmpty()) { foreach ($permission->children as $child) { $node['children'][] = $this->formatPermissionNode($child); } } return $node; } /** * 하위 리프 노드 개수를 계산합니다. * * @param Permission $permission 권한 모델 * @return int 리프 노드 개수 */ private function countLeafNodes(Permission $permission): int { if ($permission->children->isEmpty()) { return 1; } $count = 0; foreach ($permission->children as $child) { $count += $this->countLeafNodes($child); } return $count; } /** * 트리 노드 배열을 특정 타입의 리프 노드만 포함하도록 재귀적으로 필터링합니다. * * 리프 노드는 해당 타입과 일치할 때만 포함되며, * 비리프 노드(그룹)는 하위에 해당 타입의 리프가 존재할 때만 포함됩니다. * * @param array $nodes 포맷된 권한 노드 배열 * @param string $type 필터링할 권한 타입 (admin/user) * @return array 필터링된 노드 배열 */ private function filterTreeByType(array $nodes, string $type): array { $filtered = []; foreach ($nodes as $node) { $filteredNode = $this->filterNodeByType($node, $type); if ($filteredNode !== null) { $filtered[] = $filteredNode; } } return $filtered; } /** * 단일 노드를 특정 타입으로 필터링합니다 (재귀). * * @param array $node 포맷된 권한 노드 * @param string $type 필터링할 권한 타입 * @return array|null 필터링된 노드 또는 null (해당 타입 없음) */ private function filterNodeByType(array $node, string $type): ?array { // 리프 노드: 타입이 일치하면 포함, 아니면 제외 if (empty($node['children'])) { return ($node['type'] === $type) ? $node : null; } // 비리프 노드: 자식을 재귀적으로 필터링 $filteredChildren = $this->filterTreeByType($node['children'], $type); // 해당 타입의 하위 노드가 없으면 이 브랜치 제외 if (empty($filteredChildren)) { return null; } $node['children'] = $filteredChildren; $node['leaf_count'] = $this->countLeafNodesFromArray($filteredChildren); return $node; } /** * 포맷된 노드 배열에서 리프 노드 개수를 계산합니다. * * @param array $nodes 포맷된 권한 노드 배열 * @return int 리프 노드 개수 */ private function countLeafNodesFromArray(array $nodes): int { $count = 0; foreach ($nodes as $node) { if (empty($node['children'])) { $count++; } else { $count += $this->countLeafNodesFromArray($node['children']); } } return $count; } /** * 할당 가능한 권한 ID 목록을 반환합니다. (리프 노드만) * * @return array 할당 가능한 권한 ID 배열 */ public function getAssignablePermissionIds(): array { return $this->permissionRepository->getAssignableIds(); } /** * 모든 권한 목록을 반환합니다. * * @return Collection 권한 컬렉션 */ public function getAllPermissions(): Collection { return $this->permissionRepository->getAll(); } /** * 레이아웃 편집기 표시 권한 후보 목록을 반환합니다. * * 코어 + 활성 확장 권한 전체를 `{key, name}` 형태로 평탄화합니다. `key` 는 권한 * 식별자(예: `core.users.read`), `name` 은 지정 로케일 친화 표시명(다국어 array * 의 해당 로케일 → 첫 값 → 식별자 순 폴백)입니다. 편집기 속성 모달의 표시 권한 * TagInput 후보로 사용됩니다. * * 자식을 가진 상위(카테고리) 권한은 실제 할당 불가하므로 제외하고, 리프 노드 * (`isAssignable` — 자식 없음)만 후보로 반환합니다. 카테고리 행이 후보에 섞여 * 표시되던 결함을 차단합니다. * * @param string $locale 표시명 로케일 * @return array 권한 후보 목록 (리프 권한) */ public function getPermissionCandidates(string $locale): array { $all = $this->permissionRepository->getAll(); // 다른 권한의 parent_id 로 참조되는 ID 집합 = 카테고리(자식 보유). N+1 회피를 위해 // 메모리에서 1회 계산한다(getAll 로 이미 전량 로드됨). $parentIds = $all ->pluck('parent_id') ->filter(fn ($id): bool => $id !== null) ->unique() ->flip(); return $all ->reject(fn (Permission $permission): bool => $parentIds->has($permission->id)) ->map(function (Permission $permission) use ($locale): array { $name = $permission->name; $label = is_array($name) ? ($name[$locale] ?? (reset($name) ?: $permission->identifier)) : (is_string($name) && $name !== '' ? $name : $permission->identifier); return [ 'key' => (string) $permission->identifier, 'name' => (string) $label, ]; }) ->filter(fn (array $row): bool => $row['key'] !== '') ->values() ->all(); } /** * 특정 권한을 ID로 조회합니다. * * @param int $id 권한 ID * @return Permission|null 권한 모델 또는 null */ public function findById(int $id): ?Permission { return $this->permissionRepository->findById($id); } /** * 특정 확장의 권한들을 조회합니다. * * @param ExtensionOwnerType $type 확장 타입 * @param string|null $identifier 확장 식별자 * @return Collection 권한 컬렉션 */ public function getByExtension(ExtensionOwnerType $type, ?string $identifier = null): Collection { return $this->permissionRepository->getByExtension($type, $identifier); } }