feat: [menus] 최상위 그룹 상단/하단 이동 버튼 추가
- depth=0 메뉴에만 이동 버튼(↕) 표시 - 클릭 시 드롭다운으로 상단/하단 이동 선택 - 기존 reorder API 재사용하여 sort_order 일괄 변경
This commit is contained in:
@@ -415,6 +415,97 @@ window.moveMenuGroup = function(groupMenuIds, leadId, newParentId, sortOrder, cs
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 최상위 그룹 상단/하단 이동 =====
|
||||
|
||||
// 이동 드롭다운 표시
|
||||
window.showMoveGroupDropdown = function(menuId, buttonEl) {
|
||||
// 기존 드롭다운 제거
|
||||
const existing = document.getElementById('move-group-dropdown');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
// 같은 버튼 클릭 시 토글 (닫기)
|
||||
if (existing.dataset.menuId === String(menuId)) return;
|
||||
}
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.id = 'move-group-dropdown';
|
||||
dropdown.dataset.menuId = menuId;
|
||||
dropdown.className = 'move-group-dropdown';
|
||||
|
||||
dropdown.innerHTML = `
|
||||
<button type="button" onclick="moveGroupToPosition(${menuId}, 'top')" class="move-group-option">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
상단으로 이동
|
||||
</button>
|
||||
<button type="button" onclick="moveGroupToPosition(${menuId}, 'bottom')" class="move-group-option">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
하단으로 이동
|
||||
</button>
|
||||
`;
|
||||
|
||||
// 버튼 위치 기준으로 드롭다운 배치
|
||||
const rect = buttonEl.getBoundingClientRect();
|
||||
dropdown.style.position = 'fixed';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.top = (rect.bottom + 4) + 'px';
|
||||
dropdown.style.zIndex = '9999';
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
|
||||
// 외부 클릭 시 닫기
|
||||
function closeDropdown(e) {
|
||||
if (!dropdown.contains(e.target) && e.target !== buttonEl && !buttonEl.contains(e.target)) {
|
||||
dropdown.remove();
|
||||
document.removeEventListener('click', closeDropdown, true);
|
||||
}
|
||||
}
|
||||
// 다음 이벤트 루프에서 리스너 등록 (현재 클릭 무시)
|
||||
setTimeout(() => document.addEventListener('click', closeDropdown, true), 0);
|
||||
};
|
||||
|
||||
// 최상위 그룹을 상단/하단으로 이동
|
||||
window.moveGroupToPosition = function(menuId, position) {
|
||||
// 드롭다운 닫기
|
||||
const dropdown = document.getElementById('move-group-dropdown');
|
||||
if (dropdown) dropdown.remove();
|
||||
|
||||
// DOM에서 최상위 그룹(depth=0, parent_id="") 수집
|
||||
const allRows = document.querySelectorAll('#menu-sortable tr.menu-row[data-depth="0"][data-parent-id=""]');
|
||||
if (allRows.length === 0) return;
|
||||
|
||||
const groupIds = Array.from(allRows).map(row => parseInt(row.dataset.menuId));
|
||||
const targetIndex = groupIds.indexOf(menuId);
|
||||
if (targetIndex === -1) return;
|
||||
|
||||
// 이미 해당 위치에 있으면 무시
|
||||
if (position === 'top' && targetIndex === 0) return;
|
||||
if (position === 'bottom' && targetIndex === groupIds.length - 1) return;
|
||||
|
||||
// 대상을 배열에서 제거 후 처음/끝에 삽입
|
||||
groupIds.splice(targetIndex, 1);
|
||||
if (position === 'top') {
|
||||
groupIds.unshift(menuId);
|
||||
} else {
|
||||
groupIds.push(menuId);
|
||||
}
|
||||
|
||||
// sort_order 계산 (1부터 순차)
|
||||
const items = groupIds.map((id, index) => ({
|
||||
id: id,
|
||||
sort_order: index + 1
|
||||
}));
|
||||
|
||||
// CSRF 토큰
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
|
||||
// 기존 saveMenuOrder API 호출
|
||||
window.saveMenuOrder(items, csrfToken);
|
||||
};
|
||||
|
||||
// 메뉴 순서 저장 API 호출 (같은 레벨)
|
||||
window.saveMenuOrder = function(items, csrfToken) {
|
||||
fetch('/api/admin/menus/reorder', {
|
||||
|
||||
@@ -314,6 +314,35 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* 최상위 그룹 이동 드롭다운 */
|
||||
.move-group-dropdown {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
padding: 4px;
|
||||
min-width: 150px;
|
||||
}
|
||||
.move-group-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.move-group-option:hover {
|
||||
background-color: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
|
||||
@@ -65,14 +65,26 @@ class="import-checkbox w-4 h-4 rounded border-gray-300 text-green-600 focus:ring
|
||||
onchange="toggleMenuChildren(this); updateBulkButtonState()"
|
||||
class="menu-checkbox w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
</td>
|
||||
{{-- 드래그 핸들 --}}
|
||||
{{-- 드래그 핸들 + 이동 버튼 --}}
|
||||
<td class="px-2 py-2 whitespace-nowrap text-center">
|
||||
@if(!$menu->deleted_at)
|
||||
<span class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-4 h-4 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="inline-flex items-center gap-0.5">
|
||||
<span class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-4 h-4 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 16h16" />
|
||||
</svg>
|
||||
</span>
|
||||
@if(($menu->depth ?? 0) === 0)
|
||||
<button type="button"
|
||||
onclick="showMoveGroupDropdown({{ $menu->id }}, this)"
|
||||
class="move-group-btn text-gray-400 hover:text-blue-600 transition p-0.5 rounded hover:bg-blue-50"
|
||||
title="상단/하단으로 이동">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user