index.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <template>
  2. <div class="admin--admins">
  3. <div class="admin--page-header">
  4. <h3>관리자 관리</h3>
  5. <button @click="openCreateModal" class="admin--btn-primary">
  6. <span>+</span> 관리자 추가
  7. </button>
  8. </div>
  9. <!-- 검색 및 필터 -->
  10. <div class="admin--search-area">
  11. <div class="admin--search-box">
  12. <input
  13. v-model="searchQuery"
  14. type="text"
  15. placeholder="아이디, 이름, 이메일로 검색"
  16. @keyup.enter="loadAdmins"
  17. class="admin--input"
  18. />
  19. <button @click="loadAdmins" class="admin--btn-search">검색</button>
  20. </div>
  21. <div class="admin--filters">
  22. <select v-model="filterRole" @change="loadAdmins" class="admin--select">
  23. <option value="">전체 역할</option>
  24. <option value="super_admin">슈퍼 관리자</option>
  25. <option value="admin">일반 관리자</option>
  26. </select>
  27. <select v-model="filterStatus" @change="loadAdmins" class="admin--select">
  28. <option value="">전체 상태</option>
  29. <option value="active">활성</option>
  30. <option value="inactive">비활성</option>
  31. </select>
  32. </div>
  33. </div>
  34. <!-- 관리자 목록 테이블 -->
  35. <div class="admin--table-container">
  36. <table class="admin--table">
  37. <thead>
  38. <tr>
  39. <th>ID</th>
  40. <th>아이디</th>
  41. <th>이름</th>
  42. <th>이메일</th>
  43. <th>역할</th>
  44. <th>상태</th>
  45. <th>생성일</th>
  46. <th>관리</th>
  47. </tr>
  48. </thead>
  49. <tbody>
  50. <tr v-if="loading">
  51. <td colspan="8" class="admin--loading">로딩 중...</td>
  52. </tr>
  53. <tr v-else-if="admins.length === 0">
  54. <td colspan="8" class="admin--no-data">관리자가 없습니다.</td>
  55. </tr>
  56. <tr v-else v-for="admin in admins" :key="admin.id">
  57. <td>{{ admin.id }}</td>
  58. <td>{{ admin.username }}</td>
  59. <td>{{ admin.name }}</td>
  60. <td>{{ admin.email }}</td>
  61. <td>
  62. <span :class="['admin--badge', getRoleBadgeClass(admin.role)]">
  63. {{ getRoleLabel(admin.role) }}
  64. </span>
  65. </td>
  66. <td>
  67. <span :class="['admin--badge', getStatusBadgeClass(admin.status)]">
  68. {{ getStatusLabel(admin.status) }}
  69. </span>
  70. </td>
  71. <td>{{ formatDate(admin.created_at) }}</td>
  72. <td>
  73. <div class="admin--table-actions">
  74. <button @click="openEditModal(admin)" class="admin--btn-sm admin--btn-edit">
  75. 수정
  76. </button>
  77. <button @click="openPasswordModal(admin)" class="admin--btn-sm admin--btn-warning">
  78. 비밀번호
  79. </button>
  80. <button
  81. @click="confirmDeleteAdmin(admin)"
  82. class="admin--btn-sm admin--btn-delete"
  83. :disabled="admin.id === currentAdminId"
  84. >
  85. 삭제
  86. </button>
  87. </div>
  88. </td>
  89. </tr>
  90. </tbody>
  91. </table>
  92. </div>
  93. <!-- 페이지네이션 -->
  94. <div class="admin--pagination" v-if="totalPages > 1">
  95. <button
  96. @click="changePage(currentPage - 1)"
  97. :disabled="currentPage === 1"
  98. class="admin--btn-page"
  99. >
  100. 이전
  101. </button>
  102. <span class="admin--page-info">
  103. {{ currentPage }} / {{ totalPages }}
  104. </span>
  105. <button
  106. @click="changePage(currentPage + 1)"
  107. :disabled="currentPage === totalPages"
  108. class="admin--btn-page"
  109. >
  110. 다음
  111. </button>
  112. </div>
  113. <!-- 관리자 추가/수정 모달 -->
  114. <AdminModal
  115. v-if="showModal"
  116. :admin="selectedAdmin"
  117. @close="closeModal"
  118. @saved="handleSaved"
  119. />
  120. <!-- 비밀번호 변경 모달 -->
  121. <PasswordModal
  122. v-if="showPasswordModal"
  123. :admin="selectedAdmin"
  124. @close="closePasswordModal"
  125. @saved="handlePasswordChanged"
  126. />
  127. <!-- 알림 모달 -->
  128. <AdminAlertModal
  129. v-if="alertModal.show"
  130. :title="alertModal.title"
  131. :message="alertModal.message"
  132. :type="alertModal.type"
  133. @confirm="handleAlertConfirm"
  134. @cancel="handleAlertCancel"
  135. @close="closeAlertModal"
  136. />
  137. </div>
  138. </template>
  139. <script setup>
  140. import { ref, onMounted, computed } from 'vue'
  141. import AdminAlertModal from '~/components/admin/AdminAlertModal.vue'
  142. import AdminModal from '~/components/admin/AdminModal.vue'
  143. import PasswordModal from '~/components/admin/PasswordModal.vue'
  144. definePageMeta({
  145. layout: 'admin',
  146. middleware: ['auth']
  147. })
  148. const { get, del } = useApi()
  149. // 현재 로그인한 관리자 ID
  150. const currentAdminId = computed(() => {
  151. if (typeof window === 'undefined') return null
  152. const user = localStorage.getItem('admin_user')
  153. if (!user) return null
  154. try {
  155. return JSON.parse(user).id
  156. } catch {
  157. return null
  158. }
  159. })
  160. // 데이터
  161. const admins = ref([])
  162. const loading = ref(false)
  163. const searchQuery = ref('')
  164. const filterRole = ref('')
  165. const filterStatus = ref('')
  166. // 페이지네이션
  167. const currentPage = ref(1)
  168. const totalPages = ref(1)
  169. const perPage = ref(10)
  170. // 모달
  171. const showModal = ref(false)
  172. const showPasswordModal = ref(false)
  173. const selectedAdmin = ref(null)
  174. // 알림 모달
  175. const alertModal = ref({
  176. show: false,
  177. title: '알림',
  178. message: '',
  179. type: 'alert',
  180. onConfirm: null
  181. })
  182. // 알림 모달 표시
  183. const showAlert = (message, title = '알림') => {
  184. alertModal.value = {
  185. show: true,
  186. title,
  187. message,
  188. type: 'alert',
  189. onConfirm: null
  190. }
  191. }
  192. // 확인 모달 표시
  193. const showConfirm = (message, onConfirm, title = '확인') => {
  194. alertModal.value = {
  195. show: true,
  196. title,
  197. message,
  198. type: 'confirm',
  199. onConfirm
  200. }
  201. }
  202. // 알림 모달 닫기
  203. const closeAlertModal = () => {
  204. alertModal.value.show = false
  205. }
  206. // 알림 모달 확인
  207. const handleAlertConfirm = () => {
  208. if (alertModal.value.onConfirm) {
  209. alertModal.value.onConfirm()
  210. }
  211. closeAlertModal()
  212. }
  213. // 알림 모달 취소
  214. const handleAlertCancel = () => {
  215. closeAlertModal()
  216. }
  217. // 관리자 목록 로드
  218. const loadAdmins = async () => {
  219. loading.value = true
  220. try {
  221. const params = {
  222. page: currentPage.value,
  223. per_page: perPage.value
  224. }
  225. if (searchQuery.value) {
  226. params.search = searchQuery.value
  227. }
  228. if (filterRole.value) {
  229. params.role = filterRole.value
  230. }
  231. if (filterStatus.value) {
  232. params.status = filterStatus.value
  233. }
  234. const { data, error } = await get('/admin', { params })
  235. if (error) {
  236. console.error('[Admins] 목록 로드 실패:', error)
  237. return
  238. }
  239. // API 응답: { success: true, data: { items, total_pages }, message }
  240. if (data?.success && data?.data) {
  241. admins.value = data.data.items || []
  242. totalPages.value = data.data.total_pages || 1
  243. console.log('[Admins] 목록 로드 성공:', data.data)
  244. }
  245. } finally {
  246. loading.value = false
  247. }
  248. }
  249. // 페이지 변경
  250. const changePage = (page) => {
  251. if (page < 1 || page > totalPages.value) return
  252. currentPage.value = page
  253. loadAdmins()
  254. }
  255. // 모달 열기/닫기
  256. const openCreateModal = () => {
  257. selectedAdmin.value = null
  258. showModal.value = true
  259. }
  260. const openEditModal = (admin) => {
  261. selectedAdmin.value = { ...admin }
  262. showModal.value = true
  263. }
  264. const closeModal = () => {
  265. showModal.value = false
  266. selectedAdmin.value = null
  267. }
  268. const handleSaved = (message) => {
  269. closeModal()
  270. loadAdmins()
  271. if (message) {
  272. showAlert(message, '성공')
  273. }
  274. }
  275. // 비밀번호 모달
  276. const openPasswordModal = (admin) => {
  277. selectedAdmin.value = { ...admin }
  278. showPasswordModal.value = true
  279. }
  280. const closePasswordModal = () => {
  281. showPasswordModal.value = false
  282. selectedAdmin.value = null
  283. }
  284. const handlePasswordChanged = (message) => {
  285. closePasswordModal()
  286. if (message) {
  287. showAlert(message, '성공')
  288. }
  289. }
  290. // 관리자 삭제 확인
  291. const confirmDeleteAdmin = (admin) => {
  292. if (admin.id === currentAdminId.value) {
  293. showAlert('본인 계정은 삭제할 수 없습니다.', '경고')
  294. return
  295. }
  296. showConfirm(
  297. `${admin.name} (${admin.username}) 관리자를 삭제하시겠습니까?`,
  298. () => deleteAdmin(admin),
  299. '관리자 삭제'
  300. )
  301. }
  302. // 관리자 삭제
  303. const deleteAdmin = async (admin) => {
  304. const { data, error } = await del(`/admin/${admin.id}`)
  305. if (error) {
  306. showAlert('관리자 삭제에 실패했습니다.', '오류')
  307. console.error('[Admins] 삭제 실패:', error)
  308. return
  309. }
  310. if (data?.success) {
  311. showAlert('관리자가 삭제되었습니다.', '성공')
  312. loadAdmins()
  313. }
  314. }
  315. // 유틸리티 함수
  316. const getRoleLabel = (role) => {
  317. const labels = {
  318. super_admin: '슈퍼 관리자',
  319. admin: '일반 관리자'
  320. }
  321. return labels[role] || role
  322. }
  323. const getRoleBadgeClass = (role) => {
  324. return role === 'super_admin' ? 'admin--badge-danger' : 'admin--badge-primary'
  325. }
  326. const getStatusLabel = (status) => {
  327. const labels = {
  328. active: '활성',
  329. inactive: '비활성'
  330. }
  331. return labels[status] || status
  332. }
  333. const getStatusBadgeClass = (status) => {
  334. return status === 'active' ? 'admin--badge-success' : 'admin--badge-secondary'
  335. }
  336. const formatDate = (dateString) => {
  337. if (!dateString) return '-'
  338. const date = new Date(dateString)
  339. return date.toLocaleString('ko-KR', {
  340. year: 'numeric',
  341. month: '2-digit',
  342. day: '2-digit',
  343. hour: '2-digit',
  344. minute: '2-digit'
  345. })
  346. }
  347. onMounted(() => {
  348. loadAdmins()
  349. })
  350. </script>