From 79aebfa14897845d842acc14691b67f0422b61f0 Mon Sep 17 00:00:00 2001 From: hskwon Date: Mon, 24 Nov 2025 22:02:09 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 트리 구조 정렬 및 표시 (무제한 depth 지원) - 접기/펼치기 기능 추가 (재귀적 처리) - 활성/숨김 상태 토글 기능 (실시간 업데이트) - 테넌트 필터링 (전체 선택 시 마스터 메뉴만 표시) - UI 개선 (토글 버튼, 외부 메뉴 표시) 추가된 파일: - MenuService: 비즈니스 로직 처리 - MenuController (API/Web): 라우트 처리 - MenuRequest: 유효성 검증 - views/menus/: 메뉴 관리 뷰 수정된 파일: - routes/api.php, web.php: 메뉴 라우트 추가 --- .../Controllers/Api/Admin/MenuController.php | 295 ++++++++++++++++ app/Http/Controllers/MenuController.php | 48 +++ app/Http/Requests/StoreMenuRequest.php | 69 ++++ app/Http/Requests/UpdateMenuRequest.php | 80 +++++ app/Services/MenuService.php | 321 ++++++++++++++++++ resources/views/menus/create.blade.php | 171 ++++++++++ resources/views/menus/edit.blade.php | 184 ++++++++++ resources/views/menus/index.blade.php | 194 +++++++++++ .../views/menus/partials/table.blade.php | 124 +++++++ routes/api.php | 20 ++ routes/web.php | 8 + 11 files changed, 1514 insertions(+) create mode 100644 app/Http/Controllers/Api/Admin/MenuController.php create mode 100644 app/Http/Controllers/MenuController.php create mode 100644 app/Http/Requests/StoreMenuRequest.php create mode 100644 app/Http/Requests/UpdateMenuRequest.php create mode 100644 app/Services/MenuService.php create mode 100644 resources/views/menus/create.blade.php create mode 100644 resources/views/menus/edit.blade.php create mode 100644 resources/views/menus/index.blade.php create mode 100644 resources/views/menus/partials/table.blade.php diff --git a/app/Http/Controllers/Api/Admin/MenuController.php b/app/Http/Controllers/Api/Admin/MenuController.php new file mode 100644 index 00000000..59450fd0 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/MenuController.php @@ -0,0 +1,295 @@ +menuService->getMenus( + $request->all(), + $request->integer('per_page', 10) + ); + + // HTMX 요청인 경우 HTML 반환 + if ($request->header('HX-Request')) { + $html = view('menus.partials.table', compact('menus'))->render(); + return response()->json(['html' => $html]); + } + + // 일반 API 요청인 경우 JSON 반환 + return response()->json([ + 'success' => true, + 'data' => $menus->items(), + 'meta' => [ + 'current_page' => $menus->currentPage(), + 'last_page' => $menus->lastPage(), + 'per_page' => $menus->perPage(), + 'total' => $menus->total(), + ], + ]); + } + + /** + * 메뉴 트리 구조 조회 + */ + public function tree(Request $request): JsonResponse + { + $tenantId = $request->integer('tenant_id') ?: session('selected_tenant_id'); + $tree = $this->menuService->getMenuTree($tenantId); + + return response()->json([ + 'success' => true, + 'data' => $tree, + ]); + } + + /** + * 메뉴 상세 조회 + */ + public function show(int $id): JsonResponse + { + $menu = $this->menuService->getMenuById($id); + + if (!$menu) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $menu, + ]); + } + + /** + * 메뉴 생성 + */ + public function store(StoreMenuRequest $request): JsonResponse + { + try { + $menu = $this->menuService->createMenu($request->validated()); + + return response()->json([ + 'success' => true, + 'message' => '메뉴가 생성되었습니다.', + 'data' => $menu, + 'redirect' => route('menus.index'), + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '메뉴 생성에 실패했습니다: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 메뉴 수정 + */ + public function update(UpdateMenuRequest $request, int $id): JsonResponse + { + try { + $result = $this->menuService->updateMenu($id, $request->validated()); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 찾을 수 없거나 수정할 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'message' => '메뉴가 수정되었습니다.', + 'redirect' => route('menus.index'), + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '메뉴 수정에 실패했습니다: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 메뉴 삭제 + */ + public function destroy(int $id): JsonResponse + { + try { + $result = $this->menuService->deleteMenu($id); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 찾을 수 없거나 자식 메뉴가 있어 삭제할 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'message' => '메뉴가 삭제되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '메뉴 삭제에 실패했습니다: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 메뉴 복원 + */ + public function restore(Request $request, int $id): JsonResponse + { + $this->menuService->restoreMenu($id); + + // HTMX 요청 시 테이블 새로고침 트리거 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '메뉴가 복원되었습니다.', + 'action' => 'refresh', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '메뉴가 복원되었습니다.', + ]); + } + + /** + * 메뉴 영구 삭제 (슈퍼관리자 전용) + */ + public function forceDestroy(Request $request, int $id): JsonResponse + { + // 슈퍼관리자 권한 체크 + if (!auth()->user()?->is_super_admin) { + return response()->json([ + 'success' => false, + 'message' => '권한이 없습니다.', + ], 403); + } + + try { + $result = $this->menuService->forceDeleteMenu($id); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 찾을 수 없거나 자식 메뉴가 있어 영구 삭제할 수 없습니다.', + ], 404); + } + + // HTMX 요청 시 테이블 새로고침 트리거 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '메뉴가 영구 삭제되었습니다.', + 'action' => 'refresh', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '메뉴가 영구 삭제되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '메뉴 영구 삭제에 실패했습니다: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 메뉴 활성 상태 토글 + */ + public function toggleActive(Request $request, int $id): JsonResponse + { + try { + $result = $this->menuService->toggleActive($id); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 찾을 수 없습니다.', + ], 404); + } + + // HTMX 요청 시 테이블 새로고침 트리거 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '메뉴 활성 상태가 변경되었습니다.', + 'action' => 'refresh', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '메뉴 활성 상태가 변경되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '메뉴 활성 상태 변경에 실패했습니다: ' . $e->getMessage(), + ], 500); + } + } + + /** + * 메뉴 숨김 상태 토글 + */ + public function toggleHidden(Request $request, int $id): JsonResponse + { + try { + $result = $this->menuService->toggleHidden($id); + + if (!$result) { + return response()->json([ + 'success' => false, + 'message' => '메뉴를 찾을 수 없습니다.', + ], 404); + } + + // HTMX 요청 시 테이블 새로고침 트리거 + if ($request->header('HX-Request')) { + return response()->json([ + 'success' => true, + 'message' => '메뉴 숨김 상태가 변경되었습니다.', + 'action' => 'refresh', + ]); + } + + return response()->json([ + 'success' => true, + 'message' => '메뉴 숨김 상태가 변경되었습니다.', + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => '메뉴 숨김 상태 변경에 실패했습니다: ' . $e->getMessage(), + ], 500); + } + } +} diff --git a/app/Http/Controllers/MenuController.php b/app/Http/Controllers/MenuController.php new file mode 100644 index 00000000..45ad884e --- /dev/null +++ b/app/Http/Controllers/MenuController.php @@ -0,0 +1,48 @@ +menuService->getParentMenus(); + + return view('menus.create', compact('parentMenus')); + } + + /** + * 메뉴 수정 페이지 + */ + public function edit(int $id): View + { + $menu = $this->menuService->getMenuById($id); + + if (!$menu) { + abort(404, '메뉴를 찾을 수 없습니다.'); + } + + $parentMenus = $this->menuService->getParentMenus(); + + return view('menus.edit', compact('menu', 'parentMenus')); + } +} diff --git a/app/Http/Requests/StoreMenuRequest.php b/app/Http/Requests/StoreMenuRequest.php new file mode 100644 index 00000000..4c1dbde2 --- /dev/null +++ b/app/Http/Requests/StoreMenuRequest.php @@ -0,0 +1,69 @@ +|string> + */ + public function rules(): array + { + return [ + 'parent_id' => 'nullable|exists:menus,id', + 'name' => 'required|string|max:100', + 'url' => 'nullable|string|max:255', + 'icon' => 'nullable|string|max:100', + 'sort_order' => 'nullable|integer|min:0', + 'is_active' => 'nullable|boolean', + 'hidden' => 'nullable|boolean', + 'is_external' => 'nullable|boolean', + 'external_url' => 'nullable|string|max:255|required_if:is_external,1', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'parent_id' => '부모 메뉴', + 'name' => '메뉴명', + 'url' => 'URL', + 'icon' => '아이콘', + 'sort_order' => '정렬 순서', + 'is_active' => '활성 상태', + 'hidden' => '숨김 여부', + 'is_external' => '외부 링크', + 'external_url' => '외부 URL', + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'parent_id.exists' => '존재하지 않는 부모 메뉴입니다.', + 'external_url.required_if' => '외부 링크 사용 시 외부 URL은 필수입니다.', + ]; + } +} diff --git a/app/Http/Requests/UpdateMenuRequest.php b/app/Http/Requests/UpdateMenuRequest.php new file mode 100644 index 00000000..c5382836 --- /dev/null +++ b/app/Http/Requests/UpdateMenuRequest.php @@ -0,0 +1,80 @@ +|string> + */ + public function rules(): array + { + $menuId = $this->route('id'); + + return [ + 'parent_id' => [ + 'nullable', + 'exists:menus,id', + function ($attribute, $value, $fail) use ($menuId) { + // 자기 자신을 부모로 설정 방지 + if ($value == $menuId) { + $fail('자기 자신을 부모 메뉴로 설정할 수 없습니다.'); + } + }, + ], + 'name' => 'required|string|max:100', + 'url' => 'nullable|string|max:255', + 'icon' => 'nullable|string|max:100', + 'sort_order' => 'nullable|integer|min:0', + 'is_active' => 'nullable|boolean', + 'hidden' => 'nullable|boolean', + 'is_external' => 'nullable|boolean', + 'external_url' => 'nullable|string|max:255|required_if:is_external,1', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'parent_id' => '부모 메뉴', + 'name' => '메뉴명', + 'url' => 'URL', + 'icon' => '아이콘', + 'sort_order' => '정렬 순서', + 'is_active' => '활성 상태', + 'hidden' => '숨김 여부', + 'is_external' => '외부 링크', + 'external_url' => '외부 URL', + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'parent_id.exists' => '존재하지 않는 부모 메뉴입니다.', + 'external_url.required_if' => '외부 링크 사용 시 외부 URL은 필수입니다.', + ]; + } +} diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php new file mode 100644 index 00000000..12f0e2d9 --- /dev/null +++ b/app/Services/MenuService.php @@ -0,0 +1,321 @@ +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; + }); + } + + /** + * 부모 메뉴 목록 조회 (드롭다운용) + */ + public function getParentMenus(?int $tenantId = null): Collection + { + $tenantId = $tenantId ?? session('selected_tenant_id'); + + $query = Menu::query() + ->where('is_active', true) + ->orderBy('sort_order') + ->orderBy('name'); + + if ($tenantId) { + // 특정 테넌트 선택 시: 해당 테넌트의 메뉴만 + $query->where('tenant_id', $tenantId); + } else { + // 전체 선택 시: tenant_id가 NULL인 마스터 메뉴만 + $query->whereNull('tenant_id'); + } + + return $query->get(); + } + + /** + * 메뉴 생성 + */ + 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(); + } + + /** + * 메뉴 영구 삭제 (슈퍼관리자 전용) + */ + public function forceDeleteMenu(int $id): bool + { + $menu = Menu::withTrashed()->findOrFail($id); + + // 자식 메뉴가 있는 경우 영구 삭제 불가 + if ($menu->children()->withTrashed()->count() > 0) { + return false; + } + + return $menu->forceDelete(); + } + + /** + * 메뉴 활성 상태 토글 + */ + 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(); + } +} diff --git a/resources/views/menus/create.blade.php b/resources/views/menus/create.blade.php new file mode 100644 index 00000000..41346453 --- /dev/null +++ b/resources/views/menus/create.blade.php @@ -0,0 +1,171 @@ +@extends('layouts.app') + +@section('title', '메뉴 생성') + +@section('content') +
+ +
+

📋 메뉴 생성

+ + ← 목록으로 + +
+ + +
+ +
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/menus/edit.blade.php b/resources/views/menus/edit.blade.php new file mode 100644 index 00000000..56d0ef2e --- /dev/null +++ b/resources/views/menus/edit.blade.php @@ -0,0 +1,184 @@ +@extends('layouts.app') + +@section('title', '메뉴 수정') + +@section('content') +
+ +
+

📋 메뉴 수정

+ + ← 목록으로 + +
+ + +
+ +
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/menus/index.blade.php b/resources/views/menus/index.blade.php new file mode 100644 index 00000000..e4fceb67 --- /dev/null +++ b/resources/views/menus/index.blade.php @@ -0,0 +1,194 @@ +@extends('layouts.app') + +@section('title', '메뉴 관리') + +@section('content') + + @include('partials.tenant-selector') + + +
+

📋 메뉴 관리

+ + + 새 메뉴 + +
+ + +
+
+ +
+ +
+ + +
+ +
+ + + +
+
+ + + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/menus/partials/table.blade.php b/resources/views/menus/partials/table.blade.php new file mode 100644 index 00000000..1b24e5ad --- /dev/null +++ b/resources/views/menus/partials/table.blade.php @@ -0,0 +1,124 @@ +
+ + + + + + + + + + + + + + @forelse($menus as $menu) + + + + + + + + + + @empty + + + + @endforelse + +
ID메뉴명URL정렬활성숨김작업
+ 메뉴가 없습니다. +
+
+ + +@include('partials.pagination', [ + 'paginator' => $menus, + 'target' => '#menu-table', + 'includeForm' => '#filterForm' +]) diff --git a/routes/api.php b/routes/api.php index 579bc992..4a80d87f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ name('restore'); Route::delete('/{id}/force', [UserController::class, 'forceDestroy'])->name('forceDestroy'); }); + + // 메뉴 관리 API + Route::prefix('menus')->name('menus.')->group(function () { + // 고정 경로는 먼저 정의 + Route::get('/tree', [MenuController::class, 'tree'])->name('tree'); + + // 동적 경로는 나중에 정의 + Route::get('/', [MenuController::class, 'index'])->name('index'); + Route::post('/', [MenuController::class, 'store'])->name('store'); + Route::get('/{id}', [MenuController::class, 'show'])->name('show'); + Route::put('/{id}', [MenuController::class, 'update'])->name('update'); + Route::delete('/{id}', [MenuController::class, 'destroy'])->name('destroy'); + + // 추가 액션 + Route::post('/{id}/restore', [MenuController::class, 'restore'])->name('restore'); + Route::delete('/{id}/force', [MenuController::class, 'forceDestroy'])->name('forceDestroy'); + Route::post('/{id}/toggle-active', [MenuController::class, 'toggleActive'])->name('toggleActive'); + Route::post('/{id}/toggle-hidden', [MenuController::class, 'toggleHidden'])->name('toggleHidden'); + }); }); \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index ed04c7b4..f9267683 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\DepartmentController; +use App\Http\Controllers\MenuController; use App\Http\Controllers\RoleController; use App\Http\Controllers\TenantController; use App\Http\Controllers\UserController; @@ -58,6 +59,13 @@ Route::get('/{id}/edit', [UserController::class, 'edit'])->name('edit'); }); + // 메뉴 관리 (Blade 화면만) + Route::prefix('menus')->name('menus.')->group(function () { + Route::get('/', [MenuController::class, 'index'])->name('index'); + Route::get('/create', [MenuController::class, 'create'])->name('create'); + Route::get('/{id}/edit', [MenuController::class, 'edit'])->name('edit'); + }); + // 대시보드 Route::get('/dashboard', function () { return view('dashboard.index');