index.vue 10 KB

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