feat: Global Menu 계층 이동 기능 추가 및 Role 삭제 오류 수정
Global Menu indent/outdent 기능: - GlobalMenuController에 move() 메서드 추가 - MenuService에 moveGlobalMenu(), isGlobalDescendant(), reorderGlobalSiblings(), compactGlobalSiblings() 추가 - global-index.blade.php에 드래그 계층 이동 JavaScript 추가 - routes/api.php에 POST /move 라우트 추가 Role 삭제 500 에러 수정: - config/auth.php에 api guard 추가 (Spatie Permission getModelForGuard 오류 해결) - RoleService에서 불필요한 users()->detach() 제거 (FK CASCADE 처리) - RoleController에서 HTMX 요청 시 View 직접 반환 (JSON 파싱 에러 해결) - index.blade.php에서 불필요한 afterSwap 핸들러 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -277,6 +277,37 @@ public function toggleHidden(Request $request, int $id): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 글로벌 메뉴 계층 이동 (인덴트/아웃덴트)
|
||||
*/
|
||||
public function move(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'menu_id' => 'required|integer',
|
||||
'new_parent_id' => 'nullable|integer',
|
||||
'sort_order' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = $this->menuService->moveGlobalMenu(
|
||||
$validated['menu_id'],
|
||||
$validated['new_parent_id'],
|
||||
$validated['sort_order']
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
return response()->json($result, 400);
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '글로벌 메뉴 이동에 실패했습니다: '.$e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 글로벌 메뉴 순서 변경
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ public function __construct(
|
||||
/**
|
||||
* 역할 목록 조회
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse|\Illuminate\Contracts\View\View
|
||||
{
|
||||
$roles = $this->roleService->getRoles(
|
||||
$request->all(),
|
||||
@@ -27,11 +27,7 @@ public function index(Request $request): JsonResponse
|
||||
|
||||
// HTMX 요청 시 HTML 반환
|
||||
if ($request->header('HX-Request')) {
|
||||
$html = view('roles.partials.table', compact('roles'))->render();
|
||||
|
||||
return response()->json([
|
||||
'html' => $html,
|
||||
]);
|
||||
return view('roles.partials.table', compact('roles'));
|
||||
}
|
||||
|
||||
// 일반 요청 시 JSON 반환
|
||||
|
||||
@@ -836,6 +836,105 @@ public function reorderGlobalMenus(array $items): bool
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 글로벌 메뉴 이동 (계층 구조 변경 - 인덴트/아웃덴트)
|
||||
* - 다른 부모 아래로 이동 가능
|
||||
* - 하위 메뉴는 자동으로 따라감
|
||||
* - 순환 참조 방지
|
||||
*/
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 글로벌 메뉴 목록 조회 (가져오기 상태 포함)
|
||||
* - 모든 글로벌 메뉴 반환
|
||||
|
||||
@@ -183,8 +183,7 @@ public function deleteRole(int $id): bool
|
||||
// 권한 연결 해제
|
||||
$role->permissions()->detach();
|
||||
|
||||
// 사용자 연결 해제
|
||||
$role->users()->detach();
|
||||
// 사용자 연결은 FK CASCADE가 자동 처리 (model_has_roles.role_id → ON DELETE CASCADE)
|
||||
|
||||
return $role->delete();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'driver' => 'sanctum',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -83,6 +83,40 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
background: linear-gradient(135deg, #9333ea, #7c3aed);
|
||||
color: white;
|
||||
}
|
||||
.drag-indicator.indent {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
}
|
||||
.drag-indicator.outdent {
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 드래그 중 행 하이라이트 */
|
||||
.menu-row.drag-target-indent {
|
||||
background: rgba(59, 130, 246, 0.25) !important;
|
||||
border: 2px solid #3b82f6 !important;
|
||||
border-left-width: 6px !important;
|
||||
position: relative;
|
||||
animation: pulse-indent 0.8s ease-in-out infinite;
|
||||
}
|
||||
.menu-row.drag-target-outdent {
|
||||
background: rgba(249, 115, 22, 0.25) !important;
|
||||
border: 2px solid #f97316 !important;
|
||||
border-right-width: 6px !important;
|
||||
position: relative;
|
||||
animation: pulse-outdent 0.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 펄스 애니메이션 */
|
||||
@keyframes pulse-indent {
|
||||
0%, 100% { background: rgba(59, 130, 246, 0.15); }
|
||||
50% { background: rgba(59, 130, 246, 0.35); }
|
||||
}
|
||||
@keyframes pulse-outdent {
|
||||
0%, 100% { background: rgba(249, 115, 22, 0.15); }
|
||||
50% { background: rgba(249, 115, 22, 0.35); }
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
opacity: 0.9;
|
||||
@@ -92,10 +126,27 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
z-index: 9998 !important;
|
||||
}
|
||||
|
||||
/* 드래그 중 텍스트 선택 방지 */
|
||||
.sortable-drag, .sortable-chosen, .sortable-ghost {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* 드래그 중 전체 페이지 텍스트 선택 방지 */
|
||||
body.is-dragging {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
body.is-dragging * {
|
||||
user-select: none !important;
|
||||
-webkit-user-select: none !important;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@@ -110,14 +161,80 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
});
|
||||
|
||||
// HTMX 응답 처리 + SortableJS 초기화
|
||||
// 서버가 HTML을 직접 반환하므로 HTMX가 자동으로 swap 처리
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'menu-table') {
|
||||
// 테이블 로드 후 SortableJS 초기화
|
||||
initGlobalMenuSortable();
|
||||
}
|
||||
});
|
||||
|
||||
// 드래그 상태 관리
|
||||
let dragStartX = 0;
|
||||
let dragIndicator = null;
|
||||
let currentDragItem = null;
|
||||
let lastHighlightedRow = null;
|
||||
const INDENT_THRESHOLD = 40; // px - 인덴트 임계값
|
||||
|
||||
// 드래그 인디케이터 생성
|
||||
function createDragIndicator() {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'drag-indicator reorder';
|
||||
indicator.innerHTML = '↕️ 순서 변경';
|
||||
indicator.style.display = 'none';
|
||||
document.body.appendChild(indicator);
|
||||
return indicator;
|
||||
}
|
||||
|
||||
// 드래그 인디케이터 업데이트
|
||||
function updateDragIndicator(deltaX, mouseX, mouseY, canIndent, canOutdent, targetName) {
|
||||
if (!dragIndicator) return;
|
||||
|
||||
dragIndicator.style.display = 'block';
|
||||
dragIndicator.style.left = (mouseX + 15) + 'px';
|
||||
dragIndicator.style.top = (mouseY - 15) + 'px';
|
||||
|
||||
dragIndicator.classList.remove('indent', 'outdent', 'reorder');
|
||||
|
||||
if (deltaX > INDENT_THRESHOLD && canIndent) {
|
||||
dragIndicator.classList.add('indent');
|
||||
dragIndicator.innerHTML = `→ <b>${targetName}</b>의 하위로`;
|
||||
} else if (deltaX < -INDENT_THRESHOLD && canOutdent) {
|
||||
dragIndicator.classList.add('outdent');
|
||||
dragIndicator.innerHTML = '← 상위 레벨로';
|
||||
} else {
|
||||
dragIndicator.classList.add('reorder');
|
||||
dragIndicator.innerHTML = '↕️ 순서 변경';
|
||||
}
|
||||
}
|
||||
|
||||
// 행 하이라이트 업데이트
|
||||
function updateRowHighlight(prevRow, draggedRow, deltaX, canIndent, canOutdent) {
|
||||
if (lastHighlightedRow) {
|
||||
lastHighlightedRow.classList.remove('drag-target-indent', 'drag-target-outdent');
|
||||
}
|
||||
|
||||
if (deltaX > INDENT_THRESHOLD && canIndent && prevRow) {
|
||||
prevRow.classList.add('drag-target-indent');
|
||||
lastHighlightedRow = prevRow;
|
||||
} else if (deltaX < -INDENT_THRESHOLD && canOutdent && draggedRow) {
|
||||
draggedRow.classList.add('drag-target-outdent');
|
||||
lastHighlightedRow = draggedRow;
|
||||
} else {
|
||||
lastHighlightedRow = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 드래그 인디케이터 제거
|
||||
function removeDragIndicator() {
|
||||
if (dragIndicator) {
|
||||
dragIndicator.remove();
|
||||
dragIndicator = null;
|
||||
}
|
||||
if (lastHighlightedRow) {
|
||||
lastHighlightedRow.classList.remove('drag-target-indent', 'drag-target-outdent');
|
||||
lastHighlightedRow = null;
|
||||
}
|
||||
}
|
||||
|
||||
// SortableJS 초기화 함수
|
||||
function initGlobalMenuSortable() {
|
||||
const tbody = document.getElementById('menu-sortable');
|
||||
@@ -127,6 +244,42 @@ function initGlobalMenuSortable() {
|
||||
tbody.sortableInstance.destroy();
|
||||
}
|
||||
|
||||
// 드래그 중 마우스 이동 핸들러
|
||||
function onDragMove(e) {
|
||||
if (!currentDragItem) return;
|
||||
|
||||
const mouseX = e.clientX || e.touches?.[0]?.clientX || 0;
|
||||
const mouseY = e.clientY || e.touches?.[0]?.clientY || 0;
|
||||
const deltaX = mouseX - dragStartX;
|
||||
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.menu-row'));
|
||||
const draggedIndex = rows.indexOf(currentDragItem);
|
||||
|
||||
// 위 행 찾기
|
||||
let prevRow = null;
|
||||
for (let i = draggedIndex - 1; i >= 0; i--) {
|
||||
if (rows[i] !== currentDragItem && !rows[i].classList.contains('sortable-ghost')) {
|
||||
prevRow = rows[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const oldParentIdRaw = currentDragItem.dataset.parentId;
|
||||
const oldPid = oldParentIdRaw === '' ? null : (oldParentIdRaw ? parseInt(oldParentIdRaw) : null);
|
||||
const canIndent = prevRow !== null;
|
||||
const canOutdent = oldPid !== null;
|
||||
|
||||
let targetName = '';
|
||||
if (prevRow && canIndent) {
|
||||
const nameCell = prevRow.querySelector('td:nth-child(3) span');
|
||||
targetName = nameCell?.textContent?.trim() || '위 항목';
|
||||
}
|
||||
|
||||
updateDragIndicator(deltaX, mouseX, mouseY, canIndent, canOutdent, targetName);
|
||||
updateRowHighlight(prevRow, currentDragItem, deltaX, canIndent, canOutdent);
|
||||
}
|
||||
|
||||
// SortableJS 초기화 - 노션 스타일 인덴트
|
||||
tbody.sortableInstance = new Sortable(tbody, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
@@ -135,20 +288,135 @@ function initGlobalMenuSortable() {
|
||||
fallbackOnBody: true,
|
||||
ghostClass: 'bg-purple-50',
|
||||
chosenClass: 'bg-purple-100',
|
||||
dragClass: 'shadow-lg',
|
||||
|
||||
onStart: function(evt) {
|
||||
dragStartX = evt.originalEvent?.clientX || evt.originalEvent?.touches?.[0]?.clientX || 0;
|
||||
currentDragItem = evt.item;
|
||||
dragIndicator = createDragIndicator();
|
||||
document.body.classList.add('is-dragging');
|
||||
document.addEventListener('mousemove', onDragMove);
|
||||
document.addEventListener('touchmove', onDragMove);
|
||||
},
|
||||
|
||||
onEnd: function(evt) {
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.menu-row'));
|
||||
const items = rows.map((row, index) => ({
|
||||
id: parseInt(row.dataset.menuId),
|
||||
sort_order: index + 1
|
||||
}));
|
||||
document.removeEventListener('mousemove', onDragMove);
|
||||
document.removeEventListener('touchmove', onDragMove);
|
||||
removeDragIndicator();
|
||||
currentDragItem = null;
|
||||
document.body.classList.remove('is-dragging');
|
||||
|
||||
saveGlobalMenuOrder(items);
|
||||
const movedItem = evt.item;
|
||||
const menuId = parseInt(movedItem.dataset.menuId);
|
||||
const oldParentIdRaw = movedItem.dataset.parentId;
|
||||
const oldPid = oldParentIdRaw === '' ? null : (oldParentIdRaw ? parseInt(oldParentIdRaw) : null);
|
||||
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.menu-row'));
|
||||
const newIndex = rows.indexOf(movedItem);
|
||||
|
||||
const dragEndX = evt.originalEvent?.clientX || evt.originalEvent?.changedTouches?.[0]?.clientX || dragStartX;
|
||||
const deltaX = dragEndX - dragStartX;
|
||||
|
||||
let newParentId = oldPid;
|
||||
let sortOrder = 1;
|
||||
let action = 'reorder';
|
||||
|
||||
// 오른쪽으로 이동 → 인덴트 (위 항목의 자식으로)
|
||||
if (deltaX > INDENT_THRESHOLD && newIndex > 0) {
|
||||
const prevRow = rows[newIndex - 1];
|
||||
if (prevRow) {
|
||||
const prevId = parseInt(prevRow.dataset.menuId);
|
||||
if (prevId !== menuId) {
|
||||
newParentId = prevId;
|
||||
action = 'indent';
|
||||
}
|
||||
}
|
||||
}
|
||||
// 왼쪽으로 이동 → 아웃덴트 (부모의 형제로)
|
||||
else if (deltaX < -INDENT_THRESHOLD && oldPid !== null) {
|
||||
const parentRow = rows.find(r => parseInt(r.dataset.menuId) === oldPid);
|
||||
if (parentRow) {
|
||||
const grandParentIdRaw = parentRow.dataset.parentId;
|
||||
newParentId = grandParentIdRaw === '' ? null : (grandParentIdRaw ? parseInt(grandParentIdRaw) : null);
|
||||
} else {
|
||||
newParentId = null;
|
||||
}
|
||||
action = 'outdent';
|
||||
}
|
||||
|
||||
const parentChanged = oldPid !== newParentId;
|
||||
|
||||
if (parentChanged) {
|
||||
// 새 부모 하위에서의 순서 계산
|
||||
const existingSiblings = rows.filter(row => {
|
||||
if (row === movedItem) return false;
|
||||
const rowParentId = row.dataset.parentId || '';
|
||||
const rowPid = rowParentId === '' ? null : parseInt(rowParentId);
|
||||
return rowPid === newParentId;
|
||||
});
|
||||
|
||||
let siblingsBeforeMe = 0;
|
||||
for (const sibling of existingSiblings) {
|
||||
if (rows.indexOf(sibling) < newIndex) {
|
||||
siblingsBeforeMe++;
|
||||
}
|
||||
}
|
||||
sortOrder = siblingsBeforeMe + 1;
|
||||
|
||||
moveGlobalMenu(menuId, newParentId, sortOrder);
|
||||
} else {
|
||||
// 같은 레벨 내 순서 변경
|
||||
const siblingRows = rows.filter(row => {
|
||||
const rowParentId = row.dataset.parentId || '';
|
||||
const rowPid = rowParentId === '' ? null : parseInt(rowParentId);
|
||||
return rowPid === oldPid;
|
||||
});
|
||||
|
||||
const items = siblingRows.map((row, index) => ({
|
||||
id: parseInt(row.dataset.menuId),
|
||||
sort_order: index + 1
|
||||
}));
|
||||
|
||||
saveGlobalMenuOrder(items);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 메뉴 순서 저장 API 호출
|
||||
// 글로벌 메뉴 이동 API 호출 (계층 변경)
|
||||
function moveGlobalMenu(menuId, newParentId, sortOrder) {
|
||||
const payload = {
|
||||
menu_id: menuId,
|
||||
new_parent_id: newParentId,
|
||||
sort_order: sortOrder
|
||||
};
|
||||
|
||||
fetch('/api/admin/global-menus/move', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
htmx.trigger('#menu-table', 'filterSubmit');
|
||||
} else {
|
||||
showToast('메뉴 이동 실패: ' + (data.message || ''), 'error');
|
||||
htmx.trigger('#menu-table', 'filterSubmit');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showToast('메뉴 이동 중 오류 발생', 'error');
|
||||
htmx.trigger('#menu-table', 'filterSubmit');
|
||||
});
|
||||
}
|
||||
|
||||
// 글로벌 메뉴 순서 저장 API 호출
|
||||
function saveGlobalMenuOrder(items) {
|
||||
fetch('/api/admin/global-menus/reorder', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -63,16 +63,6 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
htmx.trigger('#role-table', 'filterSubmit');
|
||||
});
|
||||
|
||||
// HTMX 응답 처리
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'role-table') {
|
||||
const response = JSON.parse(event.detail.xhr.response);
|
||||
if (response.html) {
|
||||
event.detail.target.innerHTML = response.html;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 삭제 확인
|
||||
window.confirmDelete = function(id, name) {
|
||||
showDeleteConfirm(name, () => {
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
Route::middleware('super.admin')->prefix('global-menus')->name('global-menus.')->group(function () {
|
||||
// 고정 경로
|
||||
Route::post('/reorder', [GlobalMenuController::class, 'reorder'])->name('reorder');
|
||||
Route::post('/move', [GlobalMenuController::class, 'move'])->name('move');
|
||||
|
||||
// 기본 CRUD
|
||||
Route::get('/', [GlobalMenuController::class, 'index'])->name('index');
|
||||
|
||||
Reference in New Issue
Block a user