- MenuService::updateMenuForBoard() 메서드 추가 - 시스템 게시판: global_menus + 연결된 모든 menus URL 업데이트 - 테넌트 게시판: 해당 테넌트의 menus만 URL 업데이트 - BoardService::updateAnyBoard()에서 board_code/name 변경 감지 시 호출 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1556 lines
53 KiB
PHP
1556 lines
53 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Archives\ArchivedRecord;
|
|
use App\Models\Archives\ArchivedRecordRelation;
|
|
use App\Models\Commons\GlobalMenu;
|
|
use App\Models\Commons\Menu;
|
|
use App\Models\Permission;
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
class MenuService
|
|
{
|
|
// =========================================================================
|
|
// 게시판 메뉴 연동 헬퍼
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 게시판 연동 URL 패턴인지 확인
|
|
* - /customer-center/* : 시스템 게시판 (고객센터)
|
|
* - /boards/* : 테넌트 게시판
|
|
*/
|
|
public function isBoardMenuUrl(?string $url): bool
|
|
{
|
|
if (empty($url)) {
|
|
return false;
|
|
}
|
|
|
|
return str_starts_with($url, '/customer-center/') || str_starts_with($url, '/boards/');
|
|
}
|
|
|
|
/**
|
|
* 게시판 연동 메뉴인지 확인 (Menu 또는 GlobalMenu)
|
|
*/
|
|
public function isBoardMenu(Menu|GlobalMenu $menu): bool
|
|
{
|
|
return $this->isBoardMenuUrl($menu->url);
|
|
}
|
|
|
|
/**
|
|
* 게시판 메뉴 URL 수동 생성/수정 방지 검증
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 URL 패턴 사용 시
|
|
*/
|
|
public function validateNotBoardUrl(?string $url): void
|
|
{
|
|
if ($this->isBoardMenuUrl($url)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 URL 패턴(/customer-center/*, /boards/*)은 직접 사용할 수 없습니다. 게시판 관리에서 생성해주세요.'
|
|
);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 메뉴 목록 조회
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 메뉴 목록 조회 (페이지네이션) - 트리 구조로 정렬
|
|
*/
|
|
public function getMenus(array $filters = [], int $perPage = 15): LengthAwarePaginator
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
$query = Menu::query()->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);
|
|
}
|
|
|
|
/**
|
|
* 메뉴 생성
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 URL 패턴 사용 시
|
|
*/
|
|
public function createMenu(array $data): Menu
|
|
{
|
|
// 게시판 URL 패턴 수동 생성 방지
|
|
$this->validateNotBoardUrl($data['url'] ?? null);
|
|
|
|
$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);
|
|
}
|
|
|
|
/**
|
|
* 메뉴 수정
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 메뉴 수정 또는 게시판 URL 패턴 사용 시
|
|
*/
|
|
public function updateMenu(int $id, array $data): bool
|
|
{
|
|
$menu = $this->getMenuById($id);
|
|
if (! $menu) {
|
|
return false;
|
|
}
|
|
|
|
// 게시판 연동 메뉴는 수정 불가 (활성/숨김 토글만 허용)
|
|
if ($this->isBoardMenu($menu)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 메뉴는 수정할 수 없습니다. 게시판 관리에서 수정해주세요.'
|
|
);
|
|
}
|
|
|
|
// 새 URL이 게시판 URL 패턴이면 거부
|
|
$this->validateNotBoardUrl($data['url'] ?? null);
|
|
|
|
// 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)
|
|
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 메뉴 삭제 시도 시
|
|
*/
|
|
public function deleteMenu(int $id): bool
|
|
{
|
|
$menu = $this->getMenuById($id);
|
|
if (! $menu) {
|
|
return false;
|
|
}
|
|
|
|
// 게시판 연동 메뉴는 삭제 불가
|
|
if ($this->isBoardMenu($menu)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 메뉴는 삭제할 수 없습니다. 게시판 관리에서 삭제해주세요.'
|
|
);
|
|
}
|
|
|
|
// 자식 메뉴가 있는 경우 삭제 불가
|
|
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}
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 메뉴 영구 삭제 시도 시
|
|
*/
|
|
public function forceDeleteMenu(int $id): array
|
|
{
|
|
$menu = Menu::withTrashed()->findOrFail($id);
|
|
|
|
// 게시판 연동 메뉴는 영구 삭제 불가
|
|
if ($this->isBoardMenu($menu)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 메뉴는 영구 삭제할 수 없습니다. 게시판 관리에서 삭제해주세요.'
|
|
);
|
|
}
|
|
|
|
// 자식 메뉴가 있는 경우 영구 삭제 불가
|
|
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()]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 글로벌 메뉴 생성
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 URL 패턴 사용 시
|
|
*/
|
|
public function createGlobalMenu(array $data): GlobalMenu
|
|
{
|
|
// 게시판 URL 패턴 수동 생성 방지
|
|
$this->validateNotBoardUrl($data['url'] ?? null);
|
|
|
|
// 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);
|
|
}
|
|
|
|
/**
|
|
* 글로벌 메뉴 수정
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 메뉴 수정 또는 게시판 URL 패턴 사용 시
|
|
*/
|
|
public function updateGlobalMenu(int $id, array $data): bool
|
|
{
|
|
$menu = $this->getGlobalMenuById($id);
|
|
if (! $menu) {
|
|
return false;
|
|
}
|
|
|
|
// 게시판 연동 메뉴는 수정 불가 (활성/숨김 토글만 허용)
|
|
if ($this->isBoardMenu($menu)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 메뉴는 수정할 수 없습니다. 게시판 관리에서 수정해주세요.'
|
|
);
|
|
}
|
|
|
|
// 새 URL이 게시판 URL 패턴이면 거부
|
|
$this->validateNotBoardUrl($data['url'] ?? null);
|
|
|
|
// 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)
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 연동 메뉴인 경우
|
|
*/
|
|
public function deleteGlobalMenu(int $id): bool
|
|
{
|
|
$menu = $this->getGlobalMenuById($id);
|
|
if (! $menu) {
|
|
return false;
|
|
}
|
|
|
|
// 게시판 연동 메뉴는 삭제 불가
|
|
if ($this->isBoardMenu($menu)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 메뉴는 삭제할 수 없습니다. 게시판 관리에서 삭제해주세요.'
|
|
);
|
|
}
|
|
|
|
// 자식 메뉴가 있는 경우 삭제 불가
|
|
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}
|
|
*
|
|
* @throws \InvalidArgumentException 게시판 연동 메뉴인 경우
|
|
*/
|
|
public function forceDeleteGlobalMenu(int $id): array
|
|
{
|
|
$menu = GlobalMenu::withTrashed()->findOrFail($id);
|
|
|
|
// 게시판 연동 메뉴는 영구 삭제 불가
|
|
if ($this->isBoardMenu($menu)) {
|
|
throw new \InvalidArgumentException(
|
|
'게시판 연동 메뉴는 영구 삭제할 수 없습니다. 게시판 관리에서 삭제해주세요.'
|
|
);
|
|
}
|
|
|
|
// 자식 메뉴가 있는 경우 영구 삭제 불가
|
|
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,
|
|
];
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// 게시판 메뉴 자동 생성
|
|
// =========================================================================
|
|
|
|
/**
|
|
* URL로 부모 메뉴 찾기 (우선순위: /customer-center → /boards or /system-boards → null)
|
|
*
|
|
* @param bool $isSystem 시스템 게시판 여부
|
|
* @param int|null $tenantId 테넌트 ID (시스템 게시판이면 null)
|
|
*/
|
|
public function findParentMenuForBoard(bool $isSystem, ?int $tenantId = null): ?int
|
|
{
|
|
if ($isSystem) {
|
|
// 시스템 게시판: global_menus에서 찾기
|
|
// 우선순위: /customer-center → /system-boards → /boards → null
|
|
$priorityUrls = ['/customer-center', '/system-boards', '/boards'];
|
|
|
|
foreach ($priorityUrls as $url) {
|
|
$menu = GlobalMenu::where('url', $url)
|
|
->where('is_active', true)
|
|
->first();
|
|
|
|
if ($menu) {
|
|
return $menu->id;
|
|
}
|
|
}
|
|
|
|
return null; // 최상위로 추가
|
|
} else {
|
|
// 테넌트 게시판: menus에서 찾기
|
|
// 우선순위: /boards → /customer-center → null
|
|
$priorityUrls = ['/boards', '/customer-center'];
|
|
|
|
foreach ($priorityUrls as $url) {
|
|
$query = Menu::where('url', $url)
|
|
->where('is_active', true);
|
|
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
} else {
|
|
$query->whereNull('tenant_id');
|
|
}
|
|
|
|
$menu = $query->first();
|
|
|
|
if ($menu) {
|
|
return $menu->id;
|
|
}
|
|
}
|
|
|
|
return null; // 최상위로 추가
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판에 대한 메뉴 자동 생성
|
|
*
|
|
* @param array $boardData 게시판 정보 (board_code, name, is_system, tenant_id)
|
|
* @return GlobalMenu|Menu|null 생성된 메뉴 또는 null
|
|
*/
|
|
public function createMenuForBoard(array $boardData): GlobalMenu|Menu|null
|
|
{
|
|
$isSystem = $boardData['is_system'] ?? false;
|
|
$tenantId = $boardData['tenant_id'] ?? null;
|
|
$boardCode = $boardData['board_code'];
|
|
$boardName = $boardData['name'];
|
|
|
|
// 부모 메뉴 찾기
|
|
$parentId = $this->findParentMenuForBoard($isSystem, $tenantId);
|
|
|
|
if ($isSystem) {
|
|
// 시스템 게시판 → global_menus + 모든 테넌트 menus에 추가
|
|
$url = '/customer-center/'.$boardCode;
|
|
|
|
// 1. GlobalMenu 조회 또는 생성
|
|
$globalMenu = GlobalMenu::where('url', $url)->first();
|
|
if (! $globalMenu) {
|
|
$maxOrder = GlobalMenu::where('parent_id', $parentId)->max('sort_order') ?? 0;
|
|
|
|
$globalMenu = GlobalMenu::create([
|
|
'parent_id' => $parentId,
|
|
'name' => $boardName,
|
|
'url' => $url,
|
|
'icon' => 'document-text',
|
|
'sort_order' => $maxOrder + 1,
|
|
'is_active' => true,
|
|
'hidden' => false,
|
|
'is_external' => false,
|
|
]);
|
|
}
|
|
|
|
// 2. 모든 테넌트의 Menu에 추가 (global_menu_id 연결)
|
|
$tenants = Tenant::active()->get();
|
|
foreach ($tenants as $tenant) {
|
|
$menuParentId = $this->findParentMenuForBoard(false, $tenant->id);
|
|
$menuExists = Menu::where('url', $url)->where('tenant_id', $tenant->id)->exists();
|
|
if (! $menuExists) {
|
|
$maxMenuOrder = Menu::where('parent_id', $menuParentId)
|
|
->where('tenant_id', $tenant->id)
|
|
->max('sort_order') ?? 0;
|
|
|
|
Menu::create([
|
|
'tenant_id' => $tenant->id,
|
|
'parent_id' => $menuParentId,
|
|
'global_menu_id' => $globalMenu->id,
|
|
'name' => $boardName,
|
|
'url' => $url,
|
|
'icon' => 'document-text',
|
|
'sort_order' => $maxMenuOrder + 1,
|
|
'is_active' => true,
|
|
'hidden' => false,
|
|
'is_external' => false,
|
|
'is_customized' => false,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
return $globalMenu;
|
|
} else {
|
|
// 테넌트 게시판 → menus에 추가
|
|
$url = '/boards/'.$boardCode;
|
|
|
|
// 중복 체크
|
|
$query = Menu::where('url', $url);
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
} else {
|
|
$query->whereNull('tenant_id');
|
|
}
|
|
|
|
if ($query->exists()) {
|
|
return null;
|
|
}
|
|
|
|
// 정렬 순서 계산
|
|
$maxOrderQuery = Menu::where('parent_id', $parentId);
|
|
if ($tenantId) {
|
|
$maxOrderQuery->where('tenant_id', $tenantId);
|
|
} else {
|
|
$maxOrderQuery->whereNull('tenant_id');
|
|
}
|
|
$maxOrder = $maxOrderQuery->max('sort_order') ?? 0;
|
|
|
|
return Menu::create([
|
|
'tenant_id' => $tenantId,
|
|
'parent_id' => $parentId,
|
|
'name' => $boardName,
|
|
'url' => $url,
|
|
'icon' => 'document-text', // 기본 아이콘
|
|
'sort_order' => $maxOrder + 1,
|
|
'is_active' => true,
|
|
'hidden' => false,
|
|
'is_external' => false,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 삭제 시 연결된 메뉴도 삭제
|
|
*
|
|
* @param string $boardCode 게시판 코드
|
|
* @param bool $isSystem 시스템 게시판 여부
|
|
* @param int|null $tenantId 테넌트 ID
|
|
*/
|
|
public function deleteMenuForBoard(string $boardCode, bool $isSystem, ?int $tenantId = null, bool $forceDelete = false): bool
|
|
{
|
|
if ($isSystem) {
|
|
$url = '/customer-center/'.$boardCode;
|
|
|
|
// 1. GlobalMenu 삭제
|
|
$globalMenu = $forceDelete
|
|
? GlobalMenu::withTrashed()->where('url', $url)->first()
|
|
: GlobalMenu::where('url', $url)->first();
|
|
|
|
if ($globalMenu) {
|
|
$forceDelete ? $globalMenu->forceDelete() : $globalMenu->delete();
|
|
}
|
|
|
|
// 2. 모든 테넌트의 Menu 삭제
|
|
$menusQuery = $forceDelete
|
|
? Menu::withTrashed()->where('url', $url)
|
|
: Menu::where('url', $url);
|
|
|
|
$menus = $menusQuery->get();
|
|
foreach ($menus as $menu) {
|
|
if (! $forceDelete) {
|
|
$menu->deleted_by = auth()->id();
|
|
$menu->save();
|
|
}
|
|
$forceDelete ? $menu->forceDelete() : $menu->delete();
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
$url = '/boards/'.$boardCode;
|
|
$query = $forceDelete
|
|
? Menu::withTrashed()->where('url', $url)
|
|
: Menu::where('url', $url);
|
|
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
} else {
|
|
$query->whereNull('tenant_id');
|
|
}
|
|
|
|
$menu = $query->first();
|
|
|
|
if ($menu) {
|
|
if (! $forceDelete) {
|
|
$menu->deleted_by = auth()->id();
|
|
$menu->save();
|
|
}
|
|
|
|
return $forceDelete ? $menu->forceDelete() : $menu->delete();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 게시판 복원 시 연결된 메뉴도 복원
|
|
* - soft-deleted 메뉴가 있으면 복원
|
|
* - 활성 메뉴가 이미 있으면 아무것도 안함
|
|
* - 둘 다 없으면 새로 생성
|
|
*
|
|
* @param string $boardCode 게시판 코드
|
|
* @param string $boardName 게시판 이름 (메뉴 생성 시 필요)
|
|
* @param bool $isSystem 시스템 게시판 여부
|
|
* @param int|null $tenantId 테넌트 ID
|
|
*/
|
|
public function restoreMenuForBoard(string $boardCode, string $boardName, bool $isSystem, ?int $tenantId = null): bool
|
|
{
|
|
if ($isSystem) {
|
|
$url = '/customer-center/'.$boardCode;
|
|
|
|
// === GlobalMenu 복원 또는 생성 ===
|
|
$globalMenu = GlobalMenu::withTrashed()->where('url', $url)->first();
|
|
if ($globalMenu && $globalMenu->trashed()) {
|
|
$globalMenu->restore();
|
|
$globalMenu->deleted_by = null;
|
|
$globalMenu->save();
|
|
} elseif (! $globalMenu) {
|
|
// 없으면 생성
|
|
$parentId = $this->findParentMenuForBoard(true, null);
|
|
$maxOrder = GlobalMenu::where('parent_id', $parentId)->max('sort_order') ?? 0;
|
|
$globalMenu = GlobalMenu::create([
|
|
'parent_id' => $parentId,
|
|
'name' => $boardName,
|
|
'url' => $url,
|
|
'icon' => 'document-text',
|
|
'sort_order' => $maxOrder + 1,
|
|
'is_active' => true,
|
|
'hidden' => false,
|
|
'is_external' => false,
|
|
]);
|
|
}
|
|
|
|
// === 모든 활성 테넌트의 Menu 복원/생성 (global_menu_id 연결) ===
|
|
$tenants = Tenant::active()->get();
|
|
foreach ($tenants as $tenant) {
|
|
$menuTrashed = Menu::onlyTrashed()->where('url', $url)->where('tenant_id', $tenant->id)->first();
|
|
if ($menuTrashed) {
|
|
$menuTrashed->restore();
|
|
$menuTrashed->deleted_by = null;
|
|
$menuTrashed->global_menu_id = $globalMenu->id;
|
|
$menuTrashed->save();
|
|
} elseif (! Menu::where('url', $url)->where('tenant_id', $tenant->id)->exists()) {
|
|
// 없으면 생성
|
|
$menuParentId = $this->findParentMenuForBoard(false, $tenant->id);
|
|
$maxMenuOrder = Menu::where('parent_id', $menuParentId)->where('tenant_id', $tenant->id)->max('sort_order') ?? 0;
|
|
Menu::create([
|
|
'tenant_id' => $tenant->id,
|
|
'parent_id' => $menuParentId,
|
|
'global_menu_id' => $globalMenu->id,
|
|
'name' => $boardName,
|
|
'url' => $url,
|
|
'icon' => 'document-text',
|
|
'sort_order' => $maxMenuOrder + 1,
|
|
'is_active' => true,
|
|
'hidden' => false,
|
|
'is_external' => false,
|
|
'is_customized' => false,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
$url = '/boards/'.$boardCode;
|
|
|
|
// 1. soft-deleted 메뉴 확인
|
|
$trashedQuery = Menu::onlyTrashed()->where('url', $url);
|
|
if ($tenantId) {
|
|
$trashedQuery->where('tenant_id', $tenantId);
|
|
} else {
|
|
$trashedQuery->whereNull('tenant_id');
|
|
}
|
|
$trashedMenu = $trashedQuery->first();
|
|
|
|
if ($trashedMenu) {
|
|
$restored = $trashedMenu->restore();
|
|
if ($restored) {
|
|
$trashedMenu->deleted_by = null;
|
|
$trashedMenu->save();
|
|
}
|
|
|
|
return $restored;
|
|
}
|
|
|
|
// 2. 활성 메뉴가 이미 있는지 확인
|
|
$activeQuery = Menu::where('url', $url);
|
|
if ($tenantId) {
|
|
$activeQuery->where('tenant_id', $tenantId);
|
|
} else {
|
|
$activeQuery->whereNull('tenant_id');
|
|
}
|
|
$activeMenu = $activeQuery->first();
|
|
|
|
if ($activeMenu) {
|
|
return true; // 이미 존재, 아무것도 안함
|
|
}
|
|
|
|
// 3. 둘 다 없으면 새로 생성
|
|
$created = $this->createMenuForBoard([
|
|
'board_code' => $boardCode,
|
|
'name' => $boardName,
|
|
'is_system' => false,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
return $created !== null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 게시판 코드 변경 시 연결된 메뉴 URL도 업데이트
|
|
*
|
|
* @param string $oldCode 기존 게시판 코드
|
|
* @param string $newCode 새 게시판 코드
|
|
* @param string $newName 새 게시판 이름
|
|
* @param bool $isSystem 시스템 게시판 여부
|
|
* @param int|null $tenantId 테넌트 ID (테넌트 게시판인 경우)
|
|
*/
|
|
public function updateMenuForBoard(string $oldCode, string $newCode, string $newName, bool $isSystem, ?int $tenantId = null): bool
|
|
{
|
|
if ($isSystem) {
|
|
// 시스템 게시판: global_menus + 연결된 모든 menus 업데이트
|
|
$oldUrl = '/customer-center/'.$oldCode;
|
|
$newUrl = '/customer-center/'.$newCode;
|
|
|
|
// 1. GlobalMenu 업데이트
|
|
$globalMenu = GlobalMenu::where('url', $oldUrl)->first();
|
|
if ($globalMenu) {
|
|
$globalMenu->update([
|
|
'url' => $newUrl,
|
|
'name' => $newName,
|
|
]);
|
|
|
|
// 2. 해당 global_menu_id를 참조하는 모든 menus 업데이트
|
|
Menu::where('global_menu_id', $globalMenu->id)
|
|
->update([
|
|
'url' => $newUrl,
|
|
'name' => $newName,
|
|
]);
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
// 테넌트 게시판: 해당 테넌트의 menus만 업데이트
|
|
$oldUrl = '/boards/'.$oldCode;
|
|
$newUrl = '/boards/'.$newCode;
|
|
|
|
$query = Menu::where('url', $oldUrl);
|
|
if ($tenantId) {
|
|
$query->where('tenant_id', $tenantId);
|
|
} else {
|
|
$query->whereNull('tenant_id');
|
|
}
|
|
|
|
$query->update([
|
|
'url' => $newUrl,
|
|
'name' => $newName,
|
|
]);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|