feat: [메뉴] 통합메뉴관리 - 글로벌에서 가져오기 기능 구현

- PULL 방식 메뉴 가져오기 (테넌트가 글로벌에서 선택적으로 가져옴)
- 모드 전환 UI (내 메뉴 / 글로벌에서 가져오기)
- 체크박스 선택으로 다중 메뉴 가져오기 지원
- 가져오기 모드에서 읽기 전용 상태 배지 표시
- hidden input으로 HTMX mode 파라미터 전달 수정
This commit is contained in:
2025-12-02 19:16:23 +09:00
parent d4051e20fa
commit 28b4ec8afd
6 changed files with 495 additions and 18 deletions

View File

@@ -3,8 +3,10 @@
namespace App\Services;
use App\Models\Commons\Menu;
use App\Models\Tenants\Tenant;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class MenuService
{
@@ -440,4 +442,114 @@ private function compactSiblings(?int $parentId): void
$order++;
}
}
/**
* 복사 가능한 글로벌 메뉴 목록 조회
* (현재 테넌트에 존재하지 않는 글로벌 메뉴만)
*/
public function getAvailableGlobalMenus(int $tenantId): Collection
{
// 글로벌 메뉴 전체 조회
$globalMenus = Menu::whereNull('tenant_id')
->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();
// 현재 테넌트에 없는 글로벌 메뉴만 필터링
$availableMenus = $globalMenus->filter(function ($menu) use ($existingGlobalIds) {
return ! in_array($menu->id, $existingGlobalIds);
});
// 트리 구조로 정렬 (depth 정보 포함)
return $this->flattenMenuTree($availableMenus);
}
/**
* 선택한 글로벌 메뉴를 현재 테넌트로 복사
*/
public function copyFromGlobal(int $tenantId, array $menuIds): array
{
if (empty($menuIds)) {
return ['success' => false, 'message' => '복사할 메뉴를 선택해주세요.', 'copied' => 0];
}
// 선택된 글로벌 메뉴 조회
$globalMenus = Menu::whereNull('tenant_id')
->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,
];
});
}
}