162 lines
5.2 KiB
Vue
162 lines
5.2 KiB
Vue
<template>
|
|
<div>
|
|
<AppHeader />
|
|
|
|
<div class="container-fluid py-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h4 class="mb-0">
|
|
<i class="bi bi-list me-2"></i>메뉴 관리
|
|
</h4>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 align-middle">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width: 50px">No</th>
|
|
<th style="width: 150px">메뉴코드</th>
|
|
<th>메뉴명</th>
|
|
<th style="width: 200px">경로</th>
|
|
<th v-for="role in roles" :key="role.roleId" style="width: 100px" class="text-center">
|
|
{{ role.roleName }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-if="isLoading">
|
|
<td :colspan="5 + roles.length" class="text-center py-4">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>로딩 중...
|
|
</td>
|
|
</tr>
|
|
<template v-else>
|
|
<template v-for="(menu, idx) in menus" :key="menu.menuId">
|
|
<tr :class="{ 'table-secondary': !menu.parentMenuId }">
|
|
<td class="text-center">{{ idx + 1 }}</td>
|
|
<td>
|
|
<code class="small">{{ menu.menuCode }}</code>
|
|
</td>
|
|
<td>
|
|
<span v-if="menu.parentMenuId" class="text-muted me-2">└</span>
|
|
<i :class="['bi', menu.menuIcon, 'me-1']" v-if="menu.menuIcon"></i>
|
|
<strong v-if="!menu.parentMenuId">{{ menu.menuName }}</strong>
|
|
<span v-else>{{ menu.menuName }}</span>
|
|
</td>
|
|
<td>
|
|
<code class="small text-muted">{{ menu.menuPath || '-' }}</code>
|
|
</td>
|
|
<td v-for="role in roles" :key="role.roleId" class="text-center">
|
|
<input
|
|
type="checkbox"
|
|
class="form-check-input"
|
|
:checked="menu.roleIds.includes(role.roleId)"
|
|
@change="toggleRole(menu.menuId, role.roleId, $event)"
|
|
/>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 토스트 메시지 -->
|
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1100">
|
|
<div v-if="toast.show" class="toast show" role="alert">
|
|
<div class="toast-header" :class="toast.type === 'success' ? 'bg-success text-white' : 'bg-danger text-white'">
|
|
<strong class="me-auto">{{ toast.type === 'success' ? '성공' : '오류' }}</strong>
|
|
<button type="button" class="btn-close btn-close-white" @click="toast.show = false"></button>
|
|
</div>
|
|
<div class="toast-body">{{ toast.message }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const { fetchCurrentUser, hasMenuAccess } = useAuth()
|
|
const router = useRouter()
|
|
|
|
const isLoading = ref(true)
|
|
const menus = ref<any[]>([])
|
|
const roles = ref<any[]>([])
|
|
|
|
const toast = ref({
|
|
show: false,
|
|
type: 'success' as 'success' | 'error',
|
|
message: ''
|
|
})
|
|
|
|
onMounted(async () => {
|
|
const user = await fetchCurrentUser()
|
|
if (!user) {
|
|
router.push('/login')
|
|
return
|
|
}
|
|
|
|
if (!hasMenuAccess('ADMIN_MENU')) {
|
|
alert('접근 권한이 없습니다.')
|
|
router.push('/')
|
|
return
|
|
}
|
|
|
|
await loadMenus()
|
|
})
|
|
|
|
async function loadMenus() {
|
|
isLoading.value = true
|
|
try {
|
|
const res = await $fetch<any>('/api/admin/menu/list')
|
|
menus.value = res.menus
|
|
roles.value = res.roles
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
showToast('error', '메뉴 목록을 불러오는데 실패했습니다.')
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function toggleRole(menuId: number, roleId: number, event: Event) {
|
|
const checkbox = event.target as HTMLInputElement
|
|
const enabled = checkbox.checked
|
|
|
|
try {
|
|
await $fetch(`/api/admin/menu/${menuId}/toggle-role`, {
|
|
method: 'POST',
|
|
body: { roleId, enabled }
|
|
})
|
|
|
|
// 로컬 상태 업데이트
|
|
const menu = menus.value.find(m => m.menuId === menuId)
|
|
if (menu) {
|
|
if (enabled) {
|
|
if (!menu.roleIds.includes(roleId)) {
|
|
menu.roleIds.push(roleId)
|
|
}
|
|
} else {
|
|
menu.roleIds = menu.roleIds.filter((id: number) => id !== roleId)
|
|
}
|
|
}
|
|
|
|
const roleName = roles.value.find(r => r.roleId === roleId)?.roleName || ''
|
|
showToast('success', `${enabled ? '권한 추가' : '권한 제거'}: ${roleName}`)
|
|
} catch (e: any) {
|
|
console.error(e)
|
|
checkbox.checked = !enabled // 롤백
|
|
showToast('error', '권한 변경에 실패했습니다.')
|
|
}
|
|
}
|
|
|
|
function showToast(type: 'success' | 'error', message: string) {
|
|
toast.value = { show: true, type, message }
|
|
setTimeout(() => {
|
|
toast.value.show = false
|
|
}, 2500)
|
|
}
|
|
</script>
|