|
|
@@ -1,471 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="admin--admins">
|
|
|
- <div class="admin--page-header">
|
|
|
- <h3>관리자 관리</h3>
|
|
|
- <button @click="openCreateModal" class="admin--btn-small admin--btn-small-primary">
|
|
|
- + 관리자 추가
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 검색 영역 -->
|
|
|
- <div class="admin--search-box">
|
|
|
- <div class="admin--search-form">
|
|
|
- <select v-model="filterRole" @change="loadAdmins" class="admin--form-select admin--search-select">
|
|
|
- <option value="">전체 역할</option>
|
|
|
- <option value="super_admin">슈퍼 관리자</option>
|
|
|
- <option value="admin">일반 관리자</option>
|
|
|
- </select>
|
|
|
- <select v-model="filterStatus" @change="loadAdmins" class="admin--form-select admin--search-select">
|
|
|
- <option value="">전체 상태</option>
|
|
|
- <option value="active">활성</option>
|
|
|
- <option value="inactive">비활성</option>
|
|
|
- </select>
|
|
|
- <input
|
|
|
- v-model="searchQuery"
|
|
|
- type="text"
|
|
|
- placeholder="아이디, 이름으로 검색"
|
|
|
- @keyup.enter="loadAdmins"
|
|
|
- class="admin--form-input admin--search-input"
|
|
|
- />
|
|
|
- <button @click="loadAdmins" class="admin--btn-small admin--btn-small-primary">
|
|
|
- 검색
|
|
|
- </button>
|
|
|
- <button @click="resetSearch" class="admin--btn-small admin--btn-small-secondary">
|
|
|
- 초기화
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 관리자 목록 테이블 -->
|
|
|
- <div class="admin--table-container">
|
|
|
- <table class="admin--table">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>ID</th>
|
|
|
- <th>아이디</th>
|
|
|
- <th>이름</th>
|
|
|
- <th>이메일</th>
|
|
|
- <th>부서</th>
|
|
|
- <th>역할</th>
|
|
|
- <th>상태</th>
|
|
|
- <th>생성일</th>
|
|
|
- <th>관리</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr v-if="loading">
|
|
|
- <td colspan="9" class="admin--loading">로딩 중...</td>
|
|
|
- </tr>
|
|
|
- <tr v-else-if="admins.length === 0">
|
|
|
- <td colspan="9" class="admin--no-data">관리자가 없습니다.</td>
|
|
|
- </tr>
|
|
|
- <tr v-else v-for="admin in admins" :key="admin.id">
|
|
|
- <td>{{ admin.id }}</td>
|
|
|
- <td>{{ admin.username }}</td>
|
|
|
- <td>{{ admin.name }}</td>
|
|
|
- <td>{{ admin.email }}</td>
|
|
|
- <td>{{ admin.department || '-' }}</td>
|
|
|
- <td>
|
|
|
- <span :class="['admin--badge', getRoleBadgeClass(admin.role)]">
|
|
|
- {{ getRoleLabel(admin.role) }}
|
|
|
- </span>
|
|
|
- </td>
|
|
|
- <td>
|
|
|
- <div style="display: flex; gap: 4px; align-items: center;">
|
|
|
- <span :class="['admin--badge', getStatusBadgeClass(admin.status)]">
|
|
|
- {{ getStatusLabel(admin.status) }}
|
|
|
- </span>
|
|
|
- <span v-if="admin.login_attempts >= 5" class="admin--badge admin--badge-danger" title="로그인 5회 실패로 계정 잠김">
|
|
|
- 🔒 잠김
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- <td>{{ formatDate(admin.created_at) }}</td>
|
|
|
- <td>
|
|
|
- <div class="admin--table-actions admin--table-actions-col">
|
|
|
- <button @click="openEditModal(admin)" class="admin--btn-small admin--btn-small-primary">
|
|
|
- 수정
|
|
|
- </button>
|
|
|
- <button @click="openPasswordModal(admin)" class="admin--btn-small admin--btn-small-secondary">
|
|
|
- 비밀번호
|
|
|
- </button>
|
|
|
- <button
|
|
|
- v-if="admin.login_attempts >= 5"
|
|
|
- @click="confirmUnlockAccount(admin)"
|
|
|
- class="admin--btn-small admin--btn-small-warning"
|
|
|
- title="계정 잠금 해제"
|
|
|
- >
|
|
|
- 잠금해제
|
|
|
- </button>
|
|
|
- <button
|
|
|
- @click="confirmDeleteAdmin(admin)"
|
|
|
- class="admin--btn-small admin--btn-small-danger"
|
|
|
- :disabled="admin.id === currentAdminId"
|
|
|
- >
|
|
|
- 삭제
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 페이지네이션 -->
|
|
|
- <div class="admin--pagination" v-if="totalPages > 1">
|
|
|
- <button
|
|
|
- @click="changePage(currentPage - 1)"
|
|
|
- :disabled="currentPage === 1"
|
|
|
- class="admin--btn-page"
|
|
|
- >
|
|
|
- 이전
|
|
|
- </button>
|
|
|
- <span class="admin--page-info">
|
|
|
- {{ currentPage }} / {{ totalPages }}
|
|
|
- </span>
|
|
|
- <button
|
|
|
- @click="changePage(currentPage + 1)"
|
|
|
- :disabled="currentPage === totalPages"
|
|
|
- class="admin--btn-page"
|
|
|
- >
|
|
|
- 다음
|
|
|
- </button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 관리자 추가/수정 모달 -->
|
|
|
- <AdminModal
|
|
|
- v-if="showModal"
|
|
|
- :admin="selectedAdmin"
|
|
|
- @close="closeModal"
|
|
|
- @saved="handleSaved"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 비밀번호 변경 모달 -->
|
|
|
- <PasswordModal
|
|
|
- v-if="showPasswordModal"
|
|
|
- :admin="selectedAdmin"
|
|
|
- @close="closePasswordModal"
|
|
|
- @saved="handlePasswordChanged"
|
|
|
- />
|
|
|
-
|
|
|
- <!-- 알림 모달 -->
|
|
|
- <AdminAlertModal
|
|
|
- v-if="alertModal.show"
|
|
|
- :title="alertModal.title"
|
|
|
- :message="alertModal.message"
|
|
|
- :type="alertModal.type"
|
|
|
- @confirm="handleAlertConfirm"
|
|
|
- @cancel="handleAlertCancel"
|
|
|
- @close="closeAlertModal"
|
|
|
- />
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup>
|
|
|
-import { ref, onMounted, computed } from 'vue'
|
|
|
-import AdminAlertModal from '~/components/admin/AdminAlertModal.vue'
|
|
|
-import AdminModal from '~/components/admin/AdminModal.vue'
|
|
|
-import PasswordModal from '~/components/admin/PasswordModal.vue'
|
|
|
-
|
|
|
-definePageMeta({
|
|
|
- layout: 'admin',
|
|
|
- middleware: ['auth']
|
|
|
-})
|
|
|
-
|
|
|
-const { get, del, post } = useApi()
|
|
|
-
|
|
|
-// 현재 로그인한 관리자 ID
|
|
|
-const currentAdminId = computed(() => {
|
|
|
- if (typeof window === 'undefined') return null
|
|
|
- const user = localStorage.getItem('admin_user')
|
|
|
- if (!user) return null
|
|
|
- try {
|
|
|
- return JSON.parse(user).id
|
|
|
- } catch {
|
|
|
- return null
|
|
|
- }
|
|
|
-})
|
|
|
-
|
|
|
-// 데이터
|
|
|
-const admins = ref([])
|
|
|
-const loading = ref(false)
|
|
|
-const searchQuery = ref('')
|
|
|
-const filterRole = ref('')
|
|
|
-const filterStatus = ref('')
|
|
|
-
|
|
|
-// 페이지네이션
|
|
|
-const currentPage = ref(1)
|
|
|
-const totalPages = ref(1)
|
|
|
-const perPage = ref(10)
|
|
|
-
|
|
|
-// 모달
|
|
|
-const showModal = ref(false)
|
|
|
-const showPasswordModal = ref(false)
|
|
|
-const selectedAdmin = ref(null)
|
|
|
-
|
|
|
-// 알림 모달
|
|
|
-const alertModal = ref({
|
|
|
- show: false,
|
|
|
- title: '알림',
|
|
|
- message: '',
|
|
|
- type: 'alert',
|
|
|
- onConfirm: null
|
|
|
-})
|
|
|
-
|
|
|
-// 알림 모달 표시
|
|
|
-const showAlert = (message, title = '알림') => {
|
|
|
- alertModal.value = {
|
|
|
- show: true,
|
|
|
- title,
|
|
|
- message,
|
|
|
- type: 'alert',
|
|
|
- onConfirm: null
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 확인 모달 표시
|
|
|
-const showConfirm = (message, onConfirm, title = '확인') => {
|
|
|
- alertModal.value = {
|
|
|
- show: true,
|
|
|
- title,
|
|
|
- message,
|
|
|
- type: 'confirm',
|
|
|
- onConfirm
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 알림 모달 닫기
|
|
|
-const closeAlertModal = () => {
|
|
|
- alertModal.value.show = false
|
|
|
-}
|
|
|
-
|
|
|
-// 알림 모달 확인
|
|
|
-const handleAlertConfirm = () => {
|
|
|
- if (alertModal.value.onConfirm) {
|
|
|
- alertModal.value.onConfirm()
|
|
|
- }
|
|
|
- closeAlertModal()
|
|
|
-}
|
|
|
-
|
|
|
-// 알림 모달 취소
|
|
|
-const handleAlertCancel = () => {
|
|
|
- closeAlertModal()
|
|
|
-}
|
|
|
-
|
|
|
-// 관리자 목록 로드
|
|
|
-const loadAdmins = async () => {
|
|
|
- loading.value = true
|
|
|
- try {
|
|
|
- const params = {
|
|
|
- page: currentPage.value,
|
|
|
- per_page: perPage.value
|
|
|
- }
|
|
|
-
|
|
|
- if (searchQuery.value) {
|
|
|
- params.search = searchQuery.value
|
|
|
- }
|
|
|
- if (filterRole.value) {
|
|
|
- params.role = filterRole.value
|
|
|
- }
|
|
|
- if (filterStatus.value) {
|
|
|
- params.status = filterStatus.value
|
|
|
- }
|
|
|
-
|
|
|
- console.log('[Admins] 검색 파라미터:', params)
|
|
|
-
|
|
|
- const { data, error } = await get('/admin', { params })
|
|
|
-
|
|
|
- if (error) {
|
|
|
- console.error('[Admins] 목록 로드 실패:', error)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // API 응답: { success: true, data: { items, total_pages }, message }
|
|
|
- if (data?.success && data?.data) {
|
|
|
- admins.value = data.data.items || []
|
|
|
- totalPages.value = data.data.total_pages || 1
|
|
|
- console.log('[Admins] 목록 로드 성공:', data.data)
|
|
|
- console.log('[Admins] 첫번째 관리자 login_attempts:', admins.value[0]?.login_attempts)
|
|
|
- console.log('[Admins] 잠긴 계정 수:', admins.value.filter(a => a.login_attempts >= 5).length)
|
|
|
- }
|
|
|
- } finally {
|
|
|
- loading.value = false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 페이지 변경
|
|
|
-const changePage = (page) => {
|
|
|
- if (page < 1 || page > totalPages.value) return
|
|
|
- currentPage.value = page
|
|
|
- loadAdmins()
|
|
|
-}
|
|
|
-
|
|
|
-// 검색 초기화
|
|
|
-const resetSearch = () => {
|
|
|
- searchQuery.value = ''
|
|
|
- filterRole.value = ''
|
|
|
- filterStatus.value = ''
|
|
|
- currentPage.value = 1
|
|
|
- loadAdmins()
|
|
|
-}
|
|
|
-
|
|
|
-// 모달 열기/닫기
|
|
|
-const openCreateModal = () => {
|
|
|
- selectedAdmin.value = null
|
|
|
- showModal.value = true
|
|
|
-}
|
|
|
-
|
|
|
-const openEditModal = (admin) => {
|
|
|
- selectedAdmin.value = { ...admin }
|
|
|
- showModal.value = true
|
|
|
-}
|
|
|
-
|
|
|
-const closeModal = () => {
|
|
|
- showModal.value = false
|
|
|
- selectedAdmin.value = null
|
|
|
-}
|
|
|
-
|
|
|
-const handleSaved = (message) => {
|
|
|
- closeModal()
|
|
|
- loadAdmins()
|
|
|
- if (message) {
|
|
|
- showAlert(message, '성공')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 비밀번호 모달
|
|
|
-const openPasswordModal = (admin) => {
|
|
|
- selectedAdmin.value = { ...admin }
|
|
|
- showPasswordModal.value = true
|
|
|
-}
|
|
|
-
|
|
|
-const closePasswordModal = () => {
|
|
|
- showPasswordModal.value = false
|
|
|
- selectedAdmin.value = null
|
|
|
-}
|
|
|
-
|
|
|
-const handlePasswordChanged = (message) => {
|
|
|
- closePasswordModal()
|
|
|
- if (message) {
|
|
|
- showAlert(message, '성공')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 관리자 삭제 확인
|
|
|
-const confirmDeleteAdmin = (admin) => {
|
|
|
- if (admin.id === currentAdminId.value) {
|
|
|
- showAlert('본인 계정은 삭제할 수 없습니다.', '경고')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- showConfirm(
|
|
|
- `${admin.name} (${admin.username}) 관리자를 삭제하시겠습니까?`,
|
|
|
- () => deleteAdmin(admin),
|
|
|
- '관리자 삭제'
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-// 관리자 삭제
|
|
|
-const deleteAdmin = async (admin) => {
|
|
|
- const { data, error } = await del(`/admin/${admin.id}`)
|
|
|
-
|
|
|
- if (error) {
|
|
|
- showAlert('관리자 삭제에 실패했습니다.', '오류')
|
|
|
- console.error('[Admins] 삭제 실패:', error)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (data?.success) {
|
|
|
- showAlert('관리자가 삭제되었습니다.', '성공')
|
|
|
- loadAdmins()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 계정 잠금 해제 확인
|
|
|
-const confirmUnlockAccount = (admin) => {
|
|
|
- showConfirm(
|
|
|
- `${admin.name} (${admin.username}) 계정의 잠금을 해제하시겠습니까?`,
|
|
|
- () => unlockAccount(admin),
|
|
|
- '계정 잠금 해제'
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-// 계정 잠금 해제
|
|
|
-const unlockAccount = async (admin) => {
|
|
|
- const { data, error } = await post(`/admin/${admin.id}/unlock`)
|
|
|
-
|
|
|
- if (error) {
|
|
|
- showAlert('계정 잠금 해제에 실패했습니다.', '오류')
|
|
|
- console.error('[Admins] 잠금 해제 실패:', error)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (data?.success) {
|
|
|
- showAlert('계정 잠금이 해제되었습니다.', '성공')
|
|
|
- loadAdmins()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 유틸리티 함수
|
|
|
-const getRoleLabel = (role) => {
|
|
|
- const labels = {
|
|
|
- super_admin: '슈퍼 관리자',
|
|
|
- admin: '일반 관리자'
|
|
|
- }
|
|
|
- return labels[role] || role
|
|
|
-}
|
|
|
-
|
|
|
-const getRoleBadgeClass = (role) => {
|
|
|
- return role === 'super_admin' ? 'admin--badge-danger' : 'admin--badge-primary'
|
|
|
-}
|
|
|
-
|
|
|
-const getStatusLabel = (status) => {
|
|
|
- const labels = {
|
|
|
- active: '활성',
|
|
|
- inactive: '비활성'
|
|
|
- }
|
|
|
- return labels[status] || status
|
|
|
-}
|
|
|
-
|
|
|
-const getStatusBadgeClass = (status) => {
|
|
|
- return status === 'active' ? 'admin--badge-success' : 'admin--badge-secondary'
|
|
|
-}
|
|
|
-
|
|
|
-const formatDate = (dateString) => {
|
|
|
- if (!dateString) return '-'
|
|
|
- const date = new Date(dateString)
|
|
|
- return date.toLocaleString('ko-KR', {
|
|
|
- year: 'numeric',
|
|
|
- month: '2-digit',
|
|
|
- day: '2-digit',
|
|
|
- hour: '2-digit',
|
|
|
- minute: '2-digit'
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-onMounted(() => {
|
|
|
- loadAdmins()
|
|
|
-})
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped>
|
|
|
-.admin--search-box .admin--form-input,
|
|
|
-.admin--search-box .admin--search-input,
|
|
|
-.admin--search-box .admin--form-select,
|
|
|
-.admin--search-box .admin--search-select {
|
|
|
- border: 1px solid var(--admin-border-color) !important;
|
|
|
- height: 33px !important;
|
|
|
- padding: 6px 14px !important;
|
|
|
- font-size: 13px !important;
|
|
|
-}
|
|
|
-
|
|
|
-.admin--btn-small-warning {
|
|
|
- background: #ff9800;
|
|
|
- color: #ffffff;
|
|
|
- border: none;
|
|
|
-}
|
|
|
-
|
|
|
-.admin--btn-small-warning:hover:not(:disabled) {
|
|
|
- background: #f57c00;
|
|
|
-}
|
|
|
-</style>
|