index.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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. if (data) {
  240. admins.value = data.items || []
  241. totalPages.value = data.total_pages || 1
  242. console.log('[Admins] 목록 로드 성공:', data)
  243. }
  244. } finally {
  245. loading.value = false
  246. }
  247. }
  248. // 페이지 변경
  249. const changePage = (page) => {
  250. if (page < 1 || page > totalPages.value) return
  251. currentPage.value = page
  252. loadAdmins()
  253. }
  254. // 모달 열기/닫기
  255. const openCreateModal = () => {
  256. selectedAdmin.value = null
  257. showModal.value = true
  258. }
  259. const openEditModal = (admin) => {
  260. selectedAdmin.value = { ...admin }
  261. showModal.value = true
  262. }
  263. const closeModal = () => {
  264. showModal.value = false
  265. selectedAdmin.value = null
  266. }
  267. const handleSaved = (message) => {
  268. closeModal()
  269. loadAdmins()
  270. if (message) {
  271. showAlert(message, '성공')
  272. }
  273. }
  274. // 비밀번호 모달
  275. const openPasswordModal = (admin) => {
  276. selectedAdmin.value = { ...admin }
  277. showPasswordModal.value = true
  278. }
  279. const closePasswordModal = () => {
  280. showPasswordModal.value = false
  281. selectedAdmin.value = null
  282. }
  283. const handlePasswordChanged = (message) => {
  284. closePasswordModal()
  285. if (message) {
  286. showAlert(message, '성공')
  287. }
  288. }
  289. // 관리자 삭제 확인
  290. const confirmDeleteAdmin = (admin) => {
  291. if (admin.id === currentAdminId.value) {
  292. showAlert('본인 계정은 삭제할 수 없습니다.', '경고')
  293. return
  294. }
  295. showConfirm(
  296. `${admin.name} (${admin.username}) 관리자를 삭제하시겠습니까?`,
  297. () => deleteAdmin(admin),
  298. '관리자 삭제'
  299. )
  300. }
  301. // 관리자 삭제
  302. const deleteAdmin = async (admin) => {
  303. const { data, error } = await del(`/admin/${admin.id}`)
  304. if (error) {
  305. showAlert('관리자 삭제에 실패했습니다.', '오류')
  306. console.error('[Admins] 삭제 실패:', error)
  307. return
  308. }
  309. if (data?.success) {
  310. showAlert('관리자가 삭제되었습니다.', '성공')
  311. loadAdmins()
  312. }
  313. }
  314. // 유틸리티 함수
  315. const getRoleLabel = (role) => {
  316. const labels = {
  317. super_admin: '슈퍼 관리자',
  318. admin: '일반 관리자'
  319. }
  320. return labels[role] || role
  321. }
  322. const getRoleBadgeClass = (role) => {
  323. return role === 'super_admin' ? 'admin--badge-danger' : 'admin--badge-primary'
  324. }
  325. const getStatusLabel = (status) => {
  326. const labels = {
  327. active: '활성',
  328. inactive: '비활성'
  329. }
  330. return labels[status] || status
  331. }
  332. const getStatusBadgeClass = (status) => {
  333. return status === 'active' ? 'admin--badge-success' : 'admin--badge-secondary'
  334. }
  335. const formatDate = (dateString) => {
  336. if (!dateString) return '-'
  337. const date = new Date(dateString)
  338. return date.toLocaleString('ko-KR', {
  339. year: 'numeric',
  340. month: '2-digit',
  341. day: '2-digit',
  342. hour: '2-digit',
  343. minute: '2-digit'
  344. })
  345. }
  346. onMounted(() => {
  347. loadAdmins()
  348. })
  349. </script>