권한, 사용자, 메뉴 등에 대한 기능 업데이트
This commit is contained in:
231
frontend/components/common/RoleManageModal.vue
Normal file
231
frontend/components/common/RoleManageModal.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5)">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-shield-lock me-2"></i>권한관리
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" @click="$emit('close')"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 상단 버튼 -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span>권한 목록 총 <strong>{{ roles.length }}</strong>건</span>
|
||||
<div>
|
||||
<button class="btn btn-primary btn-sm me-2" @click="showAddForm = true" v-if="!showAddForm">
|
||||
<i class="bi bi-plus-lg me-1"></i>신규
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
@click="deleteSelected"
|
||||
:disabled="selectedIds.length === 0"
|
||||
>
|
||||
<i class="bi bi-trash me-1"></i>선택 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 신규 추가 폼 -->
|
||||
<div v-if="showAddForm" class="card mb-3 border-primary">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">권한코드 *</label>
|
||||
<input type="text" class="form-control form-control-sm" v-model="newRole.roleCode" placeholder="ROLE_XXX" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">권한명 *</label>
|
||||
<input type="text" class="form-control form-control-sm" v-model="newRole.roleName" placeholder="권한명" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button class="btn btn-primary btn-sm me-1" @click="createRole" :disabled="isCreating">
|
||||
<span v-if="isCreating" class="spinner-border spinner-border-sm me-1"></span>
|
||||
저장
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" @click="cancelAdd">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 권한 목록 테이블 -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px" class="text-center">
|
||||
<input type="checkbox" class="form-check-input" v-model="selectAll" @change="toggleSelectAll" />
|
||||
</th>
|
||||
<th style="width: 50px" class="text-center">No</th>
|
||||
<th style="width: 180px">권한코드</th>
|
||||
<th>권한명</th>
|
||||
<th style="width: 80px" class="text-center">사용자 수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="isLoading">
|
||||
<td colspan="5" class="text-center py-4">
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>로딩 중...
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else v-for="(role, idx) in roles" :key="role.role_id">
|
||||
<td class="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
:value="role.role_id"
|
||||
v-model="selectedIds"
|
||||
:disabled="isProtectedRole(role.role_code)"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">{{ idx + 1 }}</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm border-0 bg-transparent"
|
||||
v-model="role.role_code"
|
||||
:disabled="isProtectedRole(role.role_code)"
|
||||
@blur="updateRole(role)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-sm border-0 bg-transparent"
|
||||
v-model="role.role_name"
|
||||
@blur="updateRole(role)"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-center">{{ role.user_count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="$emit('close')">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['close', 'updated'])
|
||||
|
||||
const isLoading = ref(true)
|
||||
const isCreating = ref(false)
|
||||
const roles = ref<any[]>([])
|
||||
const selectedIds = ref<number[]>([])
|
||||
const selectAll = ref(false)
|
||||
const showAddForm = ref(false)
|
||||
|
||||
const newRole = ref({
|
||||
roleCode: '',
|
||||
roleName: ''
|
||||
})
|
||||
|
||||
const protectedRoles = ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_USER']
|
||||
|
||||
function isProtectedRole(code: string): boolean {
|
||||
return protectedRoles.includes(code)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRoles()
|
||||
})
|
||||
|
||||
async function loadRoles() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const response = await $fetch<any>('/api/admin/role/list')
|
||||
roles.value = response.roles || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert('권한 목록을 불러오는데 실패했습니다.')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectAll.value) {
|
||||
selectedIds.value = roles.value
|
||||
.filter(r => !isProtectedRole(r.role_code))
|
||||
.map(r => r.role_id)
|
||||
} else {
|
||||
selectedIds.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
showAddForm.value = false
|
||||
newRole.value = { roleCode: '', roleName: '' }
|
||||
}
|
||||
|
||||
async function createRole() {
|
||||
if (!newRole.value.roleCode || !newRole.value.roleName) {
|
||||
alert('권한코드와 권한명은 필수입니다.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isCreating.value = true
|
||||
await $fetch('/api/admin/role/create', {
|
||||
method: 'POST',
|
||||
body: newRole.value
|
||||
})
|
||||
cancelAdd()
|
||||
await loadRoles()
|
||||
emit('updated')
|
||||
} catch (e: any) {
|
||||
alert(e.data?.message || '권한 생성에 실패했습니다.')
|
||||
} finally {
|
||||
isCreating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function updateRole(role: any) {
|
||||
try {
|
||||
await $fetch(`/api/admin/role/${role.role_id}/update`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
roleName: role.role_name
|
||||
}
|
||||
})
|
||||
emit('updated')
|
||||
} catch (e: any) {
|
||||
alert(e.data?.message || '권한 수정에 실패했습니다.')
|
||||
await loadRoles()
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
if (selectedIds.value.length === 0) return
|
||||
|
||||
if (!confirm(`선택한 ${selectedIds.value.length}개의 권한을 삭제하시겠습니까?`)) return
|
||||
|
||||
try {
|
||||
for (const id of selectedIds.value) {
|
||||
await $fetch(`/api/admin/role/${id}/delete`, { method: 'DELETE' })
|
||||
}
|
||||
selectedIds.value = []
|
||||
selectAll.value = false
|
||||
await loadRoles()
|
||||
emit('updated')
|
||||
} catch (e: any) {
|
||||
alert(e.data?.message || '권한 삭제에 실패했습니다.')
|
||||
await loadRoles()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-check-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.form-control:disabled {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -12,49 +12,31 @@
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<NuxtLink class="nav-link" to="/" active-class="active">
|
||||
<i class="bi bi-house me-1"></i> 대시보드
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NuxtLink class="nav-link" to="/report/weekly" active-class="active">
|
||||
<i class="bi bi-journal-text me-1"></i> 주간보고
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NuxtLink class="nav-link" to="/report/summary" active-class="active">
|
||||
<i class="bi bi-collection me-1"></i> 취합보고
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NuxtLink class="nav-link" to="/project" active-class="active">
|
||||
<i class="bi bi-folder me-1"></i> 프로젝트
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NuxtLink class="nav-link" to="/employee" active-class="active">
|
||||
<i class="bi bi-people me-1"></i> 직원관리
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<NuxtLink class="nav-link" to="/feedback" active-class="active">
|
||||
<i class="bi bi-lightbulb me-1"></i> 개선의견
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<!-- 관리자 메뉴 (coziny@gmail.com 전용) -->
|
||||
<li class="nav-item dropdown" v-if="isAdmin">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-gear me-1"></i> 관리자
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<NuxtLink class="dropdown-item" to="/admin/bulk-import">
|
||||
<i class="bi bi-file-earmark-arrow-up me-2"></i>주간보고 일괄등록
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- 동적 메뉴 렌더링 -->
|
||||
<template v-for="menu in userMenus" :key="menu.menuId">
|
||||
<!-- 자식이 없는 메뉴 -->
|
||||
<li class="nav-item" v-if="!menu.children || menu.children.length === 0">
|
||||
<NuxtLink class="nav-link" :to="menu.menuPath || '/'" active-class="active">
|
||||
<i :class="['bi', menu.menuIcon, 'me-1']" v-if="menu.menuIcon"></i>
|
||||
{{ menu.menuName }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<!-- 자식이 있는 메뉴 (드롭다운) -->
|
||||
<li class="nav-item dropdown" v-else>
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i :class="['bi', menu.menuIcon, 'me-1']" v-if="menu.menuIcon"></i>
|
||||
{{ menu.menuName }}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li v-for="child in menu.children" :key="child.menuId">
|
||||
<NuxtLink class="dropdown-item" :to="child.menuPath || '/'">
|
||||
<i :class="['bi', child.menuIcon, 'me-2']" v-if="child.menuIcon"></i>
|
||||
{{ child.menuName }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<!-- 사용자 정보 -->
|
||||
@@ -73,11 +55,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { currentUser, logout } = useAuth()
|
||||
const { currentUser, userMenus, logout } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const isAdmin = computed(() => currentUser.value?.employeeEmail === 'coziny@gmail.com')
|
||||
|
||||
async function handleLogout() {
|
||||
await logout()
|
||||
router.push('/login')
|
||||
|
||||
Reference in New Issue
Block a user