admin.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. <template>
  2. <div class="admin--layout">
  3. <!-- Header -->
  4. <header class="admin--header">
  5. <div class="admin--header-left">
  6. <div class="admin--logo">
  7. <h1>AUDI</h1>
  8. <span class="admin--logo-sub">고진모터스</span>
  9. </div>
  10. </div>
  11. <div class="admin--header-right">
  12. <button class="admin--header-btn" @click="goToProfile">정보수정</button>
  13. <button
  14. type="button"
  15. class="admin--header-btn admin--header-btn-logout"
  16. @click.prevent="handleLogout"
  17. >
  18. 로그아웃
  19. </button>
  20. </div>
  21. </header>
  22. <!-- Main Content Area -->
  23. <div class="admin--content-wrapper">
  24. <!-- Sidebar GNB -->
  25. <aside class="admin--sidebar">
  26. <nav class="admin--gnb">
  27. <div v-for="menu in menuItems" :key="menu.id" class="admin--gnb-group">
  28. <div class="admin--gnb-title" @click="toggleMenu(menu.id)">
  29. {{ menu.title }}
  30. <span
  31. class="admin--gnb-arrow"
  32. :class="{ 'is-open': openMenus.includes(menu.id) }"
  33. >
  34. </span>
  35. </div>
  36. <transition name="admin--submenu">
  37. <ul v-show="openMenus.includes(menu.id)" class="admin--gnb-submenu">
  38. <li
  39. v-for="submenu in menu.children"
  40. :key="submenu.path"
  41. class="admin--gnb-item"
  42. :class="{ 'is-active': isActiveRoute(submenu.path) }"
  43. >
  44. <NuxtLink :to="submenu.path" class="admin--gnb-link">
  45. {{ submenu.title }}
  46. </NuxtLink>
  47. </li>
  48. </ul>
  49. </transition>
  50. </div>
  51. </nav>
  52. </aside>
  53. <!-- Content Area -->
  54. <main class="admin--main">
  55. <!-- Breadcrumb & Title -->
  56. <div class="admin--page-header">
  57. <h2 class="admin--page-title">{{ pageTitle }}</h2>
  58. <div class="admin--breadcrumb">
  59. <span v-for="(crumb, index) in breadcrumbs" :key="index">
  60. <NuxtLink v-if="crumb.path" :to="crumb.path" class="admin--breadcrumb-link">
  61. {{ crumb.title }}
  62. </NuxtLink>
  63. <span v-else class="admin--breadcrumb-current">{{ crumb.title }}</span>
  64. <span
  65. v-if="index < breadcrumbs.length - 1"
  66. class="admin--breadcrumb-separator"
  67. >/</span
  68. >
  69. </span>
  70. </div>
  71. </div>
  72. <!-- Page Content -->
  73. <div class="admin--page-content">
  74. <slot />
  75. </div>
  76. <!-- Admin Footer -->
  77. <footer class="admin--footer">
  78. <p>
  79. &copy; {{ new Date().getFullYear() }} Audi 고진모터스. All rights reserved.
  80. </p>
  81. </footer>
  82. </main>
  83. </div>
  84. <!-- 로그아웃 확인 모달 -->
  85. <AdminAlertModal
  86. v-if="showLogoutModal"
  87. title="로그아웃"
  88. message="로그아웃 하시겠습니까?"
  89. type="confirm"
  90. @confirm="confirmLogout"
  91. @cancel="closeLogoutModal"
  92. @close="closeLogoutModal"
  93. />
  94. <!-- 관리자 정보수정 모달 -->
  95. <AdminModal
  96. v-if="showProfileModal"
  97. :admin="currentAdmin"
  98. @close="closeProfileModal"
  99. @saved="handleProfileSaved"
  100. />
  101. <!-- 알림 모달 -->
  102. <AdminAlertModal
  103. v-if="alertModal.show"
  104. :title="alertModal.title"
  105. :message="alertModal.message"
  106. :type="alertModal.type"
  107. @confirm="handleAlertConfirm"
  108. @cancel="handleAlertCancel"
  109. @close="closeAlertModal"
  110. />
  111. </div>
  112. </template>
  113. <script setup>
  114. import { ref, computed } from "vue";
  115. import { useRoute, useRouter } from "vue-router";
  116. import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
  117. import AdminModal from "~/components/admin/AdminModal.vue";
  118. const route = useRoute();
  119. const router = useRouter();
  120. // 메뉴 열림 상태 관리
  121. const openMenus = ref(["basic", "branch", "staff", "service", "board", "system"]);
  122. // GNB 메뉴 구조
  123. const menuItems = ref([
  124. {
  125. id: "basic",
  126. title: "기본정보관리",
  127. children: [
  128. { title: "사이트 정보", path: "/admin/basic/site-info", pattern: /^\/admin\/basic\/site-info/ },
  129. { title: "팝업관리", path: "/admin/basic/popup", pattern: /^\/admin\/basic\/popup/ },
  130. ],
  131. },
  132. {
  133. id: "branch",
  134. title: "지점장관리",
  135. children: [
  136. { title: "지점목록", path: "/admin/branch/list", pattern: /^\/admin\/branch\/(list|create|edit)/ },
  137. { title: "지점장목록", path: "/admin/branch/manager", pattern: /^\/admin\/branch\/manager/ },
  138. ],
  139. },
  140. {
  141. id: "staff",
  142. title: "사원관리",
  143. children: [
  144. { title: "영업사원관리", path: "/admin/staff/sales", pattern: /^\/admin\/staff\/sales/ },
  145. { title: "어드바이저등록", path: "/admin/staff/advisor", pattern: /^\/admin\/staff\/advisor/ },
  146. ],
  147. },
  148. // {
  149. // id: 'service',
  150. // title: '서비스관리',
  151. // children: [
  152. // { title: '브로셔요청', path: '/admin/service/brochure', pattern: /^\/admin\/service\/brochure/ }
  153. // ]
  154. // },
  155. {
  156. id: "board",
  157. title: "게시판관리",
  158. children: [
  159. { title: "이벤트", path: "/admin/board/event", pattern: /^\/admin\/board\/event/ },
  160. { title: "뉴스", path: "/admin/board/news", pattern: /^\/admin\/board\/news/ },
  161. { title: "IR", path: "/admin/board/ir", pattern: /^\/admin\/board\/ir/ },
  162. ],
  163. },
  164. {
  165. id: "system",
  166. title: "시스템관리",
  167. children: [{ title: "관리자관리", path: "/admin/admins", pattern: /^\/admin\/admins/ }],
  168. },
  169. ]);
  170. // 메뉴 토글
  171. const toggleMenu = (menuId) => {
  172. const index = openMenus.value.indexOf(menuId);
  173. if (index > -1) {
  174. openMenus.value.splice(index, 1);
  175. } else {
  176. openMenus.value.push(menuId);
  177. }
  178. };
  179. // 현재 활성 라우트 체크
  180. const isActiveRoute = (path) => {
  181. return route.path === path;
  182. };
  183. // 현재 경로에 맞는 메뉴 찾기
  184. const findCurrentMenu = () => {
  185. const currentPath = route.path;
  186. // 대시보드인 경우
  187. if (currentPath === '/admin/dashboard' || currentPath === '/admin') {
  188. return { menu: null, child: null };
  189. }
  190. for (const menu of menuItems.value) {
  191. for (const child of menu.children) {
  192. // pattern이 있으면 정규식으로 매칭, 없으면 정확히 일치하는지 확인
  193. if (child.pattern && child.pattern.test(currentPath)) {
  194. return { menu, child };
  195. } else if (currentPath === child.path) {
  196. return { menu, child };
  197. }
  198. }
  199. }
  200. return { menu: null, child: null };
  201. };
  202. // 페이지 타이틀 계산
  203. const pageTitle = computed(() => {
  204. const currentPath = route.path;
  205. // 대시보드
  206. if (currentPath === '/admin/dashboard' || currentPath === '/admin') {
  207. return "대시보드";
  208. }
  209. const { child } = findCurrentMenu();
  210. if (child) {
  211. // 상세 페이지 타이틀 처리
  212. if (currentPath.includes('/create')) {
  213. return `${child.title} 등록`;
  214. } else if (currentPath.includes('/edit/')) {
  215. return `${child.title} 수정`;
  216. } else if (currentPath.includes('/print/')) {
  217. return `${child.title} 인쇄`;
  218. } else if (currentPath.includes('/print-a2')) {
  219. return `${child.title} 인쇄 (A2)`;
  220. }
  221. return child.title;
  222. }
  223. return "대시보드";
  224. });
  225. // Breadcrumb 계산
  226. const breadcrumbs = computed(() => {
  227. const currentPath = route.path;
  228. const crumbs = [{ title: "Home", path: "/admin/dashboard" }];
  229. // 대시보드인 경우 Home만 표시
  230. if (currentPath === '/admin/dashboard' || currentPath === '/admin') {
  231. return crumbs;
  232. }
  233. const { menu, child } = findCurrentMenu();
  234. if (menu && child) {
  235. // 메뉴 그룹 추가
  236. crumbs.push({ title: menu.title, path: null });
  237. // 현재 페이지 타이틀 추가
  238. if (currentPath.includes('/create')) {
  239. crumbs.push({ title: child.title, path: child.path });
  240. crumbs.push({ title: "등록", path: null });
  241. } else if (currentPath.includes('/edit/')) {
  242. crumbs.push({ title: child.title, path: child.path });
  243. crumbs.push({ title: "수정", path: null });
  244. } else if (currentPath.includes('/print/')) {
  245. crumbs.push({ title: child.title, path: child.path });
  246. crumbs.push({ title: "인쇄", path: null });
  247. } else if (currentPath.includes('/print-a2')) {
  248. crumbs.push({ title: child.title, path: child.path });
  249. crumbs.push({ title: "인쇄 (A2)", path: null });
  250. } else {
  251. crumbs.push({ title: child.title, path: null });
  252. }
  253. }
  254. return crumbs;
  255. });
  256. // 로그아웃 모달
  257. const showLogoutModal = ref(false);
  258. const handleLogout = () => {
  259. console.log("[Logout] 로그아웃 버튼 클릭");
  260. showLogoutModal.value = true;
  261. console.log("[Logout] showLogoutModal:", showLogoutModal.value);
  262. };
  263. const confirmLogout = () => {
  264. console.log("[Logout] 로그아웃 확인");
  265. localStorage.removeItem("admin_token");
  266. localStorage.removeItem("admin_user");
  267. router.push("/admin");
  268. };
  269. const closeLogoutModal = () => {
  270. console.log("[Logout] 모달 닫기");
  271. showLogoutModal.value = false;
  272. };
  273. // 정보수정 모달
  274. const showProfileModal = ref(false);
  275. const currentAdmin = ref(null);
  276. const { get } = useApi();
  277. // 알림 모달
  278. const alertModal = ref({
  279. show: false,
  280. title: '알림',
  281. message: '',
  282. type: 'alert',
  283. onConfirm: null
  284. });
  285. // 알림 모달 표시
  286. const showAlert = (message, title = '알림') => {
  287. alertModal.value = {
  288. show: true,
  289. title,
  290. message,
  291. type: 'alert',
  292. onConfirm: null
  293. }
  294. };
  295. // 알림 모달 닫기
  296. const closeAlertModal = () => {
  297. alertModal.value.show = false;
  298. };
  299. // 알림 모달 확인
  300. const handleAlertConfirm = () => {
  301. if (alertModal.value.onConfirm) {
  302. alertModal.value.onConfirm();
  303. }
  304. closeAlertModal();
  305. };
  306. // 알림 모달 취소
  307. const handleAlertCancel = () => {
  308. closeAlertModal();
  309. };
  310. // 현재 로그인한 관리자 ID 가져오기
  311. const getCurrentAdminId = () => {
  312. if (typeof window === 'undefined') return null;
  313. const user = localStorage.getItem('admin_user');
  314. if (!user) return null;
  315. try {
  316. return JSON.parse(user).id;
  317. } catch {
  318. return null;
  319. }
  320. };
  321. // 정보수정 버튼 클릭
  322. const goToProfile = async () => {
  323. const adminId = getCurrentAdminId();
  324. if (!adminId) {
  325. showAlert('로그인 정보를 찾을 수 없습니다.', '오류');
  326. return;
  327. }
  328. // 현재 관리자 정보 조회
  329. const { data, error } = await get(`/admin/${adminId}`);
  330. if (error) {
  331. showAlert('관리자 정보를 불러올 수 없습니다.', '오류');
  332. console.error('[Profile] 관리자 정보 조회 실패:', error);
  333. return;
  334. }
  335. if (data?.success && data?.data) {
  336. currentAdmin.value = data.data;
  337. showProfileModal.value = true;
  338. }
  339. };
  340. // 정보수정 모달 닫기
  341. const closeProfileModal = () => {
  342. showProfileModal.value = false;
  343. currentAdmin.value = null;
  344. };
  345. // 정보수정 완료
  346. const handleProfileSaved = (message) => {
  347. closeProfileModal();
  348. if (message) {
  349. showAlert(message, '성공');
  350. // 로컬스토리지의 사용자 정보도 업데이트
  351. if (currentAdmin.value) {
  352. localStorage.setItem('admin_user', JSON.stringify(currentAdmin.value));
  353. }
  354. }
  355. };
  356. </script>