refactor: 메뉴 트리 스크립트 공통화 및 디자인 통일

- public/js/menu-tree.js 공통 스크립트 생성
  - 테이블(tr.menu-row) / div(.menu-item) 둘 다 지원
  - toggleChildren, hideChildren, showChildren 함수 통합

- 권한 관리 페이지들 메뉴 트리 디자인 통일
  - role-permissions, department-permissions, user-permissions
  - 폴더/파일 아이콘, 접기/펼치기 버튼, chevron 아이콘
  - menu-row 클래스 및 data 속성 추가

- permission-analyze 접기/펼치기 기능 추가
  - data-parent-id, data-depth 속성 추가
  - 폴더 버튼 클릭으로 하위 메뉴 토글

- menus 페이지 스크립트 공통화

- 각 페이지 중복 코드 제거 및 공통 menu-tree.js 로드
This commit is contained in:
2025-11-27 10:25:02 +09:00
parent 82c072d72e
commit 604aa256f6
11 changed files with 196 additions and 70 deletions

63
public/js/menu-tree.js Normal file
View File

@@ -0,0 +1,63 @@
/**
* 메뉴 트리 공통 스크립트
* - 부서 권한 관리 (department-permissions) - 테이블 기반
* - 개인 권한 관리 (user-permissions) - 테이블 기반
* - 권한 분석 (permission-analyze) - div 기반
*/
// 자식 메뉴 접기/펼치기
window.toggleChildren = function(menuId) {
const button = document.querySelector(`.toggle-btn[data-menu-id="${menuId}"]`);
if (!button) return;
const chevron = button.querySelector('.chevron-icon');
if (!chevron) return;
const isCollapsed = chevron.classList.contains('rotate-[-90deg]');
if (isCollapsed) {
// 펼치기
chevron.classList.remove('rotate-[-90deg]');
showChildren(menuId);
} else {
// 접기
chevron.classList.add('rotate-[-90deg]');
hideChildren(menuId);
}
};
// 자식 요소 선택자 (테이블: tr.menu-row, div: .menu-item)
function getChildElements(parentId) {
// 테이블 기반 (tr.menu-row)
let children = document.querySelectorAll(`tr.menu-row[data-parent-id="${parentId}"]`);
if (children.length > 0) return children;
// div 기반 (.menu-item)
return document.querySelectorAll(`.menu-item[data-parent-id="${parentId}"]`);
}
// 재귀적으로 자식 메뉴 숨기기
function hideChildren(parentId) {
const children = getChildElements(parentId);
children.forEach(child => {
child.style.display = 'none';
const childId = child.getAttribute('data-menu-id');
hideChildren(childId);
});
}
// 재귀적으로 직계 자식만 표시
function showChildren(parentId) {
const children = getChildElements(parentId);
children.forEach(child => {
child.style.display = '';
const childId = child.getAttribute('data-menu-id');
const childButton = child.querySelector(`.toggle-btn[data-menu-id="${childId}"]`);
if (childButton) {
const chevron = childButton.querySelector('.chevron-icon');
if (chevron && !chevron.classList.contains('rotate-[-90deg]')) {
showChildren(childId);
}
}
});
}

View File

@@ -148,4 +148,5 @@ function reloadPermissions() {
}
});
</script>
<script src="{{ asset('js/menu-tree.js') }}"></script>
@endsection

View File

@@ -20,16 +20,43 @@
$permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'];
@endphp
@forelse($menus as $index => $menu)
<tr>
<tr class="menu-row"
data-menu-id="{{ $menu->id }}"
data-parent-id="{{ $menu->parent_id ?? '' }}"
data-depth="{{ $menu->depth ?? 0 }}">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
{{ $index + 1 }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center text-sm text-gray-900" style="padding-left: {{ ($menu->depth ?? 0) * 20 }}px;">
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
{{-- 트리 구조 표시 --}}
@if(($menu->depth ?? 0) > 0)
<span class="mr-2 text-gray-400"></span>
<span class="text-gray-300 text-xs font-mono flex-shrink-0"></span>
@endif
<span>{{ $menu->name }}</span>
{{-- 폴더/아이템 아이콘 (폴더는 클릭으로 접기/펼치기) --}}
@if($menu->has_children)
<button type="button"
onclick="toggleChildren({{ $menu->id }})"
class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
data-menu-id="{{ $menu->id }}">
<svg class="w-4 h-4 transform transition-transform flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@else
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@endif
{{-- 메뉴명 --}}
<span class="text-sm {{ ($menu->depth ?? 0) === 0 ? 'font-semibold text-gray-900' : 'font-medium text-gray-700' }}">
{{ $menu->name }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">

View File

@@ -142,50 +142,6 @@ class="bg-white rounded-lg shadow-sm overflow-hidden">
});
};
// 자식 메뉴 접기/펼치기
window.toggleChildren = function(menuId) {
const button = document.querySelector(`.toggle-btn[data-menu-id="${menuId}"]`);
const svg = button.querySelector('svg');
const isCollapsed = svg.classList.contains('rotate-[-90deg]');
if (isCollapsed) {
// 펼치기
svg.classList.remove('rotate-[-90deg]');
showChildren(menuId);
} else {
// 접기
svg.classList.add('rotate-[-90deg]');
hideChildren(menuId);
}
};
// 재귀적으로 자식 메뉴 숨기기
function hideChildren(parentId) {
const children = document.querySelectorAll(`tr.menu-row[data-parent-id="${parentId}"]`);
children.forEach(child => {
child.style.display = 'none';
const childId = child.getAttribute('data-menu-id');
// 하위의 하위도 숨기기
hideChildren(childId);
});
}
// 재귀적으로 직계 자식만 표시
function showChildren(parentId) {
const children = document.querySelectorAll(`tr.menu-row[data-parent-id="${parentId}"]`);
children.forEach(child => {
child.style.display = '';
// 하위 메뉴는 해당 메뉴가 펼쳐져 있을 때만 표시
const childId = child.getAttribute('data-menu-id');
const childButton = child.querySelector(`.toggle-btn[data-menu-id="${childId}"]`);
if (childButton) {
const childSvg = childButton.querySelector('svg');
// 자식이 접혀있으면 그 하위는 보여주지 않음
if (!childSvg.classList.contains('rotate-[-90deg]')) {
showChildren(childId);
}
}
});
}
</script>
<script src="{{ asset('js/menu-tree.js') }}"></script>
@endpush

View File

@@ -21,24 +21,36 @@
{{ $menu->id }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center" style="padding-left: {{ ($menu->depth ?? 0) * 20 }}px;">
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
{{-- 트리 구조 표시 --}}
@if(($menu->depth ?? 0) > 0)
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
@endif
{{-- 폴더/아이템 아이콘 (폴더는 클릭으로 접기/펼치기) --}}
@if($menu->has_children)
<button type="button"
onclick="toggleChildren({{ $menu->id }})"
class="mr-1 text-gray-500 hover:text-gray-700 focus:outline-none toggle-btn"
class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
data-menu-id="{{ $menu->id }}">
<svg class="w-4 h-4 transform transition-transform" fill="currentColor" viewBox="0 0 20 20">
<svg class="w-4 h-4 transform transition-transform flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@else
<span class="w-4 mr-1"></span>
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@endif
@if(($menu->depth ?? 0) > 0)
<span class="mr-2 text-gray-400"></span>
@endif
<div>
<div class="text-sm font-medium text-gray-900">{{ $menu->name }}</div>
{{-- 메뉴 정보 --}}
<div class="flex items-center gap-2">
<span class="text-sm {{ ($menu->depth ?? 0) === 0 ? 'font-semibold text-gray-900' : 'font-medium text-gray-700' }}">
{{ $menu->name }}
</span>
@if($menu->is_external)
<span class="text-xs text-blue-600">(외부)</span>
@endif

View File

@@ -295,4 +295,5 @@ function exportCsv() {
}
});
</script>
<script src="{{ asset('js/menu-tree.js') }}"></script>
@endpush

View File

@@ -2,6 +2,8 @@
<div
class="menu-item flex items-center gap-2 py-2 rounded-lg border border-transparent cursor-pointer transition-colors hover:bg-gray-50 {{ $menu->depth > 0 ? 'ml-4 border-l-2 border-gray-200' : '' }}"
data-menu-id="{{ $menu->id }}"
data-parent-id="{{ $menu->parent_id ?? '' }}"
data-depth="{{ $menu->depth ?? 0 }}"
data-menu-name="{{ $menu->name }}"
onclick="selectMenu({{ $menu->id }}, '{{ addslashes($menu->name) }}')"
style="padding-left: {{ ($menu->depth * 1.5) + 0.75 }}rem;"
@@ -11,11 +13,19 @@ class="menu-item flex items-center gap-2 py-2 rounded-lg border border-transpare
<span class="text-gray-300 text-xs font-mono flex-shrink-0">└─</span>
@endif
{{-- 폴더/아이템 아이콘 --}}
{{-- 폴더/아이템 아이콘 (폴더는 클릭으로 접기/펼치기) --}}
@if($menu->has_children)
<svg class="w-4 h-4 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<button type="button"
onclick="event.stopPropagation(); toggleChildren({{ $menu->id }})"
class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
data-menu-id="{{ $menu->id }}">
<svg class="w-4 h-4 transform transition-transform flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@else
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />

View File

@@ -148,4 +148,5 @@ function reloadPermissions() {
}
});
</script>
<script src="{{ asset('js/menu-tree.js') }}"></script>
@endsection

View File

@@ -20,16 +20,43 @@
$permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'];
@endphp
@forelse($menus as $index => $menu)
<tr>
<tr class="menu-row"
data-menu-id="{{ $menu->id }}"
data-parent-id="{{ $menu->parent_id ?? '' }}"
data-depth="{{ $menu->depth ?? 0 }}">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
{{ $index + 1 }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center text-sm text-gray-900" style="padding-left: {{ ($menu->depth ?? 0) * 20 }}px;">
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
{{-- 트리 구조 표시 --}}
@if(($menu->depth ?? 0) > 0)
<span class="mr-2 text-gray-400"></span>
<span class="text-gray-300 text-xs font-mono flex-shrink-0"></span>
@endif
<span>{{ $menu->name }}</span>
{{-- 폴더/아이템 아이콘 (폴더는 클릭으로 접기/펼치기) --}}
@if($menu->has_children)
<button type="button"
onclick="toggleChildren({{ $menu->id }})"
class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
data-menu-id="{{ $menu->id }}">
<svg class="w-4 h-4 transform transition-transform flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@else
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@endif
{{-- 메뉴명 --}}
<span class="text-sm {{ ($menu->depth ?? 0) === 0 ? 'font-semibold text-gray-900' : 'font-medium text-gray-700' }}">
{{ $menu->name }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">

View File

@@ -169,4 +169,5 @@ function reloadPermissions() {
}
});
</script>
<script src="{{ asset('js/menu-tree.js') }}"></script>
@endsection

View File

@@ -20,16 +20,43 @@
$permissionTypes = ['view', 'create', 'update', 'delete', 'approve', 'export', 'manage'];
@endphp
@forelse($menus as $index => $menu)
<tr>
<tr class="menu-row"
data-menu-id="{{ $menu->id }}"
data-parent-id="{{ $menu->parent_id ?? '' }}"
data-depth="{{ $menu->depth ?? 0 }}">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 text-center">
{{ $index + 1 }}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center text-sm text-gray-900" style="padding-left: {{ ($menu->depth ?? 0) * 20 }}px;">
<div class="flex items-center gap-2" style="padding-left: {{ (($menu->depth ?? 0) * 1.5) }}rem;">
{{-- 트리 구조 표시 --}}
@if(($menu->depth ?? 0) > 0)
<span class="mr-2 text-gray-400"></span>
<span class="text-gray-300 text-xs font-mono flex-shrink-0"></span>
@endif
<span>{{ $menu->name }}</span>
{{-- 폴더/아이템 아이콘 (폴더는 클릭으로 접기/펼치기) --}}
@if($menu->has_children)
<button type="button"
onclick="toggleChildren({{ $menu->id }})"
class="toggle-btn flex items-center text-blue-500 hover:text-blue-700 focus:outline-none"
data-menu-id="{{ $menu->id }}">
<svg class="w-4 h-4 transform transition-transform flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<svg class="w-3 h-3 ml-0.5 transform transition-transform chevron-icon" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@else
<svg class="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
@endif
{{-- 메뉴명 --}}
<span class="text-sm {{ ($menu->depth ?? 0) === 0 ? 'font-semibold text-gray-900' : 'font-medium text-gray-700' }}">
{{ $menu->name }}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">