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; }); } /** * 부모 메뉴 목록 조회 (드롭다운용) - 트리 구조 순서로 정렬, depth 정보 포함 */ public function getParentMenus(?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(); // 트리 구조로 정렬 (depth 정보 포함) return $this->flattenMenuTree($allMenus); } /** * 메뉴 생성 */ 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(); } /** * 메뉴 영구 삭제 (슈퍼관리자 전용) * - 연관 권한(permissions)도 함께 삭제 * - role_has_permissions, model_has_permissions는 FK CASCADE로 자동 삭제 * - 삭제 정보를 archived_records에 저장 * * @return array{success: bool, message: string, deleted_permissions: array} */ public function forceDeleteMenu(int $id): array { $menu = Menu::withTrashed()->findOrFail($id); // 자식 메뉴가 있는 경우 영구 삭제 불가 if ($menu->children()->withTrashed()->count() > 0) { return [ 'success' => false, 'message' => '자식 메뉴가 있어 삭제할 수 없습니다.', 'deleted_permissions' => [], ]; } return DB::transaction(function () use ($menu) { // 연관 권한 조회 (삭제 전 기록용) $permissions = Permission::where('name', 'like', "menu:{$menu->id}.%")->get(); $permissionData = $permissions->map(fn ($p) => [ 'id' => $p->id, 'name' => $p->name, 'guard_name' => $p->guard_name, 'tenant_id' => $p->tenant_id, ])->toArray(); // 역할-권한 연결 정보 조회 $rolePermissions = DB::table('role_has_permissions') ->join('roles', 'role_has_permissions.role_id', '=', 'roles.id') ->whereIn('role_has_permissions.permission_id', $permissions->pluck('id')) ->select('roles.id as role_id', 'roles.name as role_name', 'role_has_permissions.permission_id') ->get() ->toArray(); // 아카이브 레코드 생성 $batchId = (string) Str::uuid(); $archivedRecord = ArchivedRecord::create([ 'batch_id' => $batchId, 'batch_description' => "메뉴 영구 삭제: {$menu->name} (ID: {$menu->id})", 'record_type' => 'menu', 'tenant_id' => $menu->tenant_id, 'original_id' => $menu->id, 'main_data' => $menu->toArray(), 'schema_version' => '1.0', 'deleted_by' => auth()->id(), 'deleted_at' => now(), 'notes' => "메뉴 영구 삭제 - 연관 권한 {$permissions->count()}개 함께 삭제", ]); // 연관 권한 정보 저장 if ($permissions->isNotEmpty()) { ArchivedRecordRelation::create([ 'archived_record_id' => $archivedRecord->id, 'table_name' => 'permissions', 'data' => $permissionData, 'record_count' => $permissions->count(), ]); } // 역할-권한 연결 정보 저장 if (! empty($rolePermissions)) { ArchivedRecordRelation::create([ 'archived_record_id' => $archivedRecord->id, 'table_name' => 'role_has_permissions', 'data' => $rolePermissions, 'record_count' => count($rolePermissions), ]); } // 연관 권한 삭제 (FK CASCADE로 role_has_permissions, model_has_permissions 자동 삭제) Permission::where('name', 'like', "menu:{$menu->id}.%")->delete(); // 메뉴 영구 삭제 $menu->forceDelete(); return [ 'success' => true, 'message' => "메뉴 '{$menu->name}'와 연관 권한 {$permissions->count()}개가 삭제되었습니다.", 'deleted_permissions' => $permissionData, 'batch_id' => $batchId, ]; }); } /** * 메뉴 활성 상태 토글 */ 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(); } /** * 메뉴 순서 변경 (드래그앤드롭) * 같은 parent_id 내에서만 순서 변경 */ public function reorderMenus(array $items): bool { return \DB::transaction(function () use ($items) { foreach ($items as $item) { Menu::where('id', $item['id']) ->update([ 'sort_order' => $item['sort_order'], 'updated_by' => auth()->id(), ]); } return true; }); } /** * 메뉴 이동 (계층 구조 변경) * - 다른 부모 아래로 이동 가능 * - 하위 메뉴는 자동으로 따라감 * - 순환 참조 방지 */ public function moveMenu(int $menuId, ?int $newParentId, int $sortOrder): array { $menu = Menu::find($menuId); if (! $menu) { return ['success' => false, 'message' => '메뉴를 찾을 수 없습니다.']; } // 순환 참조 방지: 자신의 하위 메뉴로 이동 불가 if ($newParentId !== null && $this->isDescendant($menuId, $newParentId)) { return ['success' => false, 'message' => '자신의 하위 메뉴로 이동할 수 없습니다.']; } // 자기 자신을 부모로 설정 방지 if ($newParentId === $menuId) { return ['success' => false, 'message' => '자기 자신을 부모로 설정할 수 없습니다.']; } return \DB::transaction(function () use ($menu, $newParentId, $sortOrder) { $oldParentId = $menu->parent_id; // 부모 변경 $menu->parent_id = $newParentId; $menu->sort_order = $sortOrder; $menu->updated_by = auth()->id(); $menu->save(); // 같은 부모의 다른 메뉴들 순서 재정렬 $this->reorderSiblings($newParentId, $menu->id, $sortOrder); // 이전 부모의 메뉴들도 순서 재정렬 (빈 자리 채우기) if ($oldParentId !== $newParentId) { $this->compactSiblings($oldParentId); } return ['success' => true, 'message' => '메뉴가 이동되었습니다.']; }); } /** * 특정 메뉴가 다른 메뉴의 하위인지 확인 (순환 참조 방지) */ private function isDescendant(int $ancestorId, int $menuId): bool { $menu = Menu::find($menuId); while ($menu) { if ($menu->id === $ancestorId) { return true; } $menu = $menu->parent; } return false; } /** * 같은 부모의 형제 메뉴들 순서 재정렬 */ private function reorderSiblings(?int $parentId, int $excludeId, int $insertAt): void { $siblings = Menu::where('parent_id', $parentId) ->where('id', '!=', $excludeId) ->orderBy('sort_order') ->get(); $order = 1; foreach ($siblings as $sibling) { if ($order === $insertAt) { $order++; // 삽입 위치 건너뛰기 } if ($sibling->sort_order !== $order) { $sibling->update(['sort_order' => $order]); } $order++; } } /** * 형제 메뉴들 순서 압축 (빈 자리 채우기) */ private function compactSiblings(?int $parentId): void { $siblings = Menu::where('parent_id', $parentId) ->orderBy('sort_order') ->get(); $order = 1; foreach ($siblings as $sibling) { if ($sibling->sort_order !== $order) { $sibling->update(['sort_order' => $order]); } $order++; } } /** * 글로벌 부모 메뉴 목록 조회 (드롭다운용) - 트리 구조 순서로 정렬, depth 정보 포함 */ public function getGlobalParentMenus(): Collection { $allMenus = GlobalMenu::query() ->where('is_active', true) ->orderBy('sort_order') ->orderBy('id') ->get(); // 트리 구조로 정렬 (depth 정보 포함) return $this->flattenMenuTree($allMenus); } /** * 글로벌 메뉴 상세 조회 */ public function getGlobalMenuById(int $id): ?GlobalMenu { return GlobalMenu::with(['parent', 'children'])->find($id); } /** * 글로벌 메뉴 목록 조회 (페이지네이션) - 트리 구조로 정렬 */ public function getGlobalMenus(array $filters = [], int $perPage = 15): LengthAwarePaginator { $query = GlobalMenu::query()->withTrashed(); // 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']); } // 모든 메뉴 가져오기 (트리 구조 정렬을 위해) $allMenus = $query->with(['parent'])->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()] ); } /** * 글로벌 메뉴 생성 */ public function createGlobalMenu(array $data): GlobalMenu { // 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'; // parent_id null 처리 if (empty($data['parent_id'])) { $data['parent_id'] = null; } return GlobalMenu::create($data); } /** * 글로벌 메뉴 수정 */ public function updateGlobalMenu(int $id, array $data): bool { $menu = $this->getGlobalMenuById($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'; // 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 deleteGlobalMenu(int $id): bool { $menu = $this->getGlobalMenuById($id); if (! $menu) { return false; } // 자식 메뉴가 있는 경우 삭제 불가 if ($menu->children()->count() > 0) { return false; } return $menu->delete(); } /** * 글로벌 메뉴 복원 */ public function restoreGlobalMenu(int $id): bool { $menu = GlobalMenu::onlyTrashed()->findOrFail($id); return $menu->restore(); } /** * 글로벌 메뉴 영구 삭제 * - 연관 권한(permissions)도 함께 삭제 * - 이 글로벌 메뉴를 참조하는 테넌트 메뉴들의 global_menu_id도 null 처리 * - 삭제 정보를 archived_records에 저장 * * @return array{success: bool, message: string, deleted_permissions: array} */ public function forceDeleteGlobalMenu(int $id): array { $menu = GlobalMenu::withTrashed()->findOrFail($id); // 자식 메뉴가 있는 경우 영구 삭제 불가 if ($menu->children()->withTrashed()->count() > 0) { return [ 'success' => false, 'message' => '자식 메뉴가 있어 삭제할 수 없습니다.', 'deleted_permissions' => [], ]; } return DB::transaction(function () use ($menu) { // 연관 권한 조회 (삭제 전 기록용) $permissions = Permission::where('name', 'like', "global_menu:{$menu->id}.%")->get(); $permissionData = $permissions->map(fn ($p) => [ 'id' => $p->id, 'name' => $p->name, 'guard_name' => $p->guard_name, 'tenant_id' => $p->tenant_id, ])->toArray(); // 역할-권한 연결 정보 조회 $rolePermissions = DB::table('role_has_permissions') ->join('roles', 'role_has_permissions.role_id', '=', 'roles.id') ->whereIn('role_has_permissions.permission_id', $permissions->pluck('id')) ->select('roles.id as role_id', 'roles.name as role_name', 'role_has_permissions.permission_id') ->get() ->toArray(); // 참조하는 테넌트 메뉴 조회 $referencingMenus = Menu::withTrashed() ->where('global_menu_id', $menu->id) ->get(['id', 'tenant_id', 'name']) ->toArray(); // 아카이브 레코드 생성 $batchId = (string) Str::uuid(); $archivedRecord = ArchivedRecord::create([ 'batch_id' => $batchId, 'batch_description' => "글로벌 메뉴 영구 삭제: {$menu->name} (ID: {$menu->id})", 'record_type' => 'global_menu', 'tenant_id' => null, 'original_id' => $menu->id, 'main_data' => $menu->toArray(), 'schema_version' => '1.0', 'deleted_by' => auth()->id(), 'deleted_at' => now(), 'notes' => "글로벌 메뉴 영구 삭제 - 연관 권한 {$permissions->count()}개, 참조 테넌트 메뉴 ".count($referencingMenus).'개 해제', ]); // 연관 권한 정보 저장 if ($permissions->isNotEmpty()) { ArchivedRecordRelation::create([ 'archived_record_id' => $archivedRecord->id, 'table_name' => 'permissions', 'data' => $permissionData, 'record_count' => $permissions->count(), ]); } // 역할-권한 연결 정보 저장 if (! empty($rolePermissions)) { ArchivedRecordRelation::create([ 'archived_record_id' => $archivedRecord->id, 'table_name' => 'role_has_permissions', 'data' => $rolePermissions, 'record_count' => count($rolePermissions), ]); } // 참조 테넌트 메뉴 정보 저장 if (! empty($referencingMenus)) { ArchivedRecordRelation::create([ 'archived_record_id' => $archivedRecord->id, 'table_name' => 'menus (referencing)', 'data' => $referencingMenus, 'record_count' => count($referencingMenus), ]); } // 연관 권한 삭제 Permission::where('name', 'like', "global_menu:{$menu->id}.%")->delete(); // 이 글로벌 메뉴를 참조하는 테넌트 메뉴들의 참조 해제 Menu::withTrashed() ->where('global_menu_id', $menu->id) ->update(['global_menu_id' => null, 'is_customized' => true]); // 글로벌 메뉴 영구 삭제 $menu->forceDelete(); return [ 'success' => true, 'message' => "글로벌 메뉴 '{$menu->name}'와 연관 권한 {$permissions->count()}개가 삭제되었습니다.", 'deleted_permissions' => $permissionData, 'referencing_menus_unlinked' => count($referencingMenus), 'batch_id' => $batchId, ]; }); } /** * 글로벌 메뉴 활성 상태 토글 */ public function toggleGlobalActive(int $id): bool { $menu = $this->getGlobalMenuById($id); if (! $menu) { return false; } $menu->is_active = ! $menu->is_active; return $menu->save(); } /** * 글로벌 메뉴 숨김 상태 토글 */ public function toggleGlobalHidden(int $id): bool { $menu = $this->getGlobalMenuById($id); if (! $menu) { return false; } $menu->hidden = ! $menu->hidden; return $menu->save(); } /** * 글로벌 메뉴 순서 변경 */ public function reorderGlobalMenus(array $items): bool { return \DB::transaction(function () use ($items) { foreach ($items as $item) { GlobalMenu::where('id', $item['id']) ->update(['sort_order' => $item['sort_order']]); } return true; }); } /** * 글로벌 메뉴 이동 (계층 구조 변경 - 인덴트/아웃덴트) * - 다른 부모 아래로 이동 가능 * - 하위 메뉴는 자동으로 따라감 * - 순환 참조 방지 */ public function moveGlobalMenu(int $menuId, ?int $newParentId, int $sortOrder): array { $menu = GlobalMenu::find($menuId); if (! $menu) { return ['success' => false, 'message' => '글로벌 메뉴를 찾을 수 없습니다.']; } // 순환 참조 방지: 자신의 하위 메뉴로 이동 불가 if ($newParentId !== null && $this->isGlobalDescendant($menuId, $newParentId)) { return ['success' => false, 'message' => '자신의 하위 메뉴로 이동할 수 없습니다.']; } // 자기 자신을 부모로 설정 방지 if ($newParentId === $menuId) { return ['success' => false, 'message' => '자기 자신을 부모로 설정할 수 없습니다.']; } return \DB::transaction(function () use ($menu, $newParentId, $sortOrder) { $oldParentId = $menu->parent_id; // 부모 변경 $menu->parent_id = $newParentId; $menu->sort_order = $sortOrder; $menu->save(); // 같은 부모의 다른 메뉴들 순서 재정렬 $this->reorderGlobalSiblings($newParentId, $menu->id, $sortOrder); // 이전 부모의 메뉴들도 순서 재정렬 (빈 자리 채우기) if ($oldParentId !== $newParentId) { $this->compactGlobalSiblings($oldParentId); } return ['success' => true, 'message' => '글로벌 메뉴가 이동되었습니다.']; }); } /** * 특정 글로벌 메뉴가 다른 메뉴의 하위인지 확인 (순환 참조 방지) */ private function isGlobalDescendant(int $ancestorId, int $menuId): bool { $menu = GlobalMenu::find($menuId); while ($menu) { if ($menu->id === $ancestorId) { return true; } $menu = $menu->parent; } return false; } /** * 같은 부모의 글로벌 형제 메뉴들 순서 재정렬 */ private function reorderGlobalSiblings(?int $parentId, int $excludeId, int $insertAt): void { $siblings = GlobalMenu::where('parent_id', $parentId) ->where('id', '!=', $excludeId) ->orderBy('sort_order') ->get(); $order = 1; foreach ($siblings as $sibling) { if ($order === $insertAt) { $order++; // 삽입 위치 건너뛰기 } if ($sibling->sort_order !== $order) { $sibling->update(['sort_order' => $order]); } $order++; } } /** * 글로벌 형제 메뉴들 순서 압축 (빈 자리 채우기) */ private function compactGlobalSiblings(?int $parentId): void { $siblings = GlobalMenu::where('parent_id', $parentId) ->orderBy('sort_order') ->get(); $order = 1; foreach ($siblings as $sibling) { if ($sibling->sort_order !== $order) { $sibling->update(['sort_order' => $order]); } $order++; } } /** * 글로벌 메뉴 목록 조회 (가져오기 상태 포함) * - 모든 글로벌 메뉴 반환 * - 이미 가져온 메뉴는 is_imported=true로 표시 */ public function getAllGlobalMenusWithStatus(int $tenantId): Collection { // 글로벌 메뉴 전체 조회 (global_menus 테이블에서) $globalMenus = GlobalMenu::query() ->where('is_active', true) ->orderBy('parent_id') ->orderBy('sort_order') ->get(); // 현재 테넌트에 이미 복사된 메뉴의 global_menu_id 목록 $existingGlobalIds = Menu::where('tenant_id', $tenantId) ->whereNotNull('global_menu_id') ->pluck('global_menu_id') ->toArray(); // 각 메뉴에 is_imported 속성 추가 $globalMenus->each(function ($menu) use ($existingGlobalIds) { $menu->is_imported = in_array($menu->id, $existingGlobalIds); }); // 트리 구조로 정렬 (depth 정보 포함) return $this->flattenMenuTree($globalMenus); } /** * 선택한 글로벌 메뉴를 현재 테넌트로 복사 */ public function copyFromGlobal(int $tenantId, array $menuIds): array { if (empty($menuIds)) { return ['success' => false, 'message' => '복사할 메뉴를 선택해주세요.', 'copied' => 0]; } // 선택된 글로벌 메뉴 조회 (global_menus 테이블에서) $globalMenus = GlobalMenu::query() ->whereIn('id', $menuIds) ->orderBy('parent_id') // 부모 먼저 복사하기 위해 ->orderBy('sort_order') ->get(); if ($globalMenus->isEmpty()) { return ['success' => false, 'message' => '유효한 글로벌 메뉴가 없습니다.', 'copied' => 0]; } $copied = 0; return DB::transaction(function () use ($globalMenus, $tenantId, &$copied) { // global_menu_id → 새로 생성된 tenant menu id 매핑 $idMapping = []; foreach ($globalMenus as $globalMenu) { // 이미 복사된 메뉴인지 확인 $exists = Menu::where('tenant_id', $tenantId) ->where('global_menu_id', $globalMenu->id) ->exists(); if ($exists) { continue; } // 부모 메뉴 매핑 (글로벌 → 테넌트) $newParentId = null; if ($globalMenu->parent_id) { // 이번 복사에서 생성된 부모가 있는지 확인 if (isset($idMapping[$globalMenu->parent_id])) { $newParentId = $idMapping[$globalMenu->parent_id]; } else { // 기존에 복사된 부모 메뉴가 있는지 확인 $parentTenantMenu = Menu::where('tenant_id', $tenantId) ->where('global_menu_id', $globalMenu->parent_id) ->first(); $newParentId = $parentTenantMenu?->id; } } // 새 테넌트 메뉴 생성 $newMenu = Menu::create([ 'tenant_id' => $tenantId, 'parent_id' => $newParentId, 'global_menu_id' => $globalMenu->id, 'name' => $globalMenu->name, 'url' => $globalMenu->url, 'icon' => $globalMenu->icon, 'sort_order' => $globalMenu->sort_order, 'is_active' => $globalMenu->is_active, 'hidden' => $globalMenu->hidden, 'is_external' => $globalMenu->is_external, 'external_url' => $globalMenu->external_url, 'is_customized' => false, 'created_by' => auth()->id(), 'updated_by' => auth()->id(), ]); // ID 매핑 저장 (자식 메뉴 복사 시 참조용) $idMapping[$globalMenu->id] = $newMenu->id; $copied++; } return [ 'success' => true, 'message' => "{$copied}개 메뉴가 복사되었습니다.", 'copied' => $copied, ]; }); } }