Преглед на файлове

관리자 고진 기준 기본세팅완료

송용우 преди 1 месец
родител
ревизия
fc69ea061e

+ 14 - 10
app/components/admin/AdminModal.vue

@@ -347,13 +347,15 @@ const save = async () => {
 }
 
 .admin--btn-small {
-  padding: 10px 20px;
-  border-radius: 4px;
-  cursor: pointer;
-  font-size: 14px;
-  transition: all 0.2s;
+  padding: 6px 14px;
   border: none;
+  border-radius: 4px;
+  font-size: 13px;
   font-weight: 500;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-family: 'AudiType', sans-serif;
+  white-space: nowrap;
 }
 
 .admin--btn-small:disabled {
@@ -362,18 +364,20 @@ const save = async () => {
 }
 
 .admin--btn-small-secondary {
-  background: #252525;
-  color: #e0e0e0;
-  border: 1px solid #404040;
+  background: var(--admin-bg-tertiary, #252525);
+  color: var(--admin-text-secondary, #e0e0e0);
+  border: 1px solid var(--admin-border-color, #404040);
 }
 
 .admin--btn-small-secondary:hover:not(:disabled) {
-  background: #2d2d2d;
+  background: var(--admin-bg-primary, #2d2d2d);
+  border-color: var(--admin-accent-primary, #217346);
+  color: var(--admin-text-primary, #ffffff);
 }
 
 .admin--btn-small-primary {
   background: var(--admin-accent-primary, #217346);
-  color: white;
+  color: var(--admin-text-primary, #ffffff);
 }
 
 .admin--btn-small-primary:hover:not(:disabled) {

+ 212 - 22
app/components/admin/PasswordModal.vue

@@ -1,34 +1,34 @@
 <template>
   <div class="admin--modal-overlay" @click.self="close">
-    <div class="admin--modal admin--modal-sm">
+    <div class="admin--modal">
       <div class="admin--modal-header">
         <h4>비밀번호 변경</h4>
         <button @click="close" class="admin--modal-close">&times;</button>
       </div>
 
-      <div class="admin--modal-body">
-        <div class="admin--info-box">
-          <p><strong>관리자:</strong> {{ admin.name }} ({{ admin.username }})</p>
-        </div>
+      <form @submit.prevent="save" class="admin--modal-form">
+        <div class="admin--modal-body">
+          <div class="admin--info-box">
+            <p><strong>관리자:</strong> {{ admin.name }} ({{ admin.username }})</p>
+          </div>
 
-        <form @submit.prevent="save">
           <div class="admin--form-group" v-if="isCurrentUser">
-            <label class="admin--label">현재 비밀번호</label>
+            <label class="admin--form-label">현재 비밀번호</label>
             <input
               v-model="formData.current_password"
               type="password"
-              class="admin--input"
+              class="admin--form-input"
               placeholder="현재 비밀번호 입력"
             />
             <small class="admin--help-text">본인 계정의 경우 현재 비밀번호를 입력해주세요.</small>
           </div>
 
           <div class="admin--form-group">
-            <label class="admin--label">새 비밀번호 *</label>
+            <label class="admin--form-label">새 비밀번호 *</label>
             <input
               v-model="formData.new_password"
               type="password"
-              class="admin--input"
+              class="admin--form-input"
               required
               placeholder="최소 8자 이상"
               minlength="8"
@@ -36,27 +36,27 @@
           </div>
 
           <div class="admin--form-group">
-            <label class="admin--label">새 비밀번호 확인 *</label>
+            <label class="admin--form-label">새 비밀번호 확인 *</label>
             <input
               v-model="formData.new_password_confirm"
               type="password"
-              class="admin--input"
+              class="admin--form-input"
               required
               placeholder="새 비밀번호 재입력"
               minlength="8"
             />
           </div>
+        </div>
 
-          <div class="admin--modal-footer">
-            <button type="button" @click="close" class="admin--btn-secondary">
-              취소
-            </button>
-            <button type="submit" class="admin--btn-primary" :disabled="saving">
-              {{ saving ? '변경 중...' : '변경' }}
-            </button>
-          </div>
-        </form>
-      </div>
+        <div class="admin--modal-footer">
+          <button type="button" @click="close" class="admin--btn-small admin--btn-small-secondary">
+            취소
+          </button>
+          <button type="submit" class="admin--btn-small admin--btn-small-primary" :disabled="saving">
+            {{ saving ? '변경 중...' : '변경' }}
+          </button>
+        </div>
+      </form>
     </div>
   </div>
 </template>
@@ -144,3 +144,193 @@ const save = async () => {
   }
 }
 </script>
+
+<style scoped>
+.admin--modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.7);
+  z-index: 9999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.admin--modal {
+  background: #2d2d2d;
+  padding: 0;
+  border-radius: 8px;
+  min-width: 500px;
+  max-width: 600px;
+  width: 90%;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.admin--modal-form {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+}
+
+.admin--modal-header {
+  padding: 20px;
+  border-bottom: 1px solid #404040;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.admin--modal-header h4 {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #ffffff;
+}
+
+.admin--modal-close {
+  background: none;
+  border: none;
+  font-size: 28px;
+  cursor: pointer;
+  color: #999;
+  line-height: 1;
+  transition: color 0.2s;
+  padding: 0;
+  width: 30px;
+  height: 30px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.admin--modal-close:hover {
+  color: #fff;
+}
+
+.admin--modal-body {
+  padding: 24px;
+  overflow-y: auto;
+  flex: 1;
+  min-height: 0;
+}
+
+.admin--info-box {
+  background: #1a1a1a;
+  border: 1px solid #404040;
+  border-radius: 4px;
+  padding: 12px 16px;
+  margin-bottom: 20px;
+}
+
+.admin--info-box p {
+  margin: 0;
+  color: #e0e0e0;
+  font-size: 14px;
+}
+
+.admin--info-box strong {
+  color: #ffffff;
+  font-weight: 600;
+}
+
+.admin--form-group {
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 20px;
+}
+
+.admin--form-group:last-child {
+  margin-bottom: 0;
+}
+
+.admin--form-label {
+  display: block;
+  margin-bottom: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #e0e0e0;
+}
+
+.admin--form-input {
+  width: 100%;
+  padding: 10px 14px;
+  border: 1px solid #404040;
+  border-radius: 4px;
+  background: #1a1a1a;
+  color: #ffffff;
+  font-size: 14px;
+  transition: border-color 0.2s;
+}
+
+.admin--form-input:focus {
+  outline: none;
+  border-color: var(--admin-accent-primary, #217346);
+}
+
+.admin--form-input::placeholder {
+  color: #666;
+}
+
+.admin--help-text {
+  display: block;
+  margin-top: 6px;
+  font-size: 12px;
+  color: #999;
+  line-height: 1.4;
+}
+
+.admin--modal-footer {
+  padding: 20px 24px;
+  border-top: 1px solid #404040;
+  display: flex;
+  gap: 10px;
+  justify-content: flex-end;
+  flex-shrink: 0;
+}
+
+.admin--btn-small {
+  padding: 6px 14px;
+  border: none;
+  border-radius: 4px;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-family: 'AudiType', sans-serif;
+  white-space: nowrap;
+}
+
+.admin--btn-small:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.admin--btn-small-secondary {
+  background: var(--admin-bg-tertiary, #252525);
+  color: var(--admin-text-secondary, #e0e0e0);
+  border: 1px solid var(--admin-border-color, #404040);
+}
+
+.admin--btn-small-secondary:hover:not(:disabled) {
+  background: var(--admin-bg-primary, #2d2d2d);
+  border-color: var(--admin-accent-primary, #217346);
+  color: var(--admin-text-primary, #ffffff);
+}
+
+.admin--btn-small-primary {
+  background: var(--admin-accent-primary, #217346);
+  color: var(--admin-text-primary, #ffffff);
+}
+
+.admin--btn-small-primary:hover:not(:disabled) {
+  background: var(--admin-accent-hover, #1a5c37);
+}
+</style>

+ 2 - 2
app/composables/useApi.js

@@ -94,9 +94,9 @@ apiClient.interceptors.response.use(
 
 export const useApi = () => {
   // GET 요청
-  const get = async (url, params = {}) => {
+  const get = async (url, config = {}) => {
     try {
-      const response = await apiClient.get(url, { params })
+      const response = await apiClient.get(url, config)
       // 전체 응답 반환: { success, data, message }
       return { data: response.data, error: null }
     } catch (error) {

+ 199 - 23
app/layouts/admin.vue

@@ -97,6 +97,25 @@
       @cancel="closeLogoutModal"
       @close="closeLogoutModal"
     />
+
+    <!-- 관리자 정보수정 모달 -->
+    <AdminModal
+      v-if="showProfileModal"
+      :admin="currentAdmin"
+      @close="closeProfileModal"
+      @saved="handleProfileSaved"
+    />
+
+    <!-- 알림 모달 -->
+    <AdminAlertModal
+      v-if="alertModal.show"
+      :title="alertModal.title"
+      :message="alertModal.message"
+      :type="alertModal.type"
+      @confirm="handleAlertConfirm"
+      @cancel="handleAlertCancel"
+      @close="closeAlertModal"
+    />
   </div>
 </template>
 
@@ -104,6 +123,7 @@
   import { ref, computed } from "vue";
   import { useRoute, useRouter } from "vue-router";
   import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
+  import AdminModal from "~/components/admin/AdminModal.vue";
 
   const route = useRoute();
   const router = useRouter();
@@ -117,46 +137,46 @@
       id: "basic",
       title: "기본정보관리",
       children: [
-        { title: "사이트 정보", path: "/admin/basic/site-info" },
-        { title: "팝업관리", path: "/admin/basic/popup" },
+        { title: "사이트 정보", path: "/admin/basic/site-info", pattern: /^\/admin\/basic\/site-info/ },
+        { title: "팝업관리", path: "/admin/basic/popup", pattern: /^\/admin\/basic\/popup/ },
       ],
     },
     {
       id: "branch",
       title: "지점장관리",
       children: [
-        { title: "지점목록", path: "/admin/branch/list" },
-        { title: "지점장목록", path: "/admin/branch/manager" },
+        { title: "지점목록", path: "/admin/branch/list", pattern: /^\/admin\/branch\/(list|create|edit)/ },
+        { title: "지점장목록", path: "/admin/branch/manager", pattern: /^\/admin\/branch\/manager/ },
       ],
     },
     {
       id: "staff",
       title: "사원관리",
       children: [
-        { title: "영업사원관리", path: "/admin/staff/sales" },
-        { title: "어드바이저등록", path: "/admin/staff/advisor" },
+        { title: "영업사원관리", path: "/admin/staff/sales", pattern: /^\/admin\/staff\/sales/ },
+        { title: "어드바이저등록", path: "/admin/staff/advisor", pattern: /^\/admin\/staff\/advisor/ },
       ],
     },
     // {
     //   id: 'service',
     //   title: '서비스관리',
     //   children: [
-    //     { title: '브로셔요청', path: '/admin/service/brochure' }
+    //     { title: '브로셔요청', path: '/admin/service/brochure', pattern: /^\/admin\/service\/brochure/ }
     //   ]
     // },
     {
       id: "board",
       title: "게시판관리",
       children: [
-        { title: "이벤트", path: "/admin/board/event" },
-        { title: "뉴스", path: "/admin/board/news" },
-        { title: "IR", path: "/admin/board/ir" },
+        { title: "이벤트", path: "/admin/board/event", pattern: /^\/admin\/board\/event/ },
+        { title: "뉴스", path: "/admin/board/news", pattern: /^\/admin\/board\/news/ },
+        { title: "IR", path: "/admin/board/ir", pattern: /^\/admin\/board\/ir/ },
       ],
     },
     {
       id: "system",
       title: "시스템관리",
-      children: [{ title: "관리자관리", path: "/admin/admins" }],
+      children: [{ title: "관리자관리", path: "/admin/admins", pattern: /^\/admin\/admins/ }],
     },
   ]);
 
@@ -175,25 +195,88 @@
     return route.path === path;
   };
 
+  // 현재 경로에 맞는 메뉴 찾기
+  const findCurrentMenu = () => {
+    const currentPath = route.path;
+
+    // 대시보드인 경우
+    if (currentPath === '/admin/dashboard' || currentPath === '/admin') {
+      return { menu: null, child: null };
+    }
+
+    for (const menu of menuItems.value) {
+      for (const child of menu.children) {
+        // pattern이 있으면 정규식으로 매칭, 없으면 정확히 일치하는지 확인
+        if (child.pattern && child.pattern.test(currentPath)) {
+          return { menu, child };
+        } else if (currentPath === child.path) {
+          return { menu, child };
+        }
+      }
+    }
+
+    return { menu: null, child: null };
+  };
+
   // 페이지 타이틀 계산
   const pageTitle = computed(() => {
-    for (const menu of menuItems.value) {
-      const found = menu.children.find((child) => child.path === route.path);
-      if (found) return found.title;
+    const currentPath = route.path;
+
+    // 대시보드
+    if (currentPath === '/admin/dashboard' || currentPath === '/admin') {
+      return "대시보드";
     }
+
+    const { child } = findCurrentMenu();
+
+    if (child) {
+      // 상세 페이지 타이틀 처리
+      if (currentPath.includes('/create')) {
+        return `${child.title} 등록`;
+      } else if (currentPath.includes('/edit/')) {
+        return `${child.title} 수정`;
+      } else if (currentPath.includes('/print/')) {
+        return `${child.title} 인쇄`;
+      } else if (currentPath.includes('/print-a2')) {
+        return `${child.title} 인쇄 (A2)`;
+      }
+      return child.title;
+    }
+
     return "대시보드";
   });
 
   // Breadcrumb 계산
   const breadcrumbs = computed(() => {
+    const currentPath = route.path;
     const crumbs = [{ title: "Home", path: "/admin/dashboard" }];
 
-    for (const menu of menuItems.value) {
-      const found = menu.children.find((child) => child.path === route.path);
-      if (found) {
-        crumbs.push({ title: menu.title, path: null });
-        crumbs.push({ title: found.title, path: null });
-        break;
+    // 대시보드인 경우 Home만 표시
+    if (currentPath === '/admin/dashboard' || currentPath === '/admin') {
+      return crumbs;
+    }
+
+    const { menu, child } = findCurrentMenu();
+
+    if (menu && child) {
+      // 메뉴 그룹 추가
+      crumbs.push({ title: menu.title, path: null });
+
+      // 현재 페이지 타이틀 추가
+      if (currentPath.includes('/create')) {
+        crumbs.push({ title: child.title, path: child.path });
+        crumbs.push({ title: "등록", path: null });
+      } else if (currentPath.includes('/edit/')) {
+        crumbs.push({ title: child.title, path: child.path });
+        crumbs.push({ title: "수정", path: null });
+      } else if (currentPath.includes('/print/')) {
+        crumbs.push({ title: child.title, path: child.path });
+        crumbs.push({ title: "인쇄", path: null });
+      } else if (currentPath.includes('/print-a2')) {
+        crumbs.push({ title: child.title, path: child.path });
+        crumbs.push({ title: "인쇄 (A2)", path: null });
+      } else {
+        crumbs.push({ title: child.title, path: null });
       }
     }
 
@@ -221,8 +304,101 @@
     showLogoutModal.value = false;
   };
 
-  // 정보수정
-  const goToProfile = () => {
-    router.push("/admin/profile");
+  // 정보수정 모달
+  const showProfileModal = ref(false);
+  const currentAdmin = ref(null);
+
+  const { get } = useApi();
+
+  // 알림 모달
+  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 closeAlertModal = () => {
+    alertModal.value.show = false;
+  };
+
+  // 알림 모달 확인
+  const handleAlertConfirm = () => {
+    if (alertModal.value.onConfirm) {
+      alertModal.value.onConfirm();
+    }
+    closeAlertModal();
+  };
+
+  // 알림 모달 취소
+  const handleAlertCancel = () => {
+    closeAlertModal();
+  };
+
+  // 현재 로그인한 관리자 ID 가져오기
+  const getCurrentAdminId = () => {
+    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 goToProfile = async () => {
+    const adminId = getCurrentAdminId();
+    if (!adminId) {
+      showAlert('로그인 정보를 찾을 수 없습니다.', '오류');
+      return;
+    }
+
+    // 현재 관리자 정보 조회
+    const { data, error } = await get(`/admin/${adminId}`);
+
+    if (error) {
+      showAlert('관리자 정보를 불러올 수 없습니다.', '오류');
+      console.error('[Profile] 관리자 정보 조회 실패:', error);
+      return;
+    }
+
+    if (data?.success && data?.data) {
+      currentAdmin.value = data.data;
+      showProfileModal.value = true;
+    }
+  };
+
+  // 정보수정 모달 닫기
+  const closeProfileModal = () => {
+    showProfileModal.value = false;
+    currentAdmin.value = null;
+  };
+
+  // 정보수정 완료
+  const handleProfileSaved = (message) => {
+    closeProfileModal();
+    if (message) {
+      showAlert(message, '성공');
+
+      // 로컬스토리지의 사용자 정보도 업데이트
+      if (currentAdmin.value) {
+        localStorage.setItem('admin_user', JSON.stringify(currentAdmin.value));
+      }
+    }
   };
 </script>

+ 4 - 4
app/pages/admin/admins/index.vue

@@ -75,16 +75,16 @@
             </td>
             <td>{{ formatDate(admin.created_at) }}</td>
             <td>
-              <div class="admin--table-actions">
-                <button @click="openEditModal(admin)" class="admin--btn-sm admin--btn-edit">
+              <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-sm admin--btn-warning">
+                <button @click="openPasswordModal(admin)" class="admin--btn-small admin--btn-small-secondary">
                   비밀번호
                 </button>
                 <button
                   @click="confirmDeleteAdmin(admin)"
-                  class="admin--btn-sm admin--btn-delete"
+                  class="admin--btn-small admin--btn-small-danger"
                   :disabled="admin.id === currentAdminId"
                 >
                   삭제

+ 4 - 6
app/pages/admin/basic/popup/edit/[id].vue

@@ -294,12 +294,10 @@ const loadPopup = async () => {
       link_url: popup.link_url || ''
     }
 
-    // 이미지 미리보기 설정
-    if (popup.type === 'image' && popup.image_url) {
-      imagePreview.value = popup.image_url
-      console.log('[Popup Edit] 이미지 URL:', popup.image_url)
-      console.log('[Popup Edit] 이미지 미리보기 설정:', imagePreview.value)
-    }
+    // 이미지 미리보기는 새 이미지 업로드시에만 사용
+    // 기존 이미지는 formData.image_url을 통해 getImageUrl()로 표시
+    imagePreview.value = null
+    console.log('[Popup Edit] 이미지 URL:', popup.image_url)
 
     console.log('[Popup Edit] 데이터 로드 성공:', formData.value)
   } else {

+ 2 - 1
app/pages/admin/staff/sales/edit/[id].vue

@@ -275,7 +275,8 @@ const loadSales = async () => {
       is_top30: sales.is_top30 || false,
       display_order: sales.display_order || 0
     }
-    photoPreview.value = sales.photo_url || null
+    // photoPreview는 새 이미지 업로드시에만 사용
+    photoPreview.value = null
     console.log('[SalesEdit] 로드 성공')
   }