Files
weeklyreport/frontend/admin/menu/index.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>