Kaynağa Gözat

메뉴 정리, 프로젝트 개발내역 추가, 반응형 서비스

DESKTOP-T61HUSC\user 2 hafta önce
ebeveyn
işleme
534977d754
31 değiştirilmiş dosya ile 399 ekleme ve 7526 silme
  1. 296 12
      app/assets/scss/admin.scss
  2. 2 0
      app/components/admin/DatePicker.vue
  3. 17 1
      app/layouts/admin.vue
  4. 0 310
      app/pages/site-manager/board/event/create.vue
  5. 0 381
      app/pages/site-manager/board/event/edit/[id].vue
  6. 0 221
      app/pages/site-manager/board/event/index.vue
  7. 0 251
      app/pages/site-manager/board/ir/create.vue
  8. 0 307
      app/pages/site-manager/board/ir/edit/[id].vue
  9. 0 221
      app/pages/site-manager/board/ir/index.vue
  10. 0 261
      app/pages/site-manager/board/news/create.vue
  11. 0 317
      app/pages/site-manager/board/news/edit/[id].vue
  12. 0 221
      app/pages/site-manager/board/news/index.vue
  13. 0 276
      app/pages/site-manager/board/notice/create.vue
  14. 0 340
      app/pages/site-manager/board/notice/edit/[id].vue
  15. 0 225
      app/pages/site-manager/board/notice/index.vue
  16. 1 1
      app/pages/site-manager/fishing/list.vue
  17. 1 1
      app/pages/site-manager/onboard/list.vue
  18. 0 293
      app/pages/site-manager/service-center/create.vue
  19. 0 327
      app/pages/site-manager/service-center/edit/[id].vue
  20. 0 337
      app/pages/site-manager/service-center/list.vue
  21. 0 234
      app/pages/site-manager/service/brochure.vue
  22. 1 1
      app/pages/site-manager/species_quest/list.vue
  23. 0 219
      app/pages/site-manager/staff/advisor/create.vue
  24. 0 254
      app/pages/site-manager/staff/advisor/edit/[id].vue
  25. 0 252
      app/pages/site-manager/staff/advisor/index.vue
  26. 0 430
      app/pages/site-manager/staff/sales/create.vue
  27. 0 440
      app/pages/site-manager/staff/sales/edit/[id].vue
  28. 0 798
      app/pages/site-manager/staff/sales/index.vue
  29. 0 300
      app/pages/site-manager/staff/sales/print-a2.vue
  30. 0 287
      app/pages/site-manager/staff/sales/print/[id].vue
  31. 81 8
      info.md

+ 296 - 12
app/assets/scss/admin.scss

@@ -1652,6 +1652,7 @@ footer {
   bottom: 0;
   overflow-y: auto;
   background-color: var(--admin-bg-navy);
+  z-index: 10;
   padding: 24px 16px;
   .admin--brand{
     display: flex;
@@ -2339,6 +2340,7 @@ footer {
   .admin--form-input,
   .admin--search-input {
     border: 1px solid #e8eaef !important;
+    min-width: 140px;
     border-radius: 4px;
     padding: 12px;
     font-size: 13px !important;
@@ -3077,20 +3079,10 @@ footer {
   }
 }
 
-// Responsive
-@media (max-width: 1024px) {
-  .admin--sidebar {
-    width: 220px;
-  }
-
-  .admin--main {
-    margin-left: 220px;
-  }
-}
-
 @media (max-width: 768px) {
   .admin--header {
     padding: 0 16px;
+    width: 100%;
   }
 
   .admin--sidebar {
@@ -3116,7 +3108,8 @@ footer {
   .admin--form {
     .admin--form-actions {
       flex-direction: column;
-
+      border-top: none;
+      margin-top: 0;
       .admin--btn {
         width: 100%;
       }
@@ -3131,6 +3124,31 @@ footer {
         }
       }
     }
+
+    .admin--form--table{
+      tr{
+        &:first-child{
+          border-top: none;
+          th{
+            padding-top: 0;
+          }
+        }
+        &:last-child{
+          border-bottom: none;
+        }
+      }
+      th{
+        padding: 24px 0 0 0;
+      }
+      td{
+        padding: 12px 0 24px 0;
+      }
+    }
+  }
+  .admin--search-box{
+    flex-direction: column;
+    flex-wrap: wrap;
+    gap: 12px;
   }
 }
 
@@ -8183,3 +8201,269 @@ footer {
     }
   }
 }
+
+
+// ===========================================
+// 반응형 정책
+//   * 1601px 이상: 자연스러운 레이아웃
+//   * 769px ~ 1600px: 1600px 고정 → 가로 스크롤
+//   * 768px 이하: 모바일 (사이드바 햄버거 토글)
+// ===========================================
+
+// 햄버거 메뉴 토글 (기본 데스크탑에선 숨김)
+.admin--mobile-toggle {
+  display: none;
+  background: transparent;
+  border: none;
+  font-size: 22px;
+  cursor: pointer;
+  color: var(--admin-text-primary);
+  padding: 6px 10px;
+  margin-right: 6px;
+  line-height: 1;
+}
+
+// 모바일 사이드바 백드롭 (기본 숨김)
+.admin--sidebar-backdrop {
+  display: none;
+}
+
+// 1600px 이하 — 가로 스크롤 (모바일 영역 제외)
+@media (max-width: 1600px) and (min-width: 769px) {
+  body {
+    min-width: 1600px;
+    overflow-x: auto;
+  }
+  .admin--header{
+    position: absolute;
+    width: 1360px;
+  }
+  .admin--sidebar{
+    position: absolute;
+    height: 100vh;
+  }
+}
+
+// 768px 이하 — 모바일
+@media (max-width: 768px) {
+  body {
+    min-width: 0;
+    overflow-x: hidden;
+  }
+
+
+
+  .admin--layout {
+    min-width: 0;
+  }
+
+  .admin--mobile-toggle {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  // 헤더 — 사이드바 너비 무시, 풀폭
+  .admin--header {
+    left: 0 !important;
+    padding: 0 12px;
+    height: 56px;
+
+    .admin--header-left {
+      display: flex;
+      align-items: center;
+      flex-direction: row;
+      gap: 4px;
+
+      h1 {
+        font-size: 16px;
+      }
+    }
+
+    .admin--breadcrumb {
+      display: none;
+    }
+  }
+
+  // 사이드바 — 슬라이드 인 패널
+  .admin--sidebar {
+    position: fixed !important;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    width: 260px;
+    transform: translateX(-100%);
+    transition: transform 0.25s ease;
+    z-index: 9001;
+    box-shadow: 4px 0 24px rgba(0, 0, 0, 0.2);
+
+    &.is-open {
+      transform: translateX(0);
+    }
+  }
+
+  // 백드롭 (사이드바 열렸을 때 본문 클릭으로 닫기)
+  .admin--sidebar-backdrop {
+    display: block;
+    position: fixed;
+    inset: 0;
+    background: rgba(0, 0, 0, 0.45);
+    z-index: 9000;
+  }
+
+  .admin--main {
+    margin-left: 0 !important;
+    padding: 16px 12px 80px 12px;
+    min-width: 0; // flex item이 자식 따라 늘어나지 않게 → wrapper의 overflow-x가 동작
+  }
+
+  .admin--search-box{
+    .admin--search-form{
+      .admin--filter-radio{
+        width: 100%;
+        button{
+          flex: 1;
+          width: calc(100% / 4);
+          min-width: 0;
+        }
+      }
+    }
+  }
+
+  // 검색/액션 줄바꿈
+  .admin--search-form,
+  .admin--search-actions,
+  .admin--search--inner--box {
+    flex-wrap: wrap;
+    gap: 6px;
+    width: 100%;
+  }
+  .admin--search--inner--box{
+    flex-direction: column-reverse;
+    flex-wrap: wrap;
+    gap: 12px!important;
+    align-items: start!important;
+  }
+  .admin--datepicker{
+    flex: 1;
+    input:read-only{
+      min-width: 0;
+      width: 100%;
+    }
+  }
+
+  .admin--quick-range{
+    width: 100%;
+    .range--btn{
+      min-width: 0!important;
+      width: calc(100% / 6)!important;
+      flex: 1;
+    }
+  }
+
+  // 테이블 — 가로 스크롤
+  .admin--table-wrapper {
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+  .admin--table {
+    min-width: 900px;
+    &.fishing--table{
+      min-width: 1200px;
+    }
+    &.fish--table{
+      min-width: 1200px;
+      tbody{
+        tr{
+          td{
+            padding: 14px 4px!important;
+            &:first-child{
+              padding-left: 8px!important;
+            }
+            &:last-child{
+              padding-right: 8px!important;
+            }
+            .admin--range-cell{
+              gap: 4px;
+            }
+          }
+          &.admin--table-row-new{
+            .admin--range-cell{
+              gap: 4px;
+            }
+            td{
+              padding: 14px 4px!important;
+              &:first-child{
+                padding-left: 8px!important;
+              }
+              &:last-child{
+                padding-right: 8px!important;
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // 폼 테이블 — 셀 stack
+  .admin--form--table {
+    th,
+    td {
+      display: block;
+      width: 100% !important;
+      box-sizing: border-box;
+    }
+    th {
+      padding-bottom: 4px;
+      background: transparent;
+    }
+    td {
+      min-width: 0;          // flex 자식이 부모 넘지 않게
+      overflow: hidden;
+
+      .input--wrap {
+        flex-wrap: wrap;
+        gap: 6px;
+        width: 100%;
+
+        input,
+        select,
+        textarea,
+        .admin--form-input,
+        .admin--form-select,
+        .admin--form-textarea {
+          width: 100%;
+          max-width: 100%;
+          box-sizing: border-box;
+        }
+      }
+
+      // 고정 width 클래스(w--280, w--160 등) 무력화 — 모바일에선 풀폭
+      .w--80,
+      .w--100,
+      .w--120,
+      .w--140,
+      .w--160,
+      .w--200,
+      .w--280 {
+        width: 100% !important;
+        max-width: 100%;
+        flex: 1 1 100%;
+      }
+    }
+  }
+
+  // 모달 가로폭
+  .admin--alert-modal {
+    min-width: 0 !important;
+    max-width: calc(100vw - 32px) !important;
+    margin: 0 16px;
+  }
+
+  // 페이지네이션
+  .admin--pagination {
+    flex-wrap: wrap;
+    justify-content: center;
+  }
+}

+ 2 - 0
app/components/admin/DatePicker.vue

@@ -59,6 +59,8 @@ onMounted(() => {
     minDate: props.minDate,
     maxDate: props.maxDate,
     defaultDate: props.modelValue || null,
+    // 모바일에서 native date input으로 전환 차단 (placeholder가 "연도-월-일" 으로 깨지는 원인)
+    disableMobile: true,
     onChange: (_selectedDates, dateStr) => {
       emit('update:modelValue', dateStr)
     },

+ 17 - 1
app/layouts/admin.vue

@@ -10,6 +10,11 @@
   // 메뉴 열림 상태 관리
   const openMenus = ref(["dashboard"]);
 
+  // 모바일 사이드바 토글
+  const isSidebarOpen = ref(false);
+  const toggleSidebar = () => { isSidebarOpen.value = !isSidebarOpen.value; };
+  const closeSidebar = () => { isSidebarOpen.value = false; };
+
   // 로그인한 관리자 정보 (권한 분기용 — 모달용 currentAdmin과 별개)
   const authUser = ref(null);
   const loadAuthUser = () => {
@@ -185,6 +190,7 @@
     () => {
       const { menu } = findCurrentMenu();
       openMenus.value = menu?.children ? [menu.id] : [];
+      closeSidebar();   // 모바일에서 라우트 이동 시 사이드바 자동 닫힘
     },
     { immediate: true }
   );
@@ -392,6 +398,9 @@
     <!-- Header -->
     <header class="admin--header">
       <div class="admin--header-left">
+        <button type="button" class="admin--mobile-toggle" @click="toggleSidebar" aria-label="메뉴 열기">
+          ☰
+        </button>
         <div class="admin--page-header">
           <div class="admin--breadcrumb">
             <span v-for="(crumb, index) in breadcrumbs" :key="index">
@@ -423,8 +432,15 @@
 
     <!-- Main Content Area -->
     <div class="admin--content--wrapper">
+      <!-- 모바일 백드롭 (사이드바 열렸을 때만 노출) -->
+      <div
+        v-if="isSidebarOpen"
+        class="admin--sidebar-backdrop"
+        @click="closeSidebar"
+      ></div>
+
       <!-- Sidebar GNB -->
-      <aside class="admin--sidebar">
+      <aside class="admin--sidebar" :class="{ 'is-open': isSidebarOpen }">
         <NuxtLink to="/site-manager" class="admin--brand" @click="goToDashboard">
           <div class="brand--logo">🏴‍☠️</div>
           <div class="brand--title">

+ 0 - 310
app/pages/site-manager/board/event/create.vue

@@ -1,310 +0,0 @@
-<template>
-  <div class="admin--event-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 사이트 선택 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사이트 <span class="admin--required">*</span></label>
-        <select v-model="formData.site" class="admin--form-select" required>
-          <option value="common">공통</option>
-          <option value="ford">포드</option>
-          <option value="lincoln">링컨</option>
-        </select>
-      </div>
-
-      <!-- 카테고리 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">카테고리 <span class="admin--required">*</span></label>
-        <select v-model="formData.category" class="admin--form-select" required>
-          <option value="">카테고리를 선택하세요</option>
-          <option value="진행중">진행중</option>
-          <option value="완료">완료</option>
-        </select>
-      </div>
-
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox">
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox">
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 기간 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">기간 <span class="admin--required">*</span></label>
-        <div class="admin--date-range">
-          <DatePicker
-            v-model="formData.start_date"
-            placeholder="시작일 선택"
-            :max-date="formData.end_date"
-            required
-          />
-          <span class="admin--date-separator">~</span>
-          <DatePicker
-            v-model="formData.end_date"
-            placeholder="종료일 선택"
-            :min-date="formData.start_date"
-            required
-          />
-        </div>
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">내용 <span class="admin--required">*</span></label>
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div class="admin--file-list">
-          <div v-for="(file, index) in attachedFiles" :key="index" class="admin--file-item">
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        >
-        <button type="button" class="admin--btn admin--btn-secondary" @click="triggerFileInput">
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-import { useRouter } from 'vue-router'
-import SunEditor from '~/components/admin/SunEditor.vue'
-import DatePicker from '~/components/admin/DatePicker.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { post, upload } = useApi()
-
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const attachedFiles = ref([])
-const fileInput = ref(null)
-
-const formData = ref({
-  site: 'common',
-  category: '',
-  allow_comment: false,
-  is_notice: false,
-  name: '고진',
-  email: 'admin@admin.kr',
-  start_date: '',
-  end_date: '',
-  title: '',
-  content: '',
-  file_urls: []
-})
-
-const triggerFileInput = () => {
-  fileInput.value?.click()
-}
-
-const handleFileAdd = (event) => {
-  const files = Array.from(event.target.files)
-  attachedFiles.value.push(...files)
-  event.target.value = ''
-}
-
-const removeFile = (index) => {
-  attachedFiles.value.splice(index, 1)
-}
-
-const formatFileSize = (bytes) => {
-  if (bytes === 0) return '0 Bytes'
-  const k = 1024
-  const sizes = ['Bytes', 'KB', 'MB', 'GB']
-  const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.category) {
-    errorMessage.value = '카테고리를 선택하세요.'
-    return
-  }
-
-  if (!formData.value.title) {
-    errorMessage.value = '제목을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.content) {
-    errorMessage.value = '내용을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.start_date || !formData.value.end_date) {
-    errorMessage.value = '기간을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let fileUrls = []
-
-    // 파일 업로드
-    if (attachedFiles.value.length > 0) {
-      for (const file of attachedFiles.value) {
-        const formDataFile = new FormData()
-        formDataFile.append('file', file)
-
-        const { data: uploadData, error: uploadError } = await upload('/upload/event-file', formDataFile)
-
-        if (uploadError) {
-          errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`
-          isSaving.value = false
-          return
-        }
-
-        if (!uploadData?.success || !uploadData?.data?.url) {
-          errorMessage.value = '파일 업로드 응답이 올바르지 않습니다.'
-          isSaving.value = false
-          return
-        }
-
-        fileUrls.push({
-          name: file.name,
-          url: uploadData.data.url,
-          size: file.size
-        })
-      }
-    }
-
-    // content에서 도메인 제거
-    let contentToSave = formData.value.content
-    if (contentToSave) {
-      // http://도메인 또는 https://도메인 제거
-      contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, '')
-    }
-
-    const submitData = {
-      ...formData.value,
-      allow_comment: formData.value.allow_comment ? 1 : 0,
-      is_notice: formData.value.is_notice ? 1 : 0,
-      content: contentToSave,
-      file_urls: fileUrls
-    }
-
-    const { data, error } = await post('/board/event', submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '등록에 실패했습니다.'
-    } else {
-      successMessage.value = '이벤트가 등록되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/board/event')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/board/event')
-}
-</script>

+ 0 - 381
app/pages/site-manager/board/event/edit/[id].vue

@@ -1,381 +0,0 @@
-<template>
-  <div class="admin--event-form">
-    <div v-if="isLoading" class="admin--loading">
-      데이터를 불러오는 중...
-    </div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 사이트 선택 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사이트 <span class="admin--required">*</span></label>
-        <select v-model="formData.site" class="admin--form-select" required>
-          <option value="common">공통</option>
-          <option value="ford">포드</option>
-          <option value="lincoln">링컨</option>
-        </select>
-      </div>
-
-      <!-- 카테고리 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">카테고리 <span class="admin--required">*</span></label>
-        <select v-model="formData.category" class="admin--form-select" required>
-          <option value="">카테고리를 선택하세요</option>
-          <option value="진행중">진행중</option>
-          <option value="완료">완료</option>
-        </select>
-      </div>
-
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox">
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox">
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 기간 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">기간 <span class="admin--required">*</span></label>
-        <div class="admin--date-range">
-          <DatePicker
-            v-model="formData.start_date"
-            placeholder="시작일 선택"
-            :max-date="formData.end_date"
-            required
-          />
-          <span class="admin--date-separator">~</span>
-          <DatePicker
-            v-model="formData.end_date"
-            placeholder="종료일 선택"
-            :min-date="formData.start_date"
-            required
-          />
-        </div>
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">내용 <span class="admin--required">*</span></label>
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 기존 첨부파일 -->
-      <div v-if="existingFiles.length > 0" class="admin--form-group">
-        <label class="admin--form-label">기존 첨부파일</label>
-        <div class="admin--file-list">
-          <div v-for="(file, index) in existingFiles" :key="'existing-' + index" class="admin--file-item">
-            <a :href="file.url" target="_blank" class="admin--file-name">{{ file.name }}</a>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeExistingFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-      </div>
-
-      <!-- 새 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div v-if="attachedFiles.length > 0" class="admin--file-list">
-          <div v-for="(file, index) in attachedFiles" :key="'new-' + index" class="admin--file-item">
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        >
-        <button type="button" class="admin--btn admin--btn-secondary" @click="triggerFileInput">
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import SunEditor from '~/components/admin/SunEditor.vue'
-import DatePicker from '~/components/admin/DatePicker.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const route = useRoute()
-const router = useRouter()
-const { get, put, upload } = useApi()
-const { getImageUrl } = useImage()
-
-const isLoading = ref(true)
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const attachedFiles = ref([])
-const existingFiles = ref([])
-const fileInput = ref(null)
-
-const formData = ref({
-  site: 'common',
-  category: '',
-  allow_comment: false,
-  is_notice: false,
-  name: '',
-  email: '',
-  start_date: '',
-  end_date: '',
-  title: '',
-  content: '',
-  file_urls: []
-})
-
-const loadEvent = async () => {
-  isLoading.value = true
-
-  const id = route.params.id
-  const { data, error } = await get(`/board/event/${id}`)
-  console.log('[EventEdit] 데이터 로드:', { data, error })
-
-  if (data?.success && data?.data) {
-    const event = data.data
-
-    // content 내 상대 경로 이미지를 절대 URL로 변환
-    let content = event.content || ''
-    if (content) {
-      // src="/uploads/..." 또는 src='/uploads/...' 패턴을 절대 URL로 변환
-      content = content.replace(/src=["'](?!https?:\/\/)([^"']+)["']/g, (match, path) => {
-        const fullUrl = getImageUrl(path)
-        return `src="${fullUrl}"`
-      })
-    }
-
-    formData.value = {
-      site: event.site || 'common',
-      category: event.category || '',
-      allow_comment: event.allow_comment === 1 || event.allow_comment === '1',
-      is_notice: event.is_notice === 1 || event.is_notice === '1',
-      name: event.name || '',
-      email: event.email || '',
-      start_date: event.start_date || '',
-      end_date: event.end_date || '',
-      title: event.title || '',
-      content: content,
-      file_urls: event.file_urls || []
-    }
-    existingFiles.value = event.file_urls || []
-    console.log('[EventEdit] 로드 성공')
-  }
-
-  isLoading.value = false
-}
-
-const triggerFileInput = () => {
-  fileInput.value?.click()
-}
-
-const handleFileAdd = (event) => {
-  const files = Array.from(event.target.files)
-  attachedFiles.value.push(...files)
-  event.target.value = ''
-}
-
-const removeFile = (index) => {
-  attachedFiles.value.splice(index, 1)
-}
-
-const removeExistingFile = (index) => {
-  existingFiles.value.splice(index, 1)
-}
-
-const formatFileSize = (bytes) => {
-  if (bytes === 0) return '0 Bytes'
-  const k = 1024
-  const sizes = ['Bytes', 'KB', 'MB', 'GB']
-  const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.category) {
-    errorMessage.value = '카테고리를 선택하세요.'
-    return
-  }
-
-  if (!formData.value.title) {
-    errorMessage.value = '제목을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.content) {
-    errorMessage.value = '내용을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.start_date || !formData.value.end_date) {
-    errorMessage.value = '기간을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let fileUrls = [...existingFiles.value]
-
-    // 새 파일 업로드
-    if (attachedFiles.value.length > 0) {
-      for (const file of attachedFiles.value) {
-        const formDataFile = new FormData()
-        formDataFile.append('file', file)
-
-        const { data: uploadData, error: uploadError } = await upload('/upload/event-file', formDataFile)
-
-        if (uploadError) {
-          errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`
-          isSaving.value = false
-          return
-        }
-
-        if (!uploadData?.success || !uploadData?.data?.url) {
-          errorMessage.value = '파일 업로드 응답이 올바르지 않습니다.'
-          isSaving.value = false
-          return
-        }
-
-        fileUrls.push({
-          name: file.name,
-          url: uploadData.data.url,
-          size: file.size
-        })
-      }
-    }
-
-    // content에서 도메인 제거
-    let contentToSave = formData.value.content
-    if (contentToSave) {
-      // http://도메인 또는 https://도메인 제거
-      contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, '')
-    }
-
-    const submitData = {
-      ...formData.value,
-      allow_comment: formData.value.allow_comment ? 1 : 0,
-      is_notice: formData.value.is_notice ? 1 : 0,
-      content: contentToSave,
-      file_urls: fileUrls
-    }
-
-    const id = route.params.id
-    const { data, error } = await put(`/board/event/${id}`, submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
-    } else {
-      successMessage.value = '이벤트가 수정되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/board/event')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/board/event')
-}
-
-onMounted(() => {
-  loadEvent()
-})
-</script>

+ 0 - 221
app/pages/site-manager/board/event/index.vue

@@ -1,221 +0,0 @@
-<template>
-  <div class="admin--board-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <select v-model="searchType" class="admin--form-select admin--search-select">
-          <option value="title">제목</option>
-          <option value="name">이름</option>
-          <option value="content">내용</option>
-        </select>
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="검색어를 입력하세요"
-          @keyup.enter="handleSearch"
-        />
-        <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">검색</button>
-        <button class="admin--btn-small admin--btn-small-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">+ 등록</button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>제목</th>
-            <th>이름</th>
-            <th>등록일</th>
-            <th>조회</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!posts || posts.length === 0">
-            <td colspan="6" class="admin--table-empty">등록된 게시물이 없습니다.</td>
-          </tr>
-          <tr v-else v-for="(post, index) in posts" :key="post.id">
-            <td>
-              {{
-                (post.is_notice === 1 || post.is_notice === '1')
-                  ? "공지"
-                  : totalCount - ((currentPage - 1) * perPage + index)
-              }}
-            </td>
-            <td class="admin--table-title">{{ post.title }}</td>
-            <td>{{ post.name }}</td>
-            <td>{{ formatDate(post.created_at) }}</td>
-            <td>{{ post.views }}</td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(post.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(post.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-  import { ref, computed, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({ layout: "admin", middleware: ["auth"] });
-
-  const router = useRouter();
-  const { get, del } = useApi();
-  const posts = ref([]);
-  const searchType = ref("title");
-  const searchKeyword = ref("");
-  const currentPage = ref(1);
-  const perPage = ref(10);
-  const totalCount = ref(0);
-  const totalPages = ref(0);
-
-  const visiblePages = computed(() => {
-    const pages = [];
-    const maxVisible = 5;
-    let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
-    let end = Math.min(totalPages.value, start + maxVisible - 1);
-    if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
-    for (let i = start; i <= end; i++) pages.push(i);
-    return pages;
-  });
-
-  const loadPosts = async () => {
-    const params = { page: currentPage.value, per_page: perPage.value };
-    if (searchKeyword.value) {
-      params.search_type = searchType.value;
-      params.search_keyword = searchKeyword.value;
-    }
-    const { data, error } = await get("/board/event", { params });
-
-    console.log("[EventBoard] API 응답:", { data, error });
-
-    // API 응답: { success: true, data: { items, total }, message }
-    if (data?.success && data?.data) {
-      posts.value = data.data.items || [];
-      totalCount.value = data.data.total || 0;
-      totalPages.value = Math.ceil(totalCount.value / perPage.value);
-      console.log("[EventBoard] 로드 성공:", posts.value.length);
-    }
-  };
-
-  const handleSearch = () => {
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const handleReset = () => {
-    searchType.value = "title";
-    searchKeyword.value = "";
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const changePage = (page) => {
-    if (page < 1 || page > totalPages.value) return;
-    currentPage.value = page;
-    loadPosts();
-    window.scrollTo({ top: 0, behavior: 'smooth' });
-  };
-  const goToCreate = () => router.push("/site-manager/board/event/create");
-  const goToEdit = (id) => router.push(`/site-manager/board/event/edit/${id}`);
-  const handleDelete = async (id) => {
-    if (!confirm("정말 삭제하시겠습니까?")) return;
-    const { error } = await del(`/board/event/${id}`);
-    if (error) alert("삭제에 실패했습니다.");
-    else {
-      alert("삭제되었습니다.");
-      loadPosts();
-    }
-  };
-  const formatDate = (dateString) => {
-    if (!dateString) return "-";
-    const date = new Date(dateString);
-    return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(
-      2,
-      "0"
-    )}.${String(date.getDate()).padStart(2, "0")}`;
-  };
-
-  onMounted(() => loadPosts());
-</script>
-
-<style scoped>
-  /* 검색 영역 input/select 스타일 통일 */
-  .admin--search-box .admin--form-input,
-  .admin--search-box .admin--search-input,
-  .admin--search-box .admin--form-select,
-  .admin--search-box .admin--search-select {
-    border: 1px solid var(--admin-border-color) !important;
-    border-radius: 4px;
-    height: 33px !important;
-    padding: 6px 14px !important;
-    font-size: 13px !important;
-  }
-</style>

+ 0 - 251
app/pages/site-manager/board/ir/create.vue

@@ -1,251 +0,0 @@
-<template>
-  <div class="admin--ir-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox">
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox">
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- URL -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">URL</label>
-        <input
-          v-model="formData.url"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com"
-        >
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">내용 <span class="admin--required">*</span></label>
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div class="admin--file-list">
-          <div v-for="(file, index) in attachedFiles" :key="index" class="admin--file-item">
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        >
-        <button type="button" class="admin--btn admin--btn-secondary" @click="triggerFileInput">
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-import { useRouter } from 'vue-router'
-import SunEditor from '~/components/admin/SunEditor.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { post, upload } = useApi()
-
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const attachedFiles = ref([])
-const fileInput = ref(null)
-
-const formData = ref({
-  allow_comment: false,
-  is_notice: false,
-  name: '고진',
-  email: 'admin@admin.kr',
-  url: '',
-  title: '',
-  content: '',
-  file_urls: []
-})
-
-const triggerFileInput = () => {
-  fileInput.value?.click()
-}
-
-const handleFileAdd = (event) => {
-  const files = Array.from(event.target.files)
-  attachedFiles.value.push(...files)
-  event.target.value = ''
-}
-
-const removeFile = (index) => {
-  attachedFiles.value.splice(index, 1)
-}
-
-const formatFileSize = (bytes) => {
-  if (bytes === 0) return '0 Bytes'
-  const k = 1024
-  const sizes = ['Bytes', 'KB', 'MB', 'GB']
-  const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.title) {
-    errorMessage.value = '제목을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.content) {
-    errorMessage.value = '내용을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let fileUrls = []
-
-    // 파일 업로드
-    if (attachedFiles.value.length > 0) {
-      for (const file of attachedFiles.value) {
-        const formDataFile = new FormData()
-        formDataFile.append('file', file)
-
-        const { data: uploadData, error: uploadError } = await upload('/upload/file', formDataFile)
-
-        if (uploadError) {
-          errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`
-          isSaving.value = false
-          return
-        }
-
-        fileUrls.push({
-          name: file.name,
-          url: uploadData.url,
-          size: file.size
-        })
-      }
-    }
-
-    const submitData = {
-      ...formData.value,
-      file_urls: fileUrls
-    }
-
-    const { data, error } = await post('/board/ir', submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '등록에 실패했습니다.'
-    } else {
-      successMessage.value = 'IR 자료가 등록되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/board/ir')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/board/ir')
-}
-</script>

+ 0 - 307
app/pages/site-manager/board/ir/edit/[id].vue

@@ -1,307 +0,0 @@
-<template>
-  <div class="admin--ir-form">
-    <div v-if="isLoading" class="admin--loading">
-      데이터를 불러오는 중...
-    </div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox">
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox">
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- URL -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">URL</label>
-        <input
-          v-model="formData.url"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com"
-        >
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">내용 <span class="admin--required">*</span></label>
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 기존 첨부파일 -->
-      <div v-if="existingFiles.length > 0" class="admin--form-group">
-        <label class="admin--form-label">기존 첨부파일</label>
-        <div class="admin--file-list">
-          <div v-for="(file, index) in existingFiles" :key="'existing-' + index" class="admin--file-item">
-            <a :href="file.url" target="_blank" class="admin--file-name">{{ file.name }}</a>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeExistingFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-      </div>
-
-      <!-- 새 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div v-if="attachedFiles.length > 0" class="admin--file-list">
-          <div v-for="(file, index) in attachedFiles" :key="'new-' + index" class="admin--file-item">
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        >
-        <button type="button" class="admin--btn admin--btn-secondary" @click="triggerFileInput">
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import SunEditor from '~/components/admin/SunEditor.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const route = useRoute()
-const router = useRouter()
-const { get, put, upload } = useApi()
-
-const isLoading = ref(true)
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const attachedFiles = ref([])
-const existingFiles = ref([])
-const fileInput = ref(null)
-
-const formData = ref({
-  allow_comment: false,
-  is_notice: false,
-  name: '',
-  email: '',
-  url: '',
-  title: '',
-  content: '',
-  file_urls: []
-})
-
-const loadIR = async () => {
-  isLoading.value = true
-
-  const id = route.params.id
-  const { data, error } = await get(`/board/ir/${id}`)
-  console.log('[IREdit] 데이터 로드:', { data, error })
-
-  if (data?.success && data?.data) {
-    const ir = data.data
-    formData.value = {
-      allow_comment: ir.allow_comment || false,
-      is_notice: ir.is_notice || false,
-      name: ir.name || '',
-      email: ir.email || '',
-      url: ir.url || '',
-      title: ir.title || '',
-      content: ir.content || '',
-      file_urls: ir.file_urls || []
-    }
-    existingFiles.value = ir.file_urls || []
-    console.log('[IREdit] 로드 성공')
-  }
-
-  isLoading.value = false
-}
-
-const triggerFileInput = () => {
-  fileInput.value?.click()
-}
-
-const handleFileAdd = (event) => {
-  const files = Array.from(event.target.files)
-  attachedFiles.value.push(...files)
-  event.target.value = ''
-}
-
-const removeFile = (index) => {
-  attachedFiles.value.splice(index, 1)
-}
-
-const removeExistingFile = (index) => {
-  existingFiles.value.splice(index, 1)
-}
-
-const formatFileSize = (bytes) => {
-  if (bytes === 0) return '0 Bytes'
-  const k = 1024
-  const sizes = ['Bytes', 'KB', 'MB', 'GB']
-  const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.title) {
-    errorMessage.value = '제목을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.content) {
-    errorMessage.value = '내용을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let fileUrls = [...existingFiles.value]
-
-    // 새 파일 업로드
-    if (attachedFiles.value.length > 0) {
-      for (const file of attachedFiles.value) {
-        const formDataFile = new FormData()
-        formDataFile.append('file', file)
-
-        const { data: uploadData, error: uploadError } = await upload('/upload/file', formDataFile)
-
-        if (uploadError) {
-          errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`
-          isSaving.value = false
-          return
-        }
-
-        fileUrls.push({
-          name: file.name,
-          url: uploadData.url,
-          size: file.size
-        })
-      }
-    }
-
-    const submitData = {
-      ...formData.value,
-      file_urls: fileUrls
-    }
-
-    const id = route.params.id
-    const { data, error } = await put(`/board/ir/${id}`, submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
-    } else {
-      successMessage.value = 'IR 자료가 수정되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/board/ir')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/board/ir')
-}
-
-onMounted(() => {
-  loadIR()
-})
-</script>

+ 0 - 221
app/pages/site-manager/board/ir/index.vue

@@ -1,221 +0,0 @@
-<template>
-  <div class="admin--board-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <select v-model="searchType" class="admin--form-select admin--search-select">
-          <option value="title">제목</option>
-          <option value="name">이름</option>
-          <option value="content">내용</option>
-        </select>
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="검색어를 입력하세요"
-          @keyup.enter="handleSearch"
-        />
-        <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">검색</button>
-        <button class="admin--btn-small admin--btn-small-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">+ 등록</button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>제목</th>
-            <th>이름</th>
-            <th>등록일</th>
-            <th>조회</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!posts || posts.length === 0">
-            <td colspan="6" class="admin--table-empty">등록된 게시물이 없습니다.</td>
-          </tr>
-          <tr v-else v-for="(post, index) in posts" :key="post.id">
-            <td>
-              {{
-                post.is_notice
-                  ? "공지"
-                  : totalCount - ((currentPage - 1) * perPage + index)
-              }}
-            </td>
-            <td class="admin--table-title">{{ post.title }}</td>
-            <td>{{ post.name }}</td>
-            <td>{{ formatDate(post.created_at) }}</td>
-            <td>{{ post.views }}</td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(post.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(post.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-  import { ref, computed, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({ layout: "admin", middleware: ["auth"] });
-
-  const router = useRouter();
-  const { get, del } = useApi();
-  const posts = ref([]);
-  const searchType = ref("title");
-  const searchKeyword = ref("");
-  const currentPage = ref(1);
-  const perPage = ref(10);
-  const totalCount = ref(0);
-  const totalPages = ref(0);
-
-  const visiblePages = computed(() => {
-    const pages = [];
-    const maxVisible = 5;
-    let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
-    let end = Math.min(totalPages.value, start + maxVisible - 1);
-    if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
-    for (let i = start; i <= end; i++) pages.push(i);
-    return pages;
-  });
-
-  const loadPosts = async () => {
-    const params = { page: currentPage.value, per_page: perPage.value };
-    if (searchKeyword.value) {
-      params.search_type = searchType.value;
-      params.search_keyword = searchKeyword.value;
-    }
-    const { data, error } = await get("/board/ir", { params });
-
-    console.log("[IRBoard] API 응답:", { data, error });
-
-    // API 응답: { success: true, data: { items, total }, message }
-    if (data?.success && data?.data) {
-      posts.value = data.data.items || [];
-      totalCount.value = data.data.total || 0;
-      totalPages.value = Math.ceil(totalCount.value / perPage.value);
-      console.log("[IRBoard] 로드 성공:", posts.value.length);
-    }
-  };
-
-  const handleSearch = () => {
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const handleReset = () => {
-    searchType.value = "title";
-    searchKeyword.value = "";
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const changePage = (page) => {
-    if (page < 1 || page > totalPages.value) return;
-    currentPage.value = page;
-    loadPosts();
-    window.scrollTo({ top: 0, behavior: 'smooth' });
-  };
-  const goToCreate = () => router.push("/site-manager/board/ir/create");
-  const goToEdit = (id) => router.push(`/site-manager/board/ir/edit/${id}`);
-  const handleDelete = async (id) => {
-    if (!confirm("정말 삭제하시겠습니까?")) return;
-    const { error } = await del(`/board/ir/${id}`);
-    if (error) alert("삭제에 실패했습니다.");
-    else {
-      alert("삭제되었습니다.");
-      loadPosts();
-    }
-  };
-  const formatDate = (dateString) => {
-    if (!dateString) return "-";
-    const date = new Date(dateString);
-    return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(
-      2,
-      "0"
-    )}.${String(date.getDate()).padStart(2, "0")}`;
-  };
-
-  onMounted(() => loadPosts());
-</script>
-
-<style scoped>
-  /* 검색 영역 input/select 스타일 통일 */
-  .admin--search-box .admin--form-input,
-  .admin--search-box .admin--search-input,
-  .admin--search-box .admin--form-select,
-  .admin--search-box .admin--search-select {
-    border: 1px solid var(--admin-border-color) !important;
-    border-radius: 4px;
-    height: 33px !important;
-    padding: 6px 14px !important;
-    font-size: 13px !important;
-  }
-</style>

+ 0 - 261
app/pages/site-manager/board/news/create.vue

@@ -1,261 +0,0 @@
-<template>
-  <div class="admin--news-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox">
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox">
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- URL -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">URL</label>
-        <input
-          v-model="formData.url"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com"
-        >
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">내용 <span class="admin--required">*</span></label>
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div class="admin--file-list">
-          <div v-for="(file, index) in attachedFiles" :key="index" class="admin--file-item">
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        >
-        <button type="button" class="admin--btn admin--btn-secondary" @click="triggerFileInput">
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref } from 'vue'
-import { useRouter } from 'vue-router'
-import SunEditor from '~/components/admin/SunEditor.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { post, upload } = useApi()
-
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const attachedFiles = ref([])
-const fileInput = ref(null)
-
-const formData = ref({
-  allow_comment: false,
-  is_notice: false,
-  name: '고진',
-  email: 'admin@admin.kr',
-  url: '',
-  title: '',
-  content: '',
-  file_urls: []
-})
-
-const triggerFileInput = () => {
-  fileInput.value?.click()
-}
-
-const handleFileAdd = (event) => {
-  const files = Array.from(event.target.files)
-  attachedFiles.value.push(...files)
-  event.target.value = ''
-}
-
-const removeFile = (index) => {
-  attachedFiles.value.splice(index, 1)
-}
-
-const formatFileSize = (bytes) => {
-  if (bytes === 0) return '0 Bytes'
-  const k = 1024
-  const sizes = ['Bytes', 'KB', 'MB', 'GB']
-  const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.title) {
-    errorMessage.value = '제목을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.content) {
-    errorMessage.value = '내용을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let fileUrls = []
-
-    // 파일 업로드
-    if (attachedFiles.value.length > 0) {
-      for (const file of attachedFiles.value) {
-        const formDataFile = new FormData()
-        formDataFile.append('file', file)
-
-        const { data: uploadData, error: uploadError } = await upload('/upload/news-file', formDataFile)
-
-        if (uploadError) {
-          errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`
-          isSaving.value = false
-          return
-        }
-
-        fileUrls.push({
-          name: file.name,
-          url: uploadData.data.url,
-          size: file.size
-        })
-      }
-    }
-
-    // content에서 도메인 제거
-    let contentToSave = formData.value.content
-    if (contentToSave) {
-      // http://도메인 또는 https://도메인 제거
-      contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, '')
-    }
-
-    const submitData = {
-      ...formData.value,
-      allow_comment: formData.value.allow_comment ? 1 : 0,
-      is_notice: formData.value.is_notice ? 1 : 0,
-      content: contentToSave,
-      file_urls: fileUrls
-    }
-
-    const { data, error } = await post('/board/news', submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '등록에 실패했습니다.'
-    } else {
-      successMessage.value = '뉴스가 등록되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/board/news')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/board/news')
-}
-</script>

+ 0 - 317
app/pages/site-manager/board/news/edit/[id].vue

@@ -1,317 +0,0 @@
-<template>
-  <div class="admin--news-form">
-    <div v-if="isLoading" class="admin--loading">
-      데이터를 불러오는 중...
-    </div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox">
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox">
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- URL -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">URL</label>
-        <input
-          v-model="formData.url"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com"
-        >
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">제목 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">내용 <span class="admin--required">*</span></label>
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 기존 첨부파일 -->
-      <div v-if="existingFiles.length > 0" class="admin--form-group">
-        <label class="admin--form-label">기존 첨부파일</label>
-        <div class="admin--file-list">
-          <div v-for="(file, index) in existingFiles" :key="'existing-' + index" class="admin--file-item">
-            <a :href="file.url" target="_blank" class="admin--file-name">{{ file.name }}</a>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeExistingFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-      </div>
-
-      <!-- 새 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div v-if="attachedFiles.length > 0" class="admin--file-list">
-          <div v-for="(file, index) in attachedFiles" :key="'new-' + index" class="admin--file-item">
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button type="button" class="admin--btn-remove-file" @click="removeFile(index)">
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        >
-        <button type="button" class="admin--btn admin--btn-secondary" @click="triggerFileInput">
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-import SunEditor from '~/components/admin/SunEditor.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const route = useRoute()
-const router = useRouter()
-const { get, put, upload } = useApi()
-
-const isLoading = ref(true)
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const attachedFiles = ref([])
-const existingFiles = ref([])
-const fileInput = ref(null)
-
-const formData = ref({
-  allow_comment: false,
-  is_notice: false,
-  name: '',
-  email: '',
-  url: '',
-  title: '',
-  content: '',
-  file_urls: []
-})
-
-const loadNews = async () => {
-  isLoading.value = true
-
-  const id = route.params.id
-  const { data, error } = await get(`/board/news/${id}`)
-  console.log('[NewsEdit] 데이터 로드:', { data, error })
-
-  if (data?.success && data?.data) {
-    const news = data.data
-    formData.value = {
-      allow_comment: news.allow_comment === 1 || news.allow_comment === '1',
-      is_notice: news.is_notice === 1 || news.is_notice === '1',
-      name: news.name || '',
-      email: news.email || '',
-      url: news.url || '',
-      title: news.title || '',
-      content: news.content || '',
-      file_urls: news.file_urls || []
-    }
-    existingFiles.value = news.file_urls || []
-    console.log('[NewsEdit] 로드 성공')
-  }
-
-  isLoading.value = false
-}
-
-const triggerFileInput = () => {
-  fileInput.value?.click()
-}
-
-const handleFileAdd = (event) => {
-  const files = Array.from(event.target.files)
-  attachedFiles.value.push(...files)
-  event.target.value = ''
-}
-
-const removeFile = (index) => {
-  attachedFiles.value.splice(index, 1)
-}
-
-const removeExistingFile = (index) => {
-  existingFiles.value.splice(index, 1)
-}
-
-const formatFileSize = (bytes) => {
-  if (bytes === 0) return '0 Bytes'
-  const k = 1024
-  const sizes = ['Bytes', 'KB', 'MB', 'GB']
-  const i = Math.floor(Math.log(bytes) / Math.log(k))
-  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.title) {
-    errorMessage.value = '제목을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.content) {
-    errorMessage.value = '내용을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let fileUrls = [...existingFiles.value]
-
-    // 새 파일 업로드
-    if (attachedFiles.value.length > 0) {
-      for (const file of attachedFiles.value) {
-        const formDataFile = new FormData()
-        formDataFile.append('file', file)
-
-        const { data: uploadData, error: uploadError } = await upload('/upload/news-file', formDataFile)
-
-        if (uploadError) {
-          errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`
-          isSaving.value = false
-          return
-        }
-
-        fileUrls.push({
-          name: file.name,
-          url: uploadData.data.url,
-          size: file.size
-        })
-      }
-    }
-
-    // content에서 도메인 제거
-    let contentToSave = formData.value.content
-    if (contentToSave) {
-      // http://도메인 또는 https://도메인 제거
-      contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, '')
-    }
-
-    const submitData = {
-      ...formData.value,
-      allow_comment: formData.value.allow_comment ? 1 : 0,
-      is_notice: formData.value.is_notice ? 1 : 0,
-      content: contentToSave,
-      file_urls: fileUrls
-    }
-
-    const id = route.params.id
-    const { data, error } = await put(`/board/news/${id}`, submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
-    } else {
-      successMessage.value = '뉴스가 수정되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/board/news')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/board/news')
-}
-
-onMounted(() => {
-  loadNews()
-})
-</script>

+ 0 - 221
app/pages/site-manager/board/news/index.vue

@@ -1,221 +0,0 @@
-<template>
-  <div class="admin--board-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <select v-model="searchType" class="admin--form-select admin--search-select">
-          <option value="title">제목</option>
-          <option value="name">이름</option>
-          <option value="content">내용</option>
-        </select>
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="검색어를 입력하세요"
-          @keyup.enter="handleSearch"
-        />
-        <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">검색</button>
-        <button class="admin--btn-small admin--btn-small-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">+ 등록</button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>제목</th>
-            <th>이름</th>
-            <th>등록일</th>
-            <th>조회</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!posts || posts.length === 0">
-            <td colspan="6" class="admin--table-empty">등록된 게시물이 없습니다.</td>
-          </tr>
-          <tr v-else v-for="(post, index) in posts" :key="post.id">
-            <td>
-              {{
-                (post.is_notice === 1 || post.is_notice === '1')
-                  ? "공지"
-                  : totalCount - ((currentPage - 1) * perPage + index)
-              }}
-            </td>
-            <td class="admin--table-title">{{ post.title }}</td>
-            <td>{{ post.name }}</td>
-            <td>{{ formatDate(post.created_at) }}</td>
-            <td>{{ post.views }}</td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(post.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(post.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-  import { ref, computed, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({ layout: "admin", middleware: ["auth"] });
-
-  const router = useRouter();
-  const { get, del } = useApi();
-  const posts = ref([]);
-  const searchType = ref("title");
-  const searchKeyword = ref("");
-  const currentPage = ref(1);
-  const perPage = ref(10);
-  const totalCount = ref(0);
-  const totalPages = ref(0);
-
-  const visiblePages = computed(() => {
-    const pages = [];
-    const maxVisible = 5;
-    let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
-    let end = Math.min(totalPages.value, start + maxVisible - 1);
-    if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
-    for (let i = start; i <= end; i++) pages.push(i);
-    return pages;
-  });
-
-  const loadPosts = async () => {
-    const params = { page: currentPage.value, per_page: perPage.value };
-    if (searchKeyword.value) {
-      params.search_type = searchType.value;
-      params.search_keyword = searchKeyword.value;
-    }
-    const { data, error } = await get("/board/news", { params });
-
-    console.log("[NewsBoard] API 응답:", { data, error });
-
-    // API 응답: { success: true, data: { items, total }, message }
-    if (data?.success && data?.data) {
-      posts.value = data.data.items || [];
-      totalCount.value = data.data.total || 0;
-      totalPages.value = Math.ceil(totalCount.value / perPage.value);
-      console.log("[NewsBoard] 로드 성공:", posts.value.length);
-    }
-  };
-
-  const handleSearch = () => {
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const handleReset = () => {
-    searchType.value = "title";
-    searchKeyword.value = "";
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const changePage = (page) => {
-    if (page < 1 || page > totalPages.value) return;
-    currentPage.value = page;
-    loadPosts();
-    window.scrollTo({ top: 0, behavior: 'smooth' });
-  };
-  const goToCreate = () => router.push("/site-manager/board/news/create");
-  const goToEdit = (id) => router.push(`/site-manager/board/news/edit/${id}`);
-  const handleDelete = async (id) => {
-    if (!confirm("정말 삭제하시겠습니까?")) return;
-    const { error } = await del(`/board/news/${id}`);
-    if (error) alert("삭제에 실패했습니다.");
-    else {
-      alert("삭제되었습니다.");
-      loadPosts();
-    }
-  };
-  const formatDate = (dateString) => {
-    if (!dateString) return "-";
-    const date = new Date(dateString);
-    return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(
-      2,
-      "0"
-    )}.${String(date.getDate()).padStart(2, "0")}`;
-  };
-
-  onMounted(() => loadPosts());
-</script>
-
-<style scoped>
-  /* 검색 영역 input/select 스타일 통일 */
-  .admin--search-box .admin--form-input,
-  .admin--search-box .admin--search-input,
-  .admin--search-box .admin--form-select,
-  .admin--search-box .admin--search-select {
-    border: 1px solid var(--admin-border-color) !important;
-    border-radius: 4px;
-    height: 33px !important;
-    padding: 6px 14px !important;
-    font-size: 13px !important;
-  }
-</style>

+ 0 - 276
app/pages/site-manager/board/notice/create.vue

@@ -1,276 +0,0 @@
-<template>
-  <div class="admin--news-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox" />
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox" />
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >이름 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >이메일 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- URL -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">URL</label>
-        <input
-          v-model="formData.url"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com"
-        />
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >제목 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >내용 <span class="admin--required">*</span></label
-        >
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div class="admin--file-list">
-          <div
-            v-for="(file, index) in attachedFiles"
-            :key="index"
-            class="admin--file-item"
-          >
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button
-              type="button"
-              class="admin--btn-remove-file"
-              @click="removeFile(index)"
-            >
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        />
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="triggerFileInput"
-        >
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
-          {{ isSaving ? "저장 중..." : "확인" }}
-        </button>
-        <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-  import { ref } from "vue";
-  import { useRouter } from "vue-router";
-  import SunEditor from "~/components/admin/SunEditor.vue";
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const router = useRouter();
-  const { post, upload } = useApi();
-
-  const isSaving = ref(false);
-  const successMessage = ref("");
-  const errorMessage = ref("");
-  const attachedFiles = ref([]);
-  const fileInput = ref(null);
-
-  const formData = ref({
-    allow_comment: false,
-    is_notice: false,
-    name: "고진",
-    email: "admin@admin.kr",
-    url: "",
-    title: "",
-    content: "",
-    file_urls: [],
-  });
-
-  const triggerFileInput = () => {
-    fileInput.value?.click();
-  };
-
-  const handleFileAdd = (event) => {
-    const files = Array.from(event.target.files);
-    attachedFiles.value.push(...files);
-    event.target.value = "";
-  };
-
-  const removeFile = (index) => {
-    attachedFiles.value.splice(index, 1);
-  };
-
-  const formatFileSize = (bytes) => {
-    if (bytes === 0) return "0 Bytes";
-    const k = 1024;
-    const sizes = ["Bytes", "KB", "MB", "GB"];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
-  };
-
-  const handleSubmit = async () => {
-    successMessage.value = "";
-    errorMessage.value = "";
-
-    if (!formData.value.title) {
-      errorMessage.value = "제목을 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.content) {
-      errorMessage.value = "내용을 입력하세요.";
-      return;
-    }
-
-    isSaving.value = true;
-
-    try {
-      let fileUrls = [];
-
-      // 파일 업로드
-      if (attachedFiles.value.length > 0) {
-        for (const file of attachedFiles.value) {
-          const formDataFile = new FormData();
-          formDataFile.append("file", file);
-
-          const { data: uploadData, error: uploadError } = await upload(
-            "/upload/news-file",
-            formDataFile
-          );
-
-          if (uploadError) {
-            errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`;
-            isSaving.value = false;
-            return;
-          }
-
-          fileUrls.push({
-            name: file.name,
-            url: uploadData.data.url,
-            size: file.size,
-          });
-        }
-      }
-
-      // content에서 도메인 제거
-      let contentToSave = formData.value.content;
-      if (contentToSave) {
-        // http://도메인 또는 https://도메인 제거
-        contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, "");
-      }
-
-      const submitData = {
-        ...formData.value,
-        allow_comment: formData.value.allow_comment ? 1 : 0,
-        is_notice: formData.value.is_notice ? 1 : 0,
-        content: contentToSave,
-        file_urls: fileUrls,
-      };
-
-      const { data, error } = await post("/board/notice", submitData);
-
-      if (error) {
-        errorMessage.value = error.message || "등록에 실패했습니다.";
-      } else {
-        successMessage.value = "뉴스가 등록되었습니다.";
-        setTimeout(() => {
-          router.push("/site-manager/board/notice");
-        }, 1000);
-      }
-    } catch (error) {
-      errorMessage.value = "서버 오류가 발생했습니다.";
-      console.error("Save error:", error);
-    } finally {
-      isSaving.value = false;
-    }
-  };
-
-  const goToList = () => {
-    router.push("/site-manager/board/notice");
-  };
-</script>

+ 0 - 340
app/pages/site-manager/board/notice/edit/[id].vue

@@ -1,340 +0,0 @@
-<template>
-  <div class="admin--news-form">
-    <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 댓글허용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">댓글허용</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.allow_comment" type="checkbox" />
-            <span>댓글 허용</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 공지 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">공지</label>
-        <div class="admin--checkbox-group">
-          <label class="admin--checkbox-label">
-            <input v-model="formData.is_notice" type="checkbox" />
-            <span>공지글로 등록</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >이름 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >이메일 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="이메일을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- URL -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">URL</label>
-        <input
-          v-model="formData.url"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com"
-        />
-      </div>
-
-      <!-- 제목 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >제목 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.title"
-          type="text"
-          class="admin--form-input"
-          placeholder="제목을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 내용 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >내용 <span class="admin--required">*</span></label
-        >
-        <SunEditor v-model="formData.content" />
-      </div>
-
-      <!-- 기존 첨부파일 -->
-      <div v-if="existingFiles.length > 0" class="admin--form-group">
-        <label class="admin--form-label">기존 첨부파일</label>
-        <div class="admin--file-list">
-          <div
-            v-for="(file, index) in existingFiles"
-            :key="'existing-' + index"
-            class="admin--file-item"
-          >
-            <a :href="file.url" target="_blank" class="admin--file-name">{{
-              file.name
-            }}</a>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button
-              type="button"
-              class="admin--btn-remove-file"
-              @click="removeExistingFile(index)"
-            >
-              삭제
-            </button>
-          </div>
-        </div>
-      </div>
-
-      <!-- 새 파일첨부 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">파일첨부</label>
-        <div v-if="attachedFiles.length > 0" class="admin--file-list">
-          <div
-            v-for="(file, index) in attachedFiles"
-            :key="'new-' + index"
-            class="admin--file-item"
-          >
-            <span class="admin--file-name">{{ file.name }}</span>
-            <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
-            <button
-              type="button"
-              class="admin--btn-remove-file"
-              @click="removeFile(index)"
-            >
-              삭제
-            </button>
-          </div>
-        </div>
-        <input
-          ref="fileInput"
-          type="file"
-          multiple
-          class="admin--form-file-hidden"
-          @change="handleFileAdd"
-        />
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="triggerFileInput"
-        >
-          파일 추가
-        </button>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
-          {{ isSaving ? "저장 중..." : "확인" }}
-        </button>
-        <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-  import { ref, onMounted } from "vue";
-  import { useRoute, useRouter } from "vue-router";
-  import SunEditor from "~/components/admin/SunEditor.vue";
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const route = useRoute();
-  const router = useRouter();
-  const { get, put, upload } = useApi();
-
-  const isLoading = ref(true);
-  const isSaving = ref(false);
-  const successMessage = ref("");
-  const errorMessage = ref("");
-  const attachedFiles = ref([]);
-  const existingFiles = ref([]);
-  const fileInput = ref(null);
-
-  const formData = ref({
-    allow_comment: false,
-    is_notice: false,
-    name: "",
-    email: "",
-    url: "",
-    title: "",
-    content: "",
-    file_urls: [],
-  });
-
-  const loadNews = async () => {
-    isLoading.value = true;
-
-    const id = route.params.id;
-    const { data, error } = await get(`/board/notice/${id}`);
-    console.log("[NewsEdit] 데이터 로드:", { data, error });
-
-    if (data?.success && data?.data) {
-      const news = data.data;
-      formData.value = {
-        allow_comment: news.allow_comment === 1 || news.allow_comment === '1',
-        is_notice: news.is_notice === 1 || news.is_notice === '1',
-        name: news.name || "",
-        email: news.email || "",
-        url: news.url || "",
-        title: news.title || "",
-        content: news.content || "",
-        file_urls: news.file_urls || [],
-      };
-      existingFiles.value = news.file_urls || [];
-      console.log("[NewsEdit] 로드 성공");
-    }
-
-    isLoading.value = false;
-  };
-
-  const triggerFileInput = () => {
-    fileInput.value?.click();
-  };
-
-  const handleFileAdd = (event) => {
-    const files = Array.from(event.target.files);
-    attachedFiles.value.push(...files);
-    event.target.value = "";
-  };
-
-  const removeFile = (index) => {
-    attachedFiles.value.splice(index, 1);
-  };
-
-  const removeExistingFile = (index) => {
-    existingFiles.value.splice(index, 1);
-  };
-
-  const formatFileSize = (bytes) => {
-    if (bytes === 0) return "0 Bytes";
-    const k = 1024;
-    const sizes = ["Bytes", "KB", "MB", "GB"];
-    const i = Math.floor(Math.log(bytes) / Math.log(k));
-    return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
-  };
-
-  const handleSubmit = async () => {
-    successMessage.value = "";
-    errorMessage.value = "";
-
-    if (!formData.value.title) {
-      errorMessage.value = "제목을 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.content) {
-      errorMessage.value = "내용을 입력하세요.";
-      return;
-    }
-
-    isSaving.value = true;
-
-    try {
-      let fileUrls = [...existingFiles.value];
-
-      // 새 파일 업로드
-      if (attachedFiles.value.length > 0) {
-        for (const file of attachedFiles.value) {
-          const formDataFile = new FormData();
-          formDataFile.append("file", file);
-
-          const { data: uploadData, error: uploadError } = await upload(
-            "/upload/news-file",
-            formDataFile
-          );
-
-          if (uploadError) {
-            errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`;
-            isSaving.value = false;
-            return;
-          }
-
-          fileUrls.push({
-            name: file.name,
-            url: uploadData.data.url,
-            size: file.size,
-          });
-        }
-      }
-
-      // content에서 도메인 제거
-      let contentToSave = formData.value.content;
-      if (contentToSave) {
-        // http://도메인 또는 https://도메인 제거
-        contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, "");
-      }
-
-      const submitData = {
-        ...formData.value,
-        allow_comment: formData.value.allow_comment ? 1 : 0,
-        is_notice: formData.value.is_notice ? 1 : 0,
-        content: contentToSave,
-        file_urls: fileUrls,
-      };
-
-      const id = route.params.id;
-      const { data, error } = await put(`/board/notice/${id}`, submitData);
-
-      if (error) {
-        errorMessage.value = error.message || "수정에 실패했습니다.";
-      } else {
-        successMessage.value = "뉴스가 수정되었습니다.";
-        setTimeout(() => {
-          router.push("/site-manager/board/notice");
-        }, 1000);
-      }
-    } catch (error) {
-      errorMessage.value = "서버 오류가 발생했습니다.";
-      console.error("Save error:", error);
-    } finally {
-      isSaving.value = false;
-    }
-  };
-
-  const goToList = () => {
-    router.push("/site-manager/board/notice");
-  };
-
-  onMounted(() => {
-    loadNews();
-  });
-</script>

+ 0 - 225
app/pages/site-manager/board/notice/index.vue

@@ -1,225 +0,0 @@
-<template>
-  <div class="admin--board-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <select v-model="searchType" class="admin--form-select admin--search-select">
-          <option value="title">제목</option>
-          <option value="name">이름</option>
-          <option value="content">내용</option>
-        </select>
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="검색어를 입력하세요"
-          @keyup.enter="handleSearch"
-        />
-        <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">
-          검색
-        </button>
-        <button class="admin--btn-small admin--btn-small-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">
-          + 등록
-        </button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>제목</th>
-            <th>이름</th>
-            <th>등록일</th>
-            <th>조회</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!posts || posts.length === 0">
-            <td colspan="6" class="admin--table-empty">등록된 게시물이 없습니다.</td>
-          </tr>
-          <tr v-else v-for="(post, index) in posts" :key="post.id">
-            <td>
-              {{
-                (post.is_notice === 1 || post.is_notice === '1')
-                  ? "공지"
-                  : totalCount - ((currentPage - 1) * perPage + index)
-              }}
-            </td>
-            <td class="admin--table-title">{{ post.title }}</td>
-            <td>{{ post.name }}</td>
-            <td>{{ formatDate(post.created_at) }}</td>
-            <td>{{ post.views }}</td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(post.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(post.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-  import { ref, computed, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({ layout: "admin", middleware: ["auth"] });
-
-  const router = useRouter();
-  const { get, del } = useApi();
-  const posts = ref([]);
-  const searchType = ref("title");
-  const searchKeyword = ref("");
-  const currentPage = ref(1);
-  const perPage = ref(10);
-  const totalCount = ref(0);
-  const totalPages = ref(0);
-
-  const visiblePages = computed(() => {
-    const pages = [];
-    const maxVisible = 5;
-    let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
-    let end = Math.min(totalPages.value, start + maxVisible - 1);
-    if (end - start < maxVisible - 1) start = Math.max(1, end - maxVisible + 1);
-    for (let i = start; i <= end; i++) pages.push(i);
-    return pages;
-  });
-
-  const loadPosts = async () => {
-    const params = { page: currentPage.value, per_page: perPage.value };
-    if (searchKeyword.value) {
-      params.search_type = searchType.value;
-      params.search_keyword = searchKeyword.value;
-    }
-    const { data, error } = await get("/board/notice", { params });
-
-    console.log("[NoticeBoard] API 응답:", { data, error });
-
-    // API 응답: { success: true, data: { items, total }, message }
-    if (data?.success && data?.data) {
-      posts.value = data.data.items || [];
-      totalCount.value = data.data.total || 0;
-      totalPages.value = Math.ceil(totalCount.value / perPage.value);
-      console.log("[NoticeBoard] 로드 성공:", posts.value.length);
-    }
-  };
-
-  const handleSearch = () => {
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const handleReset = () => {
-    searchType.value = "title";
-    searchKeyword.value = "";
-    currentPage.value = 1;
-    loadPosts();
-  };
-  const changePage = (page) => {
-    if (page < 1 || page > totalPages.value) return;
-    currentPage.value = page;
-    loadPosts();
-    window.scrollTo({ top: 0, behavior: "smooth" });
-  };
-  const goToCreate = () => router.push("/site-manager/board/notice/create");
-  const goToEdit = (id) => router.push(`/site-manager/board/notice/edit/${id}`);
-  const handleDelete = async (id) => {
-    if (!confirm("정말 삭제하시겠습니까?")) return;
-    const { error } = await del(`/board/notice/${id}`);
-    if (error) alert("삭제에 실패했습니다.");
-    else {
-      alert("삭제되었습니다.");
-      loadPosts();
-    }
-  };
-  const formatDate = (dateString) => {
-    if (!dateString) return "-";
-    const date = new Date(dateString);
-    return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(
-      2,
-      "0"
-    )}.${String(date.getDate()).padStart(2, "0")}`;
-  };
-
-  onMounted(() => loadPosts());
-</script>
-
-<style scoped>
-  /* 검색 영역 input/select 스타일 통일 */
-  .admin--search-box .admin--form-input,
-  .admin--search-box .admin--search-input,
-  .admin--search-box .admin--form-select,
-  .admin--search-box .admin--search-select {
-    border: 1px solid var(--admin-border-color) !important;
-    border-radius: 4px;
-    height: 33px !important;
-    padding: 6px 14px !important;
-    font-size: 13px !important;
-  }
-</style>

+ 1 - 1
app/pages/site-manager/fishing/list.vue

@@ -54,7 +54,7 @@
 
     <!-- 테이블 -->
     <div class="admin--table-wrapper">
-      <table class="admin--table">
+      <table class="admin--table fishing--table">
         <thead>
           <tr>
             <th style="width: 40px;">번호</th>

+ 1 - 1
app/pages/site-manager/onboard/list.vue

@@ -53,7 +53,7 @@
 
     <!-- 테이블 -->
     <div class="admin--table-wrapper">
-      <table class="admin--table">
+      <table class="admin--table fishing--table">
         <thead>
           <tr>
             <th style="width: 80px;">번호</th>

+ 0 - 293
app/pages/site-manager/service-center/create.vue

@@ -1,293 +0,0 @@
-<template>
-  <div class="admin--service-center-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 서비스센터명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >서비스센터명 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="서비스센터명을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 소속명 선택 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >딜러사 명 <span class="admin--required">*</span></label
-        >
-        <select v-model="formData.branch_id" class="admin--form-select" required>
-          <option value="">소속 지점을 선택하세요</option>
-          <option v-for="branch in branches" :key="branch.id" :value="branch.id">
-            {{ branch.name }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >대표번호 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        />
-      </div>
-
-      <!-- 주소 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >주소 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.address"
-          type="text"
-          class="admin--form-input"
-          placeholder="주소를 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 상세주소 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">상세주소</label>
-        <input
-          v-model="formData.detail_address"
-          type="text"
-          class="admin--form-input"
-          placeholder="상세주소를 입력하세요"
-        />
-      </div>
-
-      <!-- 위도/경도 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">위치 좌표</label>
-        <div class="admin--coordinate-group">
-          <div class="admin--coordinate-item">
-            <label>위도</label>
-            <input
-              v-model.number="formData.latitude"
-              type="text"
-              step="any"
-              class="admin--form-input"
-              placeholder="37.5665"
-            />
-          </div>
-          <div class="admin--coordinate-item">
-            <label>경도</label>
-            <input
-              v-model.number="formData.longitude"
-              type="text"
-              step="any"
-              class="admin--form-input"
-              placeholder="126.9780"
-            />
-          </div>
-        </div>
-      </div>
-
-      <!-- 영업시간 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">영업시간</label>
-        <textarea
-          v-model="formData.business_hours"
-          class="admin--form-textarea"
-          rows="3"
-          placeholder="평일: 09:00 - 18:00&#10;주말: 10:00 - 17:00"
-        ></textarea>
-      </div>
-
-      <!-- 서비스예약 링크 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">서비스예약 링크</label>
-        <input
-          v-model="formData.service_reservation_link"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com/service-reservation"
-        />
-      </div>
-
-      <!-- 링크 관리 -->
-      <!-- <div class="admin--form-group">
-        <label class="admin--form-label">관련 링크</label>
-        <div class="admin--multi-input-wrapper">
-          <div
-            v-for="(link, index) in formData.links"
-            :key="index"
-            class="admin--multi-input-item"
-          >
-            <input
-              v-model="formData.links[index]"
-              type="url"
-              class="admin--form-input"
-              placeholder="https://example.com"
-            />
-            <button type="button" class="admin--btn-remove" @click="removeLink(index)">
-              삭제
-            </button>
-          </div>
-          <button type="button" class="admin--btn-add" @click="addLink">
-            + 링크 추가
-          </button>
-        </div>
-      </div> -->
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
-          {{ isSaving ? "저장 중..." : "확인" }}
-        </button>
-        <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-  import { ref, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const router = useRouter();
-  const { get, post } = useApi();
-
-  const isSaving = ref(false);
-  const successMessage = ref("");
-  const errorMessage = ref("");
-  const branches = ref([]);
-
-  const formData = ref({
-    name: "",
-    branch_id: "",
-    phone: "",
-    address: "",
-    detail_address: "",
-    latitude: null,
-    longitude: null,
-    business_hours: "",
-    service_reservation_link: "",
-    links: [""], // 초기 링크 하나
-  });
-
-  // 지점 목록 로드
-  const loadBranches = async () => {
-    const { data, error } = await get("/branch/list", { params: { per_page: 1000 } });
-
-    if (data?.success && data?.data?.items) {
-      branches.value = data.data.items.filter((branch) => branch.is_active == 1);
-    }
-  };
-
-  // 링크 추가
-  const addLink = () => {
-    formData.value.links.push("");
-  };
-
-  // 링크 삭제
-  const removeLink = (index) => {
-    formData.value.links.splice(index, 1);
-  };
-
-  // 폼 제출
-  const handleSubmit = async () => {
-    successMessage.value = "";
-    errorMessage.value = "";
-
-    // 유효성 검사
-    if (!formData.value.name) {
-      errorMessage.value = "서비스센터명을 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.branch_id) {
-      errorMessage.value = "소속 지점을 선택하세요.";
-      return;
-    }
-
-    if (!formData.value.phone) {
-      errorMessage.value = "대표번호를 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.address) {
-      errorMessage.value = "주소를 입력하세요.";
-      return;
-    }
-
-    isSaving.value = true;
-
-    try {
-      // 빈 링크 제거
-      const submitData = {
-        ...formData.value,
-        links: formData.value.links.filter((link) => link.trim() !== ""),
-      };
-
-      const { data, error } = await post("/service-center", submitData);
-
-      if (error || !data?.success) {
-        errorMessage.value = error?.message || data?.message || "등록에 실패했습니다.";
-      } else {
-        successMessage.value = data.message || "서비스센터가 등록되었습니다.";
-        setTimeout(() => {
-          router.push("/site-manager/service-center/list");
-        }, 1000);
-      }
-    } catch (error) {
-      errorMessage.value = "서버 오류가 발생했습니다.";
-      console.error("Save error:", error);
-    } finally {
-      isSaving.value = false;
-    }
-  };
-
-  // 목록으로 이동
-  const goToList = () => {
-    router.push("/site-manager/service-center/list");
-  };
-
-  onMounted(() => {
-    loadBranches();
-  });
-</script>
-
-<style scoped>
-  .admin--coordinate-group {
-    display: flex;
-    gap: 16px;
-  }
-
-  .admin--coordinate-item {
-    flex: 1;
-  }
-
-  .admin--coordinate-item label {
-    display: block;
-    margin-bottom: 8px;
-    font-size: 14px;
-    color: #666;
-  }
-</style>

+ 0 - 327
app/pages/site-manager/service-center/edit/[id].vue

@@ -1,327 +0,0 @@
-<template>
-  <div class="admin--service-center-form">
-    <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 서비스센터명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >서비스센터명 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="서비스센터명을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 소속명 선택 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >딜러사 명 <span class="admin--required">*</span></label
-        >
-        <select v-model="formData.branch_id" class="admin--form-select" required>
-          <option value="">소속 지점을 선택하세요</option>
-          <option v-for="branch in branches" :key="branch.id" :value="branch.id">
-            {{ branch.name }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >대표번호 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        />
-      </div>
-
-      <!-- 주소 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >주소 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.address"
-          type="text"
-          class="admin--form-input"
-          placeholder="주소를 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 상세주소 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">상세주소</label>
-        <input
-          v-model="formData.detail_address"
-          type="text"
-          class="admin--form-input"
-          placeholder="상세주소를 입력하세요"
-        />
-      </div>
-
-      <!-- 위도/경도 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">위치 좌표</label>
-        <div class="admin--coordinate-group">
-          <div class="admin--coordinate-item">
-            <label>위도</label>
-            <input
-              v-model.number="formData.latitude"
-              type="text"
-              step="any"
-              class="admin--form-input"
-              placeholder="37.5665"
-            />
-          </div>
-          <div class="admin--coordinate-item">
-            <label>경도</label>
-            <input
-              v-model.number="formData.longitude"
-              type="text"
-              step="any"
-              class="admin--form-input"
-              placeholder="126.9780"
-            />
-          </div>
-        </div>
-      </div>
-
-      <!-- 영업시간 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">영업시간</label>
-        <textarea
-          v-model="formData.business_hours"
-          class="admin--form-textarea"
-          rows="3"
-          placeholder="평일: 09:00 - 18:00&#10;주말: 10:00 - 17:00"
-        ></textarea>
-      </div>
-
-      <!-- 서비스예약 링크 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">서비스예약 링크</label>
-        <input
-          v-model="formData.service_reservation_link"
-          type="url"
-          class="admin--form-input"
-          placeholder="https://example.com/service-reservation"
-        />
-      </div>
-
-      <!-- 링크 관리 -->
-      <!-- <div class="admin--form-group">
-        <label class="admin--form-label">관련 링크</label>
-        <div class="admin--multi-input-wrapper">
-          <div
-            v-for="(link, index) in formData.links"
-            :key="index"
-            class="admin--multi-input-item"
-          >
-            <input
-              v-model="formData.links[index]"
-              type="url"
-              class="admin--form-input"
-              placeholder="https://example.com"
-            />
-            <button type="button" class="admin--btn-remove" @click="removeLink(index)">
-              삭제
-            </button>
-          </div>
-          <button type="button" class="admin--btn-add" @click="addLink">
-            + 링크 추가
-          </button>
-        </div>
-      </div> -->
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
-          {{ isSaving ? "저장 중..." : "확인" }}
-        </button>
-        <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-  import { ref, onMounted } from "vue";
-  import { useRouter, useRoute } from "vue-router";
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const router = useRouter();
-  const route = useRoute();
-  const { get, put } = useApi();
-
-  const isLoading = ref(true);
-  const isSaving = ref(false);
-  const successMessage = ref("");
-  const errorMessage = ref("");
-  const branches = ref([]);
-
-  const formData = ref({
-    name: "",
-    branch_id: "",
-    phone: "",
-    address: "",
-    detail_address: "",
-    latitude: null,
-    longitude: null,
-    business_hours: "",
-    service_reservation_link: "",
-    links: [],
-  });
-
-  // 지점 목록 로드
-  const loadBranches = async () => {
-    const { data, error } = await get("/branch/list", { params: { per_page: 1000 } });
-
-    if (data?.success && data?.data?.items) {
-      branches.value = data.data.items.filter((branch) => branch.is_active == 1);
-    }
-  };
-
-  // 서비스센터 데이터 로드
-  const loadServiceCenter = async () => {
-    const id = route.params.id;
-
-    const { data, error } = await get(`/service-center/${id}`);
-
-    if (data?.success && data?.data) {
-      const serviceCenter = data.data;
-      formData.value = {
-        name: serviceCenter.name || "",
-        branch_id: serviceCenter.branch_id || "",
-        phone: serviceCenter.main_phone || "",
-        address: serviceCenter.address || "",
-        detail_address: serviceCenter.detail_address || "",
-        latitude: serviceCenter.latitude || null,
-        longitude: serviceCenter.longitude || null,
-        business_hours: serviceCenter.business_hours || "",
-        service_reservation_link: serviceCenter.service_reservation_link || "",
-        links:
-          serviceCenter.links && serviceCenter.links.length > 0
-            ? serviceCenter.links
-            : [""],
-      };
-    }
-
-    isLoading.value = false;
-  };
-
-  // 링크 추가
-  const addLink = () => {
-    formData.value.links.push("");
-  };
-
-  // 링크 삭제
-  const removeLink = (index) => {
-    formData.value.links.splice(index, 1);
-  };
-
-  // 폼 제출
-  const handleSubmit = async () => {
-    successMessage.value = "";
-    errorMessage.value = "";
-
-    // 유효성 검사
-    if (!formData.value.name) {
-      errorMessage.value = "서비스센터명을 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.branch_id) {
-      errorMessage.value = "소속 지점을 선택하세요.";
-      return;
-    }
-
-    if (!formData.value.phone) {
-      errorMessage.value = "대표번호를 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.address) {
-      errorMessage.value = "주소를 입력하세요.";
-      return;
-    }
-
-    isSaving.value = true;
-
-    try {
-      const id = route.params.id;
-
-      // 빈 링크 제거
-      const submitData = {
-        ...formData.value,
-        links: formData.value.links.filter((link) => link.trim() !== ""),
-      };
-
-      const { data, error } = await put(`/service-center/${id}`, submitData);
-
-      if (error || !data?.success) {
-        errorMessage.value = error?.message || data?.message || "수정에 실패했습니다.";
-      } else {
-        successMessage.value = data.message || "서비스센터가 수정되었습니다.";
-        setTimeout(() => {
-          router.push("/site-manager/service-center/list");
-        }, 1000);
-      }
-    } catch (error) {
-      errorMessage.value = "서버 오류가 발생했습니다.";
-      console.error("Save error:", error);
-    } finally {
-      isSaving.value = false;
-    }
-  };
-
-  // 목록으로 이동
-  const goToList = () => {
-    router.push("/site-manager/service-center/list");
-  };
-
-  onMounted(async () => {
-    await loadBranches();
-    await loadServiceCenter();
-  });
-</script>
-
-<style scoped>
-  .admin--coordinate-group {
-    display: flex;
-    gap: 16px;
-  }
-
-  .admin--coordinate-item {
-    flex: 1;
-  }
-
-  .admin--coordinate-item label {
-    display: block;
-    margin-bottom: 8px;
-    font-size: 14px;
-    color: #666;
-  }
-</style>

+ 0 - 337
app/pages/site-manager/service-center/list.vue

@@ -1,337 +0,0 @@
-<template>
-  <div class="admin--service-center-list">
-    <!-- 상단 버튼 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form"></div>
-      <div class="admin--search-actions">
-        <button class="admin--btn admin--btn-primary" @click="goToCreate">
-          + 서비스센터 등록
-        </button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>서비스센터명</th>
-            <th>소속명</th>
-            <th>대표번호</th>
-            <th>주소</th>
-            <th>상태</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="isLoading">
-            <td colspan="7" class="admin--table-loading">
-              데이터를 불러오는 중...
-            </td>
-          </tr>
-          <tr v-else-if="!serviceCenters || serviceCenters.length === 0">
-            <td colspan="7" class="admin--table-empty">
-              등록된 서비스센터가 없습니다.
-            </td>
-          </tr>
-          <tr v-else v-for="(serviceCenter, index) in serviceCenters" :key="serviceCenter.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td class="admin--table-title">{{ serviceCenter.name }}</td>
-            <td>{{ serviceCenter.branch_name || '-' }}</td>
-            <td>{{ serviceCenter.main_phone }}</td>
-            <td>{{ serviceCenter.address }}</td>
-            <td>
-              <button
-                class="admin--toggle-btn"
-                :class="{ 'is-active': serviceCenter.is_active == 1 }"
-                @click="toggleActive(serviceCenter.id, serviceCenter.is_active)"
-              >
-                {{ serviceCenter.is_active == 1 ? '사용' : '비사용' }}
-              </button>
-            </td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(serviceCenter.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="deleteServiceCenter(serviceCenter.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-      >
-        이전
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-      >
-        다음
-      </button>
-    </div>
-
-    <!-- 알림 모달 -->
-    <AdminAlertModal
-      v-if="alertModal.show"
-      :title="alertModal.title"
-      :message="alertModal.message"
-      :type="alertModal.type"
-      @confirm="handleAlertConfirm"
-      @cancel="handleAlertCancel"
-      @close="closeAlertModal"
-    />
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-import AdminAlertModal from '~/components/admin/AdminAlertModal.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { get, del, post } = useApi()
-
-const isLoading = ref(false)
-const serviceCenters = ref([])
-const currentPage = ref(1)
-const perPage = ref(10)
-const totalCount = ref(0)
-const totalPages = ref(0)
-
-// 알림 모달
-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 showConfirm = (message, onConfirm, title = '확인') => {
-  alertModal.value = {
-    show: true,
-    title,
-    message,
-    type: 'confirm',
-    onConfirm
-  }
-}
-
-// 알림 모달 닫기
-const closeAlertModal = () => {
-  alertModal.value.show = false
-}
-
-// 알림 모달 확인
-const handleAlertConfirm = () => {
-  if (alertModal.value.onConfirm) {
-    alertModal.value.onConfirm()
-  }
-  closeAlertModal()
-}
-
-// 알림 모달 취소
-const handleAlertCancel = () => {
-  closeAlertModal()
-}
-
-// 보이는 페이지 번호 계산
-const visiblePages = computed(() => {
-  const pages = []
-  const maxVisible = 5
-  let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
-  let end = Math.min(totalPages.value, start + maxVisible - 1)
-
-  if (end - start < maxVisible - 1) {
-    start = Math.max(1, end - maxVisible + 1)
-  }
-
-  for (let i = start; i <= end; i++) {
-    pages.push(i)
-  }
-
-  return pages
-})
-
-// 데이터 로드
-const loadServiceCenters = async () => {
-  isLoading.value = true
-
-  const params = {
-    page: currentPage.value,
-    per_page: perPage.value
-  }
-
-  const { data, error } = await get('/service-center/list', { params })
-
-  console.log('[ServiceCenterList] API 응답:', { data, error })
-
-  if (data?.success && data?.data) {
-    serviceCenters.value = data.data.items || []
-    totalCount.value = data.data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-    console.log('[ServiceCenterList] 로드 성공:', serviceCenters.value.length)
-  }
-
-  isLoading.value = false
-}
-
-// 페이지 변경
-const changePage = (page) => {
-  if (page < 1 || page > totalPages.value) return
-  currentPage.value = page
-  loadServiceCenters()
-  window.scrollTo({ top: 0, behavior: 'smooth' })
-}
-
-// 서비스센터 등록 페이지로 이동
-const goToCreate = () => {
-  router.push('/site-manager/service-center/create')
-}
-
-// 서비스센터 수정 페이지로 이동
-const goToEdit = (id) => {
-  router.push(`/site-manager/service-center/edit/${id}`)
-}
-
-// 서비스센터 삭제
-const deleteServiceCenter = (id) => {
-  showConfirm(
-    '정말 삭제하시겠습니까?',
-    async () => {
-      const { data, error } = await del(`/service-center/${id}`)
-
-      if (error || !data?.success) {
-        showAlert(error?.message || data?.message || '삭제에 실패했습니다.', '오류')
-      } else {
-        showAlert(data.message || '서비스센터가 삭제되었습니다.', '성공')
-        loadServiceCenters()
-      }
-    },
-    '서비스센터 삭제'
-  )
-}
-
-// 사용/비사용 토글
-const toggleActive = (id, currentStatus) => {
-  console.log('[toggleActive] 호출:', { id, currentStatus })
-  const statusText = currentStatus == 1 ? '비사용' : '사용'
-
-  showConfirm(
-    `서비스센터를 ${statusText} 상태로 변경하시겠습니까?`,
-    async () => {
-      console.log('[toggleActive] API 호출:', `/service-center/${id}/toggle-active`)
-
-      const { data, error } = await post(`/service-center/${id}/toggle-active`)
-
-      console.log('[toggleActive] API 응답:', { data, error })
-
-      if (error || !data?.success) {
-        console.error('[toggleActive] 에러 상세:', error)
-        showAlert(error?.message || data?.message || '상태 변경에 실패했습니다.', '오류')
-      } else {
-        showAlert(data.message || '서비스센터 상태가 변경되었습니다.', '성공')
-        loadServiceCenters()
-      }
-    },
-    '상태 변경'
-  )
-}
-
-onMounted(() => {
-  loadServiceCenters()
-})
-</script>
-
-<style scoped>
-.admin--search-actions .admin--btn-primary {
-  background: var(--admin-accent-primary);
-  color: white;
-  border-color: var(--admin-accent-primary);
-  font-weight: 500;
-  padding: 8px 18px;
-  font-size: 13px;
-  border-radius: 8px;
-  transition: all 0.3s ease;
-}
-
-.admin--search-actions .admin--btn-primary:hover {
-  background: var(--admin-accent-hover);
-  border-color: var(--admin-accent-hover);
-  transform: translateY(-1px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-.admin--toggle-btn {
-  padding: 6px 16px;
-  font-size: 12px;
-  border-radius: 20px;
-  border: 1px solid #ddd;
-  background: #f5f5f5;
-  color: #666;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  font-weight: 500;
-}
-
-.admin--toggle-btn:hover {
-  border-color: #bbb;
-  background: #e8e8e8;
-}
-
-.admin--toggle-btn.is-active {
-  background: var(--admin-accent-primary);
-  color: white;
-  border-color: var(--admin-accent-primary);
-}
-
-.admin--toggle-btn.is-active:hover {
-  background: var(--admin-accent-hover);
-  border-color: var(--admin-accent-hover);
-}
-</style>

+ 0 - 234
app/pages/site-manager/service/brochure.vue

@@ -1,234 +0,0 @@
-<template>
-  <div class="admin--brochure-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="신청자명으로 검색"
-          @keyup.enter="handleSearch"
-        >
-        <button class="admin--btn admin--btn-primary" @click="handleSearch">
-          검색
-        </button>
-        <button class="admin--btn admin--btn-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn admin--btn-secondary" @click="handleExcelDownload">
-          엑셀 다운로드
-        </button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>신청자</th>
-            <th>지점</th>
-            <th>희망차종</th>
-            <th>핸드폰</th>
-            <th>구입예상</th>
-            <th>신청일자</th>
-            <th>상태</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="isLoading">
-            <td colspan="9" class="admin--table-loading">
-              데이터를 불러오는 중...
-            </td>
-          </tr>
-          <tr v-else-if="!brochures || brochures.length === 0">
-            <td colspan="9" class="admin--table-empty">
-              브로셔 요청이 없습니다.
-            </td>
-          </tr>
-          <tr v-else v-for="(brochure, index) in brochures" :key="brochure.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td class="admin--table-title">{{ brochure.name }}</td>
-            <td>{{ brochure.branch_name }}</td>
-            <td>{{ brochure.car_model }}</td>
-            <td>{{ brochure.phone }}</td>
-            <td>{{ brochure.purchase_plan }}</td>
-            <td>{{ formatDate(brochure.created_at) }}</td>
-            <td>
-              <select
-                v-model="brochure.status"
-                class="admin--status-select"
-                @change="handleStatusChange(brochure.id, brochure.status)"
-              >
-                <option value="접수">접수</option>
-                <option value="접수완료">접수완료</option>
-                <option value="계약완료">계약완료</option>
-                <option value="출고완료">출고완료</option>
-              </select>
-            </td>
-            <td>
-              <button
-                class="admin--btn-small admin--btn-small-danger"
-                @click="handleDelete(brochure.id)"
-              >
-                삭제
-              </button>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-      >
-        이전
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-      >
-        다음
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted } from 'vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const { get, put, del } = useApi()
-
-const isLoading = ref(false)
-const brochures = ref([])
-const searchKeyword = ref('')
-const currentPage = ref(1)
-const perPage = ref(10)
-const totalCount = ref(0)
-const totalPages = ref(0)
-
-const visiblePages = computed(() => {
-  const pages = []
-  const maxVisible = 5
-  let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
-  let end = Math.min(totalPages.value, start + maxVisible - 1)
-
-  if (end - start < maxVisible - 1) {
-    start = Math.max(1, end - maxVisible + 1)
-  }
-
-  for (let i = start; i <= end; i++) {
-    pages.push(i)
-  }
-
-  return pages
-})
-
-const loadBrochures = async () => {
-  isLoading.value = true
-
-  const params = {
-    page: currentPage.value,
-    per_page: perPage.value
-  }
-
-  if (searchKeyword.value) {
-    params.search_keyword = searchKeyword.value
-  }
-
-  const { data, error } = await get('/service/brochure', { params })
-
-  console.log('[Brochure] API 응답:', { data, error })
-
-  // API 응답: { success: true, data: { items, total }, message }
-  if (data?.success && data?.data) {
-    brochures.value = data.data.items || []
-    totalCount.value = data.data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-    console.log('[Brochure] 로드 성공:', brochures.value.length)
-  }
-
-  isLoading.value = false
-}
-
-const handleSearch = () => {
-  currentPage.value = 1
-  loadBrochures()
-}
-
-const handleReset = () => {
-  searchKeyword.value = ''
-  currentPage.value = 1
-  loadBrochures()
-}
-
-const changePage = (page) => {
-  if (page < 1 || page > totalPages.value) return
-  currentPage.value = page
-  loadBrochures()
-  window.scrollTo({ top: 0, behavior: 'smooth' })
-}
-
-const handleExcelDownload = () => {
-  const params = searchKeyword.value ? { search_keyword: searchKeyword.value } : {}
-  window.open(`/api/service/brochure/excel?${new URLSearchParams(params)}`, '_blank')
-}
-
-const handleStatusChange = async (id, status) => {
-  const { error } = await put(`/service/brochure/${id}/status`, { status })
-
-  if (error) {
-    alert('상태 변경에 실패했습니다.')
-    loadBrochures()
-  } else {
-    alert('상태가 변경되었습니다.')
-  }
-}
-
-const handleDelete = async (id) => {
-  if (!confirm('정말 삭제하시겠습니까?')) return
-
-  const { error } = await del(`/service/brochure/${id}`)
-
-  if (error) {
-    alert('삭제에 실패했습니다.')
-  } else {
-    alert('삭제되었습니다.')
-    loadBrochures()
-  }
-}
-
-const formatDate = (dateString) => {
-  if (!dateString) return '-'
-  const date = new Date(dateString)
-  return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`
-}
-
-onMounted(() => {
-  loadBrochures()
-})
-</script>

+ 1 - 1
app/pages/site-manager/species_quest/list.vue

@@ -77,7 +77,7 @@
                 />
               </div>
             </th>
-            <th>어종</th>
+            <th style="width: 140px;">어종</th>
             <th style="width: 100px;">구분</th>
             <th style="width: 68px;">최소금지</th>
             <th style="width: 68px;">최대길이</th>

+ 0 - 219
app/pages/site-manager/staff/advisor/create.vue

@@ -1,219 +0,0 @@
-<template>
-  <div class="admin--advisor-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 서비스센터 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">서비스센터 <span class="admin--required">*</span></label>
-        <select v-model="formData.service_center" class="admin--form-select" required>
-          <option value="">서비스센터를 선택하세요</option>
-          <option v-for="center in serviceCenters" :key="center.code" :value="center.code">
-            {{ center.code }} : {{ center.name }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.main_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        >
-      </div>
-
-      <!-- 직통번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">직통번호</label>
-        <input
-          v-model="formData.direct_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5679"
-        >
-      </div>
-
-      <!-- 사진 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사진</label>
-        <input
-          type="file"
-          accept="image/*"
-          class="admin--form-file"
-          @change="handlePhotoUpload"
-        >
-        <div v-if="photoPreview" class="admin--image-preview">
-          <img :src="photoPreview" alt="미리보기">
-          <button type="button" class="admin--btn-remove-image" @click="removePhoto">
-            삭제
-          </button>
-        </div>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { get, post, upload } = useApi()
-
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const photoPreview = ref(null)
-const photoFile = ref(null)
-
-const formData = ref({
-  service_center: '',
-  name: '',
-  main_phone: '',
-  direct_phone: '',
-  photo_url: ''
-})
-
-const serviceCenters = ref([
-  { code: 'CA', name: '성수서비스센터' },
-  { code: 'CB', name: '수원서비스센터' },
-  { code: 'CC', name: '대전서비스센터' },
-  { code: 'CD', name: '광주서비스센터' }
-])
-
-const handlePhotoUpload = (event) => {
-  const file = event.target.files[0]
-  if (!file) return
-
-  if (!file.type.startsWith('image/')) {
-    alert('이미지 파일만 업로드 가능합니다.')
-    return
-  }
-
-  photoFile.value = file
-
-  const reader = new FileReader()
-  reader.onload = (e) => {
-    photoPreview.value = e.target.result
-  }
-  reader.readAsDataURL(file)
-}
-
-const removePhoto = () => {
-  photoPreview.value = null
-  photoFile.value = null
-  formData.value.photo_url = ''
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.service_center) {
-    errorMessage.value = '서비스센터를 선택하세요.'
-    return
-  }
-
-  if (!formData.value.name) {
-    errorMessage.value = '이름을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let photoUrl = formData.value.photo_url
-
-    if (photoFile.value) {
-      const formDataImage = new FormData()
-      formDataImage.append('file', photoFile.value)
-
-      const { data: uploadData, error: uploadError } = await upload('/upload/advisor-image', formDataImage)
-
-      if (uploadError) {
-        errorMessage.value = '사진 업로드에 실패했습니다.'
-        isSaving.value = false
-        return
-      }
-
-      if (!uploadData?.success || !uploadData?.data?.url) {
-        errorMessage.value = '사진 업로드 응답이 올바르지 않습니다.'
-        isSaving.value = false
-        return
-      }
-
-      photoUrl = uploadData.data.url
-    }
-
-    const submitData = {
-      ...formData.value,
-      photo_url: photoUrl
-    }
-
-    const { data, error } = await post('/staff/advisor', submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '등록에 실패했습니다.'
-    } else {
-      successMessage.value = '어드바이저가 등록되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/staff/advisor')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/staff/advisor')
-}
-</script>

+ 0 - 254
app/pages/site-manager/staff/advisor/edit/[id].vue

@@ -1,254 +0,0 @@
-<template>
-  <div class="admin--advisor-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 서비스센터 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">서비스센터 <span class="admin--required">*</span></label>
-        <select v-model="formData.service_center" class="admin--form-select" required>
-          <option value="">서비스센터를 선택하세요</option>
-          <option v-for="center in serviceCenters" :key="center.code" :value="center.code">
-            {{ center.code }} : {{ center.name }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.main_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        >
-      </div>
-
-      <!-- 직통번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">직통번호</label>
-        <input
-          v-model="formData.direct_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5679"
-        >
-      </div>
-
-      <!-- 사진 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사진</label>
-        <input
-          type="file"
-          accept="image/*"
-          class="admin--form-file"
-          @change="handlePhotoUpload"
-        >
-        <div v-if="photoPreview" class="admin--image-preview">
-          <img :src="photoPreview" alt="미리보기">
-          <button type="button" class="admin--btn-remove-image" @click="removePhoto">
-            삭제
-          </button>
-        </div>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const route = useRoute()
-const { get, put, upload } = useApi()
-const { getImageUrl } = useImage()
-
-const isSaving = ref(false)
-const isLoading = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const photoPreview = ref(null)
-const photoFile = ref(null)
-
-const formData = ref({
-  service_center: '',
-  name: '',
-  main_phone: '',
-  direct_phone: '',
-  photo_url: ''
-})
-
-const serviceCenters = ref([
-  { code: 'CA', name: '성수서비스센터' },
-  { code: 'CB', name: '수원서비스센터' },
-  { code: 'CC', name: '대전서비스센터' },
-  { code: 'CD', name: '광주서비스센터' }
-])
-
-const loadAdvisor = async () => {
-  isLoading.value = true
-
-  const id = route.params.id
-  const { data, error } = await get(`/staff/advisor/${id}`)
-  console.log('[AdvisorEdit] 데이터 로드:', { data, error })
-
-  if (data?.success && data?.data) {
-    const advisor = data.data
-    formData.value = {
-      service_center: advisor.service_center || '',
-      name: advisor.name || '',
-      main_phone: advisor.main_phone || '',
-      direct_phone: advisor.direct_phone || '',
-      photo_url: advisor.photo_url || ''
-    }
-
-    if (advisor.photo_url) {
-      photoPreview.value = getImageUrl(advisor.photo_url)
-    }
-
-    console.log('[AdvisorEdit] 로드 성공')
-  }
-
-  isLoading.value = false
-}
-
-const handlePhotoUpload = (event) => {
-  const file = event.target.files[0]
-  if (!file) return
-
-  if (!file.type.startsWith('image/')) {
-    alert('이미지 파일만 업로드 가능합니다.')
-    return
-  }
-
-  photoFile.value = file
-
-  const reader = new FileReader()
-  reader.onload = (e) => {
-    photoPreview.value = e.target.result
-  }
-  reader.readAsDataURL(file)
-}
-
-const removePhoto = () => {
-  photoPreview.value = null
-  photoFile.value = null
-  formData.value.photo_url = ''
-}
-
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (!formData.value.service_center) {
-    errorMessage.value = '서비스센터를 선택하세요.'
-    return
-  }
-
-  if (!formData.value.name) {
-    errorMessage.value = '이름을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let photoUrl = formData.value.photo_url
-
-    if (photoFile.value) {
-      const formDataImage = new FormData()
-      formDataImage.append('file', photoFile.value)
-
-      const { data: uploadData, error: uploadError } = await upload('/upload/advisor-image', formDataImage)
-
-      if (uploadError) {
-        errorMessage.value = '사진 업로드에 실패했습니다.'
-        isSaving.value = false
-        return
-      }
-
-      if (!uploadData?.success || !uploadData?.data?.url) {
-        errorMessage.value = '사진 업로드 응답이 올바르지 않습니다.'
-        isSaving.value = false
-        return
-      }
-
-      photoUrl = uploadData.data.url
-    }
-
-    const submitData = {
-      ...formData.value,
-      photo_url: photoUrl
-    }
-
-    const id = route.params.id
-    const { data, error } = await put(`/staff/advisor/${id}`, submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
-    } else {
-      successMessage.value = '어드바이저가 수정되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/staff/advisor')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/staff/advisor')
-}
-
-onMounted(() => {
-  loadAdvisor()
-})
-</script>

+ 0 - 252
app/pages/site-manager/staff/advisor/index.vue

@@ -1,252 +0,0 @@
-<template>
-  <div class="admin--advisor-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <select v-model="searchType" class="admin--form-select admin--search-select">
-          <option value="all">전체</option>
-          <option value="name">이름</option>
-          <option value="service_center">서비스센터</option>
-        </select>
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="검색어를 입력하세요"
-          @keyup.enter="handleSearch"
-        />
-        <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">
-          검색
-        </button>
-        <button class="admin--btn-small admin--btn-small-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">
-          + 어드바이저 등록
-        </button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>사진</th>
-            <th>서비스센터</th>
-            <th>이름</th>
-            <th>대표번호</th>
-            <th>핸드폰</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!advisors || advisors.length === 0">
-            <td colspan="7" class="admin--table-empty">등록된 어드바이저가 없습니다.</td>
-          </tr>
-          <tr v-else v-for="(advisor, index) in advisors" :key="advisor.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td>
-              <div class="admin--table-photo">
-                <img
-                  v-if="advisor.photo_url"
-                  :src="getImageUrl(advisor.photo_url)"
-                  :alt="advisor.name"
-                />
-                <div v-else class="admin--table-photo-empty">사진없음</div>
-              </div>
-            </td>
-            <td>{{ advisor.service_center_name }}</td>
-            <td class="admin--table-title">{{ advisor.name }}</td>
-            <td>{{ advisor.main_phone }}</td>
-            <td>{{ advisor.direct_phone }}</td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(advisor.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(advisor.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-  import { ref, computed, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const router = useRouter();
-  const { get, del } = useApi();
-  const { getImageUrl } = useImage();
-  const advisors = ref([]);
-  const searchType = ref("all");
-  const searchKeyword = ref("");
-  const currentPage = ref(1);
-  const perPage = ref(10);
-  const totalCount = ref(0);
-  const totalPages = ref(0);
-
-  const visiblePages = computed(() => {
-    const pages = [];
-    const maxVisible = 5;
-    let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
-    let end = Math.min(totalPages.value, start + maxVisible - 1);
-
-    if (end - start < maxVisible - 1) {
-      start = Math.max(1, end - maxVisible + 1);
-    }
-
-    for (let i = start; i <= end; i++) {
-      pages.push(i);
-    }
-
-    return pages;
-  });
-
-  const loadAdvisors = async () => {
-    const params = {
-      page: currentPage.value,
-      per_page: perPage.value,
-    };
-
-    if (searchKeyword.value) {
-      params.search_type = searchType.value;
-      params.search_keyword = searchKeyword.value;
-    }
-
-    const { data, error } = await get("/staff/advisor", { params });
-
-    console.log("[Advisor] API 응답:", { data, error });
-
-    // API 응답: { success: true, data: { items, total }, message }
-    if (data?.success && data?.data) {
-      advisors.value = data.data.items || [];
-      totalCount.value = data.data.total || 0;
-      totalPages.value = Math.ceil(totalCount.value / perPage.value);
-      console.log("[Advisor] 로드 성공:", advisors.value.length);
-    }
-  };
-
-  const handleSearch = () => {
-    currentPage.value = 1;
-    loadAdvisors();
-  };
-
-  const handleReset = () => {
-    searchType.value = "all";
-    searchKeyword.value = "";
-    currentPage.value = 1;
-    loadAdvisors();
-  };
-
-  const changePage = (page) => {
-    if (page < 1 || page > totalPages.value) return;
-    currentPage.value = page;
-    loadAdvisors();
-    window.scrollTo({ top: 0, behavior: 'smooth' });
-  };
-
-  const goToCreate = () => {
-    router.push("/site-manager/staff/advisor/create");
-  };
-
-  const goToEdit = (id) => {
-    router.push(`/site-manager/staff/advisor/edit/${id}`);
-  };
-
-  const handleDelete = async (id) => {
-    if (!confirm("정말 삭제하시겠습니까?")) return;
-
-    const { error } = await del(`/staff/advisor/${id}`);
-
-    if (error) {
-      alert("삭제에 실패했습니다.");
-    } else {
-      alert("삭제되었습니다.");
-      loadAdvisors();
-    }
-  };
-
-  onMounted(() => {
-    loadAdvisors();
-  });
-</script>
-
-<style scoped>
-  /* 검색 영역 input/select 스타일 통일 */
-  .admin--search-box .admin--form-input,
-  .admin--search-box .admin--search-input,
-  .admin--search-box .admin--form-select,
-  .admin--search-box .admin--search-select {
-    border: 1px solid var(--admin-border-color) !important;
-    border-radius: 4px;
-    height: 33px !important;
-    padding: 6px 14px !important;
-    font-size: 13px !important;
-  }
-</style>

+ 0 - 430
app/pages/site-manager/staff/sales/create.vue

@@ -1,430 +0,0 @@
-<template>
-  <div class="admin--sales-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 전시장 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >전시장 <span class="admin--required">*</span></label
-        >
-        <select v-model.number="formData.showroom" class="admin--form-select" required>
-          <option value="">전시장을 선택하세요</option>
-          <option
-            v-for="showroom in showrooms"
-            :key="showroom.id"
-            :value="showroom.id"
-            :disabled="showroom.is_active !== 1 && showroom.is_active !== '1'"
-          >
-            {{ showroom.name
-            }}{{
-              showroom.is_active !== 1 && showroom.is_active !== "1" ? " (비활성화)" : ""
-            }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 영업팀 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >영업팀 <span class="admin--required">*</span></label
-        >
-        <select v-model.number="formData.team" class="admin--form-select" required>
-          <option value="">영업팀을 선택하세요</option>
-          <option v-for="team in teams" :key="team.id" :value="team.id">
-            {{ team.name }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >이름 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 직책 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >직책 <span class="admin--required">*</span></label
-        >
-        <select v-model.number="formData.position" class="admin--form-select" required>
-          <option value="">직책을 선택하세요</option>
-          <option v-for="position in positions" :key="position.value" :value="position.value">
-            {{ position.label }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >대표번호 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.main_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        />
-      </div>
-
-      <!-- 직통번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">직통번호</label>
-        <input
-          v-model="formData.direct_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5679"
-        />
-      </div>
-
-      <!-- 핸드폰 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">핸드폰</label>
-        <input
-          v-model="formData.mobile"
-          type="tel"
-          class="admin--form-input"
-          placeholder="010-1234-5678"
-        />
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일</label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="email@example.com"
-        />
-      </div>
-
-      <!-- 사진 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사진</label>
-        <input
-          type="file"
-          accept="image/*"
-          class="admin--form-file"
-          @change="handlePhotoUpload"
-        />
-        <div v-if="photoPreview" class="admin--image-preview">
-          <img :src="photoPreview" alt="미리보기" />
-          <button type="button" class="admin--btn-remove-image" @click="removePhoto">
-            삭제
-          </button>
-        </div>
-      </div>
-
-      <!-- SACT -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">SACT</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input v-model="formData.is_sact" type="radio" :value="true" name="is_sact" />
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_sact"
-              type="radio"
-              :value="false"
-              name="is_sact"
-            />
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- TOP30 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">TOP30</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_top30"
-              type="radio"
-              :value="true"
-              name="is_top30"
-            />
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_top30"
-              type="radio"
-              :value="false"
-              name="is_top30"
-            />
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- Best Sales Advisor -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">Best Sales Advisor</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_best_advisor"
-              type="radio"
-              :value="true"
-              name="is_best_advisor"
-            />
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_best_advisor"
-              type="radio"
-              :value="false"
-              name="is_best_advisor"
-            />
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- Best Sales Advisor (Year) -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">Best Sales Advisor (Year)</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_best_advisor_year"
-              type="radio"
-              :value="true"
-              name="is_best_advisor_year"
-            />
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input
-              v-model="formData.is_best_advisor_year"
-              type="radio"
-              :value="false"
-              name="is_best_advisor_year"
-            />
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 노출순서 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">노출순서</label>
-        <input
-          v-model.number="formData.display_order"
-          type="number"
-          class="admin--form-input"
-          placeholder="숫자만 입력"
-          min="0"
-        />
-        <p class="admin--form-help">숫자가 작을수록 먼저 노출됩니다.</p>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
-          {{ isSaving ? "저장 중..." : "확인" }}
-        </button>
-        <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-  import { ref, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const router = useRouter();
-  const { get, post, upload } = useApi();
-  const { teams, positions } = useSalesData();
-
-  const isSaving = ref(false);
-  const successMessage = ref("");
-  const errorMessage = ref("");
-  const photoPreview = ref(null);
-  const photoFile = ref(null);
-
-  const showrooms = ref([]);
-
-  const formData = ref({
-    showroom: "",
-    team: "",
-    name: "",
-    position: "",
-    main_phone: "",
-    direct_phone: "",
-    mobile: "",
-    email: "",
-    photo_url: "",
-    is_sact: false,
-    is_top30: false,
-    is_best_advisor: false,
-    is_best_advisor_year: false,
-    display_order: 0,
-  });
-
-  // 필터 데이터 로드
-  const loadFilters = async () => {
-    // 전시장 리스트 (지점 목록)
-    const { data: branchData, error: branchError } = await get("/branch/list", {
-      per_page: 1000,
-    });
-    console.log("[SalesCreate] 전시장(지점) API 응답:", {
-      data: branchData,
-      error: branchError,
-    });
-    if (branchData?.success && branchData?.data) {
-      showrooms.value = branchData.data.items || [];
-      console.log("[SalesCreate] 전시장(지점) 로드 성공");
-    }
-  };
-
-  // 사진 업로드
-  const handlePhotoUpload = (event) => {
-    const file = event.target.files[0];
-    if (!file) return;
-
-    if (!file.type.startsWith("image/")) {
-      alert("이미지 파일만 업로드 가능합니다.");
-      return;
-    }
-
-    photoFile.value = file;
-
-    const reader = new FileReader();
-    reader.onload = (e) => {
-      photoPreview.value = e.target.result;
-    };
-    reader.readAsDataURL(file);
-  };
-
-  // 사진 삭제
-  const removePhoto = () => {
-    photoPreview.value = null;
-    photoFile.value = null;
-    formData.value.photo_url = "";
-  };
-
-  // 폼 제출
-  const handleSubmit = async () => {
-    successMessage.value = "";
-    errorMessage.value = "";
-
-    if (formData.value.showroom === "" || formData.value.showroom === null) {
-      errorMessage.value = "전시장을 선택하세요.";
-      return;
-    }
-
-    if (formData.value.team === "" || formData.value.team === null) {
-      errorMessage.value = "영업팀을 선택하세요.";
-      return;
-    }
-
-    if (!formData.value.name) {
-      errorMessage.value = "이름을 입력하세요.";
-      return;
-    }
-
-    if (formData.value.position === "" || formData.value.position === null) {
-      errorMessage.value = "직책을 선택하세요.";
-      return;
-    }
-
-    isSaving.value = true;
-
-    try {
-      let photoUrl = formData.value.photo_url;
-
-      // 사진 업로드
-      if (photoFile.value) {
-        const formDataImage = new FormData();
-        formDataImage.append("file", photoFile.value);
-
-        const { data: uploadData, error: uploadError } = await upload(
-          "/upload/staff-image",
-          formDataImage
-        );
-
-        console.log("[SalesCreate] 이미지 업로드 응답:", {
-          data: uploadData,
-          error: uploadError,
-        });
-
-        if (uploadError) {
-          errorMessage.value =
-            "사진 업로드에 실패했습니다: " + (uploadError.message || uploadError);
-          isSaving.value = false;
-          return;
-        }
-
-        if (!uploadData?.success || !uploadData?.data?.url) {
-          errorMessage.value = "사진 업로드 응답이 올바르지 않습니다.";
-          isSaving.value = false;
-          return;
-        }
-
-        photoUrl = uploadData.data.url;
-        console.log("[SalesCreate] 업로드된 이미지 URL:", photoUrl);
-      }
-
-      const submitData = {
-        ...formData.value,
-        photo_url: photoUrl,
-      };
-
-      const { data, error } = await post("/staff/sales", submitData);
-
-      if (error) {
-        errorMessage.value = error.message || "등록에 실패했습니다.";
-      } else {
-        successMessage.value = "영업사원이 등록되었습니다.";
-        setTimeout(() => {
-          router.push("/site-manager/staff/sales");
-        }, 1000);
-      }
-    } catch (error) {
-      errorMessage.value = "서버 오류가 발생했습니다.";
-      console.error("Save error:", error);
-    } finally {
-      isSaving.value = false;
-    }
-  };
-
-  const goToList = () => {
-    router.push("/site-manager/staff/sales");
-  };
-
-  onMounted(() => {
-    loadFilters();
-  });
-</script>

+ 0 - 440
app/pages/site-manager/staff/sales/edit/[id].vue

@@ -1,440 +0,0 @@
-<template>
-  <div class="admin--sales-form">
-    <div v-if="isLoading" class="admin--loading">
-      데이터를 불러오는 중...
-    </div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 전시장 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">전시장 <span class="admin--required">*</span></label>
-        <select v-model.number="formData.showroom" class="admin--form-select" required>
-          <option value="">전시장을 선택하세요</option>
-          <option
-            v-for="showroom in showrooms"
-            :key="showroom.id"
-            :value="showroom.id"
-            :disabled="showroom.is_active !== 1 && showroom.is_active !== '1'"
-          >
-            {{ showroom.name }}{{ (showroom.is_active !== 1 && showroom.is_active !== '1') ? ' (비활성화)' : '' }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 영업팀 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">영업팀 <span class="admin--required">*</span></label>
-        <select v-model.number="formData.team" class="admin--form-select" required>
-          <option value="">영업팀을 선택하세요</option>
-          <option v-for="team in teams" :key="team.id" :value="team.id">
-            {{ team.name }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 이름 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 직책 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
-        <select v-model.number="formData.position" class="admin--form-select" required>
-          <option value="">직책을 선택하세요</option>
-          <option v-for="position in positions" :key="position.value" :value="position.value">
-            {{ position.label }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.main_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        >
-      </div>
-
-      <!-- 직통번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">직통번호</label>
-        <input
-          v-model="formData.direct_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5679"
-        >
-      </div>
-
-      <!-- 핸드폰 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">핸드폰</label>
-        <input
-          v-model="formData.mobile"
-          type="tel"
-          class="admin--form-input"
-          placeholder="010-1234-5678"
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일</label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="email@example.com"
-        >
-      </div>
-
-      <!-- 사진 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사진</label>
-        <input
-          type="file"
-          accept="image/*"
-          class="admin--form-file"
-          @change="handlePhotoUpload"
-        >
-        <div v-if="photoPreview || formData.photo_url" class="admin--image-preview">
-          <img :src="photoPreview || getImageUrl(formData.photo_url)" alt="미리보기">
-          <button type="button" class="admin--btn-remove-image" @click="removePhoto">
-            삭제
-          </button>
-        </div>
-      </div>
-
-      <!-- SACT -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">SACT</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input v-model="formData.is_sact" type="radio" :value="true" name="is_sact">
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input v-model="formData.is_sact" type="radio" :value="false" name="is_sact">
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- GOLDPLT -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">GOLDPLT</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input v-model="formData.is_goldplt" type="radio" :value="true" name="is_goldplt">
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input v-model="formData.is_goldplt" type="radio" :value="false" name="is_goldplt">
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- TOP30 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">TOP30</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input v-model="formData.is_top30" type="radio" :value="true" name="is_top30">
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input v-model="formData.is_top30" type="radio" :value="false" name="is_top30">
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- Best Sales Advisor -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">Best Sales Advisor</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input v-model="formData.is_best_advisor" type="radio" :value="true" name="is_best_advisor">
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input v-model="formData.is_best_advisor" type="radio" :value="false" name="is_best_advisor">
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- Best Sales Advisor (Year) -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">Best Sales Advisor (Year)</label>
-        <div class="admin--radio-group">
-          <label class="admin--radio-label">
-            <input v-model="formData.is_best_advisor_year" type="radio" :value="true" name="is_best_advisor_year">
-            <span>예</span>
-          </label>
-          <label class="admin--radio-label">
-            <input v-model="formData.is_best_advisor_year" type="radio" :value="false" name="is_best_advisor_year">
-            <span>아니오</span>
-          </label>
-        </div>
-      </div>
-
-      <!-- 노출순서 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">노출순서</label>
-        <input
-          v-model.number="formData.display_order"
-          type="number"
-          class="admin--form-input"
-          placeholder="숫자만 입력"
-          min="0"
-        >
-        <p class="admin--form-help">숫자가 작을수록 먼저 노출됩니다.</p>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const route = useRoute()
-const router = useRouter()
-const { get, put, upload } = useApi()
-const { getImageUrl } = useImage()
-const { teams, positions } = useSalesData()
-
-const isLoading = ref(true)
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const photoPreview = ref(null)
-const photoFile = ref(null)
-
-const showrooms = ref([])
-
-const formData = ref({
-  showroom: '',
-  team: '',
-  name: '',
-  position: '',
-  main_phone: '',
-  direct_phone: '',
-  mobile: '',
-  email: '',
-  photo_url: '',
-  is_sact: false,
-  is_goldplt: false,
-  is_top30: false,
-  is_best_advisor: false,
-  is_best_advisor_year: false,
-  display_order: 0
-})
-
-// 필터 데이터 로드
-const loadFilters = async () => {
-  // 전시장 리스트 (지점 목록)
-  const { data: branchData, error: branchError } = await get('/branch/list', { per_page: 1000 })
-  console.log('[SalesEdit] 전시장(지점) API 응답:', { data: branchData, error: branchError })
-  if (branchData?.success && branchData?.data) {
-    showrooms.value = branchData.data.items || []
-    console.log('[SalesEdit] 전시장(지점) 로드 성공')
-  }
-}
-
-// 데이터 로드
-const loadSales = async () => {
-  isLoading.value = true
-
-  const id = route.params.id
-  const { data, error } = await get(`/staff/sales/${id}`)
-  console.log('[SalesEdit] 데이터 로드:', { data, error })
-
-  if (data?.success && data?.data) {
-    const sales = data.data
-    formData.value = {
-      showroom: sales.showroom || '',
-      team: sales.team || '',
-      name: sales.name || '',
-      position: sales.position || '',
-      main_phone: sales.main_phone || '',
-      direct_phone: sales.direct_phone || '',
-      mobile: sales.mobile || '',
-      email: sales.email || '',
-      photo_url: sales.photo_url || '',
-      is_sact: sales.is_sact == 1 ? true : false,
-      is_goldplt: sales.is_goldplt == 1 ? true : false,
-      is_top30: sales.is_top30 == 1 ? true : false,
-      is_best_advisor: sales.is_best_advisor == 1 ? true : false,
-      is_best_advisor_year: sales.is_best_advisor_year == 1 ? true : false,
-      display_order: sales.display_order || 0
-    }
-    // photoPreview는 새 이미지 업로드시에만 사용
-    photoPreview.value = null
-    console.log('[SalesEdit] 로드 성공:', formData.value)
-    console.log('[SalesEdit] is_sact:', sales.is_sact, '->', formData.value.is_sact)
-    console.log('[SalesEdit] is_top30:', sales.is_top30, '->', formData.value.is_top30)
-  }
-
-  isLoading.value = false
-}
-
-// 사진 업로드
-const handlePhotoUpload = (event) => {
-  const file = event.target.files[0]
-  if (!file) return
-
-  if (!file.type.startsWith('image/')) {
-    alert('이미지 파일만 업로드 가능합니다.')
-    return
-  }
-
-  photoFile.value = file
-
-  const reader = new FileReader()
-  reader.onload = (e) => {
-    photoPreview.value = e.target.result
-  }
-  reader.readAsDataURL(file)
-}
-
-// 사진 삭제
-const removePhoto = () => {
-  photoPreview.value = null
-  photoFile.value = null
-  formData.value.photo_url = ''
-}
-
-// 폼 제출
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  if (formData.value.showroom === '' || formData.value.showroom === null) {
-    errorMessage.value = '전시장을 선택하세요.'
-    return
-  }
-
-  if (formData.value.team === '' || formData.value.team === null) {
-    errorMessage.value = '영업팀을 선택하세요.'
-    return
-  }
-
-  if (!formData.value.name) {
-    errorMessage.value = '이름을 입력하세요.'
-    return
-  }
-
-  if (formData.value.position === '' || formData.value.position === null) {
-    errorMessage.value = '직책을 선택하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let photoUrl = formData.value.photo_url
-
-    // 새 사진 업로드
-    if (photoFile.value) {
-      const formDataImage = new FormData()
-      formDataImage.append('file', photoFile.value)
-
-      const { data: uploadData, error: uploadError } = await upload('/upload/staff-image', formDataImage)
-
-      console.log('[SalesEdit] 이미지 업로드 응답:', { data: uploadData, error: uploadError })
-
-      if (uploadError) {
-        errorMessage.value = '사진 업로드에 실패했습니다: ' + (uploadError.message || uploadError)
-        isSaving.value = false
-        return
-      }
-
-      if (!uploadData?.success || !uploadData?.data?.url) {
-        errorMessage.value = '사진 업로드 응답이 올바르지 않습니다.'
-        isSaving.value = false
-        return
-      }
-
-      photoUrl = uploadData.data.url
-      console.log('[SalesEdit] 업로드된 이미지 URL:', photoUrl)
-    }
-
-    const submitData = {
-      ...formData.value,
-      photo_url: photoUrl
-    }
-
-    const id = route.params.id
-    const { data, error } = await put(`/staff/sales/${id}`, submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
-    } else {
-      successMessage.value = '영업사원 정보가 수정되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/staff/sales')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-const goToList = () => {
-  router.push('/site-manager/staff/sales')
-}
-
-onMounted(async () => {
-  await loadFilters()
-  await loadSales()
-})
-</script>

+ 0 - 798
app/pages/site-manager/staff/sales/index.vue

@@ -1,798 +0,0 @@
-<template>
-  <div class="admin--sales-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box admin--search-box-large">
-      <div class="admin--search-filters">
-        <div class="admin--filter-row">
-          <label class="admin--filter-label">전시장</label>
-          <select v-model="filters.showroom" class="admin--form-select">
-            <option value="">전체</option>
-            <option v-for="showroom in showrooms" :key="showroom.id" :value="showroom.id">
-              {{ showroom.name }}
-            </option>
-          </select>
-
-          <label class="admin--filter-label">팀</label>
-          <select v-model="filters.team" class="admin--form-select">
-            <option value="">전체</option>
-            <option v-for="team in teams" :key="team.id" :value="team.id">
-              {{ team.name }}
-            </option>
-          </select>
-
-          <label class="admin--filter-label">직책</label>
-          <select v-model="filters.position" class="admin--form-select">
-            <option value="">전체</option>
-            <option
-              v-for="position in positions"
-              :key="position.value"
-              :value="position.value"
-            >
-              {{ position.label }}
-            </option>
-          </select>
-        </div>
-
-        <div class="admin--filter-row">
-          <label class="admin--filter-label">검색어</label>
-          <input
-            v-model="filters.keyword"
-            type="text"
-            class="admin--form-input"
-            placeholder="이름으로 검색"
-            @keyup.enter="handleSearch"
-          />
-          <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">
-            검색
-          </button>
-          <button
-            class="admin--btn-small admin--btn-small-secondary"
-            @click="handleReset"
-          >
-            초기화
-          </button>
-        </div>
-      </div>
-
-      <div class="admin--search-actions">
-        <button
-          class="admin--btn-small admin--btn-small-excel"
-          @click="handleExcelDownload"
-        >
-          엑셀 다운로드
-        </button>
-        <button
-          class="admin--btn-small admin--btn-small-secondary"
-          @click="handleA2Print"
-        >
-          A2 출력하기
-        </button>
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">
-          + 사원 등록
-        </button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table admin--table-sales">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>사진</th>
-            <th>전시장</th>
-            <th>팀</th>
-            <th>이름</th>
-            <th>직책</th>
-            <th>대표번호</th>
-            <th>핸드폰</th>
-            <th>이메일</th>
-            <th>상태</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!salesList || salesList.length === 0">
-            <td colspan="11" class="admin--table-empty">등록된 영업사원이 없습니다.</td>
-          </tr>
-          <tr v-else v-for="(sales, index) in salesList" :key="sales.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td>
-              <div class="admin--table-photo">
-                <img
-                  v-if="sales.photo_url"
-                  :src="getImageUrl(sales.photo_url)"
-                  :alt="sales.name"
-                />
-                <div v-else class="admin--table-photo-empty">사진없음</div>
-              </div>
-            </td>
-            <td>{{ getShowroomName(sales.showroom) }}</td>
-            <td>{{ getTeamName(sales.team) }}</td>
-            <td class="admin--table-title">{{ sales.name }}</td>
-            <td>{{ getPositionName(sales.position) }}</td>
-            <td>{{ sales.direct_phone || "-" }}</td>
-            <td>{{ sales.main_phone }}</td>
-            <td>{{ sales.email }}</td>
-            <td>
-              <button
-                class="admin--toggle-btn"
-                :class="{ 'is-active': sales.is_act == 1 }"
-                @click="toggleActive(sales.id, sales.is_act)"
-              >
-                {{ sales.is_act == 1 ? '노출' : '비노출' }}
-              </button>
-            </td>
-            <td>
-              <div class="admin--table-actions admin--table-actions-col">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(sales.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(sales.id)"
-                >
-                  삭제
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-secondary"
-                  @click="handlePrint(sales.id)"
-                >
-                  프린트
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-excel"
-                  @click="handleExcelExport(sales.id)"
-                >
-                  엑셀출력
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-
-    <!-- 알림 모달 -->
-    <AdminAlertModal
-      v-if="alertModal.show"
-      :title="alertModal.title"
-      :message="alertModal.message"
-      :type="alertModal.type"
-      @confirm="handleAlertConfirm"
-      @cancel="handleAlertCancel"
-      @close="closeAlertModal"
-    />
-  </div>
-</template>
-
-<script setup>
-  import { ref, computed, onMounted } from "vue";
-  import { useRouter } from "vue-router";
-  import AdminAlertModal from '~/components/admin/AdminAlertModal.vue';
-
-  definePageMeta({
-    layout: "admin",
-    middleware: ["auth"],
-  });
-
-  const router = useRouter();
-  const { get, del, post } = useApi();
-  const { getImageUrl } = useImage();
-  const { teams, positions, getTeamName, getPositionName } = useSalesData();
-
-  const salesList = ref([]);
-  const showrooms = ref([]);
-
-  const filters = ref({
-    showroom: "",
-    team: "",
-    position: "",
-    keyword: "",
-  });
-
-  const currentPage = ref(1);
-  const perPage = ref(10);
-  const totalCount = ref(0);
-  const totalPages = ref(0);
-
-  // 알림 모달
-  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 showConfirm = (message, onConfirm, title = '확인') => {
-    alertModal.value = {
-      show: true,
-      title,
-      message,
-      type: 'confirm',
-      onConfirm
-    }
-  };
-
-  // 알림 모달 닫기
-  const closeAlertModal = () => {
-    alertModal.value.show = false
-  };
-
-  // 알림 모달 확인
-  const handleAlertConfirm = () => {
-    if (alertModal.value.onConfirm) {
-      alertModal.value.onConfirm()
-    }
-    closeAlertModal()
-  };
-
-  // 알림 모달 취소
-  const handleAlertCancel = () => {
-    closeAlertModal()
-  };
-
-  // 보이는 페이지 번호 계산
-  const visiblePages = computed(() => {
-    const pages = [];
-    const maxVisible = 5;
-    let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
-    let end = Math.min(totalPages.value, start + maxVisible - 1);
-
-    if (end - start < maxVisible - 1) {
-      start = Math.max(1, end - maxVisible + 1);
-    }
-
-    for (let i = start; i <= end; i++) {
-      pages.push(i);
-    }
-
-    return pages;
-  });
-
-  // 필터 데이터 로드
-  const loadFilters = async () => {
-    // 전시장 리스트 (지점 목록)
-    const { data: branchData, error: branchError } = await get("/branch/list", {
-      params: {
-        per_page: 1000,
-      },
-    });
-    console.log("[SalesList] 전시장(지점) API 응답:", {
-      data: branchData,
-      error: branchError,
-    });
-    if (branchData?.success && branchData?.data) {
-      showrooms.value = branchData.data.items || [];
-      console.log("[SalesList] 전시장(지점) 로드 성공");
-    }
-  };
-
-  // 데이터 로드
-  const loadSales = async () => {
-    const params = {
-      page: currentPage.value,
-      per_page: perPage.value,
-      ...filters.value,
-    };
-
-    const { data, error } = await get("/staff/sales", { params });
-    console.log("[SalesList] 영업사원 목록 API 응답:", { data, error });
-
-    if (data?.success && data?.data) {
-      salesList.value = data.data.items || [];
-      totalCount.value = data.data.total || 0;
-      totalPages.value = Math.ceil(totalCount.value / perPage.value);
-      console.log("[SalesList] 영업사원 목록 로드 성공");
-    }
-  };
-
-  // 검색
-  const handleSearch = () => {
-    currentPage.value = 1;
-    loadSales();
-  };
-
-  // 초기화
-  const handleReset = () => {
-    filters.value = {
-      showroom: "",
-      team: "",
-      position: "",
-      keyword: "",
-    };
-    currentPage.value = 1;
-    loadSales();
-  };
-
-  // 전시장 이름 가져오기
-  const getShowroomName = (showroomId) => {
-    const showroom = showrooms.value.find((s) => s.id === showroomId);
-    return showroom ? showroom.name : "-";
-  };
-
-  // 페이지 변경
-  const changePage = (page) => {
-    if (page < 1 || page > totalPages.value) return;
-    currentPage.value = page;
-    loadSales();
-    window.scrollTo({ top: 0, behavior: "smooth" });
-  };
-
-  // 엑셀 다운로드 (전체)
-  const handleExcelDownload = async () => {
-    // 전체 데이터 가져오기 (페이지네이션 없이)
-    const params = {
-      ...filters.value,
-      per_page: 10000, // 충분히 큰 값으로 전체 가져오기
-    };
-
-    const { data } = await get("/staff/sales", { params });
-    if (!data?.success || !data?.data) {
-      alert("데이터를 가져올 수 없습니다.");
-      return;
-    }
-
-    const allStaff = data.data.items || [];
-
-    if (allStaff.length === 0) {
-      alert("다운로드할 데이터가 없습니다.");
-      return;
-    }
-
-    // HTML 테이블 생성
-    let tableRows = "";
-    allStaff.forEach((staff, index) => {
-      const showroomName = getShowroomName(staff.showroom);
-      const teamName = getTeamName(staff.team);
-      const positionName = getPositionName(staff.position);
-      const photoUrl = staff.photo_url ? getImageUrl(staff.photo_url) : "";
-
-      tableRows += `
-        <tr style="height: 110px;">
-          <td style="text-align: center; vertical-align: middle;">${index + 1}</td>
-          <td style="text-align: center; padding: 5px; vertical-align: middle;">
-            ${
-              photoUrl
-                ? `<img src="${photoUrl}" height="100" style="display: block; margin: 0 auto; max-width: 100%;" />`
-                : "사진없음"
-            }
-          </td>
-          <td style="vertical-align: middle;">${showroomName}</td>
-          <td style="vertical-align: middle;">${teamName}</td>
-          <td style="vertical-align: middle;">${staff.name}</td>
-          <td style="vertical-align: middle;">${positionName}</td>
-          <td style="vertical-align: middle;">${staff.direct_phone || "-"}</td>
-          <td style="vertical-align: middle;">${staff.main_phone || "-"}</td>
-          <td style="vertical-align: middle;">${staff.email || ""}</td>
-        </tr>
-      `;
-    });
-
-    const html = `
-<html xmlns:x="urn:schemas-microsoft-com:office:excel">
-<head>
-    <meta charset="UTF-8">
-    <style>
-        body {
-            font-family: "Malgun Gothic", "맑은 고딕", Arial, sans-serif;
-        }
-        table {
-            border-collapse: collapse;
-            width: 100%;
-        }
-        th, td {
-            border: 1px solid #ddd;
-            padding: 10px;
-            text-align: left;
-        }
-        th {
-            background-color: #f5f5f5;
-            font-weight: bold;
-            text-align: center;
-        }
-        .title {
-            font-size: 18pt;
-            font-weight: bold;
-            margin-bottom: 20px;
-            text-align: center;
-        }
-    </style>
-</head>
-<body>
-    <div class="title">영업사원 목록</div>
-    <table>
-        <thead>
-            <tr>
-                <th>NO</th>
-                <th>사진</th>
-                <th>전시장</th>
-                <th>팀</th>
-                <th>이름</th>
-                <th>직책</th>
-                <th>대표번호</th>
-                <th>핸드폰</th>
-                <th>이메일</th>
-            </tr>
-        </thead>
-        <tbody>
-            ${tableRows}
-        </tbody>
-    </table>
-</body>
-</html>`;
-
-    // Blob 생성 및 다운로드
-    const blob = new Blob(["\ufeff" + html], {
-      type: "application/vnd.ms-excel;charset=utf-8;",
-    });
-    const link = document.createElement("a");
-    const url = URL.createObjectURL(blob);
-    link.setAttribute("href", url);
-    link.setAttribute(
-      "download",
-      `sales_staff_list_${new Date().toISOString().split("T")[0]}.xls`
-    );
-    link.style.visibility = "hidden";
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
-  };
-
-  // A2 출력
-  const handleA2Print = () => {
-    window.open("/site-manager/staff/sales/print-a2", "_blank");
-  };
-
-  // 개별 프린트
-  const handlePrint = (id) => {
-    window.open(`/site-manager/staff/sales/print/${id}`, "_blank");
-  };
-
-  // 개별 엑셀 출력
-  const handleExcelExport = async (id) => {
-    // 해당 사원 데이터 가져오기
-    const { data } = await get(`/staff/sales/${id}`);
-    if (!data?.success || !data?.data) {
-      alert("데이터를 가져올 수 없습니다.");
-      return;
-    }
-
-    const staff = data.data;
-
-    // 전시장 이름
-    const showroomName = getShowroomName(staff.showroom);
-    // 팀 이름
-    const teamName = getTeamName(staff.team);
-    // 직책 이름
-    const positionName = getPositionName(staff.position);
-
-    // 사진 URL
-    const photoUrl = staff.photo_url ? getImageUrl(staff.photo_url) : "";
-
-    // HTML 생성 (엑셀 호환 테이블 레이아웃)
-    const html = `
-<html xmlns:x="urn:schemas-microsoft-com:office:excel">
-<head>
-    <meta charset="UTF-8">
-    <style>
-        body {
-            font-family: "Malgun Gothic", "맑은 고딕", Arial, sans-serif;
-        }
-        table {
-            border-collapse: collapse;
-            width: 100%;
-        }
-        .header-table {
-            width: 100%;
-            margin-bottom: 40px;
-            border-bottom: 2px solid #000;
-            padding-bottom: 10px;
-        }
-        .header-table td {
-            border: none;
-            padding: 10px;
-        }
-        .header-title {
-            font-size: 20pt;
-            font-weight: bold;
-            text-align: left;
-        }
-        .header-logo {
-            text-align: right;
-            font-size: 10pt;
-            color: #666;
-        }
-        .content-table {
-            width: 100%;
-            border: 1px solid #ddd;
-            margin-top: 20px;
-        }
-        .content-table td {
-            border: 1px solid #ddd;
-            vertical-align: top;
-            padding: 10px;
-        }
-        .info-wrapper {
-            padding-top: 0px;
-        }
-        .photo-cell {
-            width: 200px;
-            text-align: center;
-            vertical-align: top;
-        }
-        .photo-cell img {
-            width: 200px;
-            height: auto;
-            display: block;
-        }
-        .photo-placeholder {
-            width: 200px;
-            height: 300px;
-            border: 1px solid #ddd;
-            background-color: #f9f9f9;
-            display: inline-block;
-            line-height: 300px;
-            text-align: center;
-            color: #999;
-        }
-        .info-table {
-            width: 100%;
-            border-collapse: collapse;
-            border: none;
-        }
-        .info-table tr {
-            border: none;
-        }
-        .info-table td {
-            padding: 10px;
-            border: 1px solid #ddd;
-        }
-        .info-label {
-            width: 100px;
-            font-weight: bold;
-            color: #333;
-            font-size: 13pt;
-            background-color: #f5f5f5;
-        }
-        .info-value {
-            color: #555;
-            font-size: 13pt;
-        }
-        .info-value-name {
-            color: #000;
-            font-size: 16pt;
-            font-weight: bold;
-        }
-    </style>
-</head>
-<body>
-    <table class="content-table" style="table-layout: fixed;">
-        <colgroup>
-            <col style="width: 200px;">
-            <col style="width: 100px;">
-            <col style="width: auto;">
-        </colgroup>
-        <tr>
-            <td class="photo-cell" rowspan="6" style="padding: 0;">
-                ${
-                  photoUrl
-                    ? `<img src="${photoUrl}" width="200" style="display: block;" />`
-                    : '<div class="photo-placeholder">사진없음</div>'
-                }
-            </td>
-            <td class="info-label" style="height: 44.5px;">전시장</td>
-            <td class="info-value">${showroomName}</td>
-        </tr>
-        <tr>
-            <td class="info-label" style="height: 44.5px;">팀</td>
-            <td class="info-value">${teamName}</td>
-        </tr>
-        <tr>
-            <td class="info-label" style="height: 44.5px;">이름</td>
-            <td class="info-value-name">${staff.name}</td>
-        </tr>
-        <tr>
-            <td class="info-label" style="height: 44.5px;">직책</td>
-            <td class="info-value">${positionName}</td>
-        </tr>
-        <tr>
-            <td class="info-label" style="height: 44.5px;">핸드폰</td>
-            <td class="info-value">${staff.main_phone || "-"}</td>
-        </tr>
-        <tr>
-            <td class="info-label" style="height: 44.5px;">이메일</td>
-            <td class="info-value">${staff.email || ""}</td>
-        </tr>
-    </table>
-</body>
-</html>`;
-
-    // Blob 생성 및 다운로드
-    const blob = new Blob(["\ufeff" + html], {
-      type: "application/vnd.ms-excel;charset=utf-8;",
-    });
-    const link = document.createElement("a");
-    const url = URL.createObjectURL(blob);
-    link.setAttribute("href", url);
-    link.setAttribute(
-      "download",
-      `sales_staff_${staff.name}_${new Date().toISOString().split("T")[0]}.xls`
-    );
-    link.style.visibility = "hidden";
-    document.body.appendChild(link);
-    link.click();
-    document.body.removeChild(link);
-  };
-
-  // 등록 페이지로 이동
-  const goToCreate = () => {
-    router.push("/site-manager/staff/sales/create");
-  };
-
-  // 수정 페이지로 이동
-  const goToEdit = (id) => {
-    router.push(`/site-manager/staff/sales/edit/${id}`);
-  };
-
-  // 삭제
-  const handleDelete = async (id) => {
-    showConfirm(
-      '정말 삭제하시겠습니까?',
-      async () => {
-        const { data, error } = await del(`/staff/sales/${id}`);
-
-        if (error || !data?.success) {
-          showAlert(error?.message || data?.message || '삭제에 실패했습니다.', '오류');
-        } else {
-          showAlert(data.message || '삭제되었습니다.', '성공');
-          loadSales();
-        }
-      },
-      '영업사원 삭제'
-    );
-  };
-
-  // 노출/비노출 토글
-  const toggleActive = (id, currentStatus) => {
-    const statusText = currentStatus == 1 ? '비노출' : '노출';
-
-    showConfirm(
-      `영업사원을 ${statusText} 상태로 변경하시겠습니까?`,
-      async () => {
-        const { data, error } = await post(`/staff/sales/${id}/toggle-active`);
-
-        if (error || !data?.success) {
-          showAlert(error?.message || data?.message || '상태 변경에 실패했습니다.', '오류');
-        } else {
-          showAlert(data.message || '영업사원 상태가 변경되었습니다.', '성공');
-          loadSales();
-        }
-      },
-      '상태 변경'
-    );
-  };
-
-  onMounted(() => {
-    loadFilters();
-    loadSales();
-  });
-</script>
-
-<style scoped>
-  /* 필터 영역 input/select 테두리 스타일 */
-  .admin--filter-row .admin--form-input,
-  .admin--filter-row .admin--form-select {
-    border: 1px solid var(--admin-border-color);
-    border-radius: 4px;
-    height: 33px;
-    padding: 6px 14px;
-    font-size: 13px;
-  }
-
-  /* 버튼 영역 간격 조정 */
-  .admin--search-actions {
-    display: flex;
-    gap: 12px;
-    flex-shrink: 0;
-    white-space: nowrap;
-  }
-
-  /* 상단 버튼들 세로 크기 */
-  .admin--search-actions .admin--btn-small {
-    height: 78px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-  }
-
-  /* 상태 토글 버튼 */
-  .admin--toggle-btn {
-    padding: 6px 16px;
-    font-size: 12px;
-    border-radius: 20px;
-    border: 1px solid #ddd;
-    background: #f5f5f5;
-    color: #666;
-    cursor: pointer;
-    transition: all 0.3s ease;
-    font-weight: 500;
-  }
-
-  .admin--toggle-btn:hover {
-    border-color: #bbb;
-    background: #e8e8e8;
-  }
-
-  .admin--toggle-btn.is-active {
-    background: var(--admin-accent-primary);
-    color: white;
-    border-color: var(--admin-accent-primary);
-  }
-
-  .admin--toggle-btn.is-active:hover {
-    background: var(--admin-accent-hover);
-    border-color: var(--admin-accent-hover);
-  }
-</style>

+ 0 - 300
app/pages/site-manager/staff/sales/print-a2.vue

@@ -1,300 +0,0 @@
-<template>
-  <div class="a2-print-page">
-    <div v-for="branch in groupedData" :key="branch.id" class="branch-section">
-      <h2 class="branch-name">{{ branch.name }}</h2>
-
-      <div v-for="team in branch.teams" :key="team.id" class="team-section">
-        <h3 class="team-name">{{ team.name }}</h3>
-
-        <div class="staff-grid">
-          <div v-for="staff in team.staff" :key="staff.id" class="staff-card">
-            <div class="staff-photo">
-              <img
-                v-if="staff.photo_url"
-                :src="getImageUrl(staff.photo_url)"
-                :alt="staff.name"
-              />
-              <img
-                v-else
-                :src="getMediaUrl('/img/FORDKOREA_logo.png')"
-                alt="FORDKOREA Logo"
-                class="logo-placeholder"
-              />
-            </div>
-            <div class="staff-info">
-              <div class="staff-position">
-                {{ getPositionName(staff.position) }} {{ staff.name }}
-              </div>
-              <div class="staff-phone">{{ staff.direct_phone || staff.main_phone }}</div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup>
-  import { ref, onMounted, computed } from "vue";
-
-  definePageMeta({
-    layout: false,
-  });
-
-  const config = useRuntimeConfig();
-  const { get } = useApi();
-  const { getMediaUrl } = useImage();
-  const { getImageUrl } = useImage();
-  const { getTeamName, getPositionName } = useSalesData();
-
-  const salesList = ref([]);
-  const showrooms = ref([]);
-
-  // 지점별, 팀별로 그룹화
-  const groupedData = computed(() => {
-    const branches = {};
-
-    salesList.value.forEach((staff) => {
-      const branchId = staff.showroom || 0;
-      const teamId = staff.team ?? 0;
-
-      // 지점 정보 찾기
-      const showroom = showrooms.value.find((s) => s.id === branchId);
-
-      // 지점이 없으면 건너뛰기 (미지정 제외)
-      if (!showroom) {
-        return;
-      }
-
-      // 지점 초기화
-      if (!branches[branchId]) {
-        branches[branchId] = {
-          id: branchId,
-          name: showroom.name,
-          teams: {},
-        };
-      }
-
-      // 팀 초기화
-      if (!branches[branchId].teams[teamId]) {
-        branches[branchId].teams[teamId] = {
-          id: teamId,
-          name: getTeamName(teamId),
-          staff: [],
-        };
-      }
-
-      // 직원 추가
-      branches[branchId].teams[teamId].staff.push(staff);
-    });
-
-    // 배열로 변환 및 정렬
-    return Object.values(branches)
-      .map((branch) => ({
-        ...branch,
-        teams: Object.values(branch.teams)
-          .map((team) => ({
-            ...team,
-            staff: team.staff.sort((a, b) => {
-              // company가 'w'인 경우: position 내림차순
-              // 그 외: position 오름차순
-              if (a.position !== b.position) {
-                if (config.public.company === "w") {
-                  return b.position - a.position; // 내림차순
-                } else {
-                  return a.position - b.position; // 오름차순
-                }
-              }
-              // 2. 같은 직급이면 display_order 오름차순
-              return a.display_order - b.display_order;
-            }),
-          }))
-          .sort((a, b) => a.id - b.id),
-      }))
-      .sort((a, b) => a.id - b.id);
-  });
-
-  // 데이터 로드
-  const loadData = async () => {
-    // 전시장 목록 로드 (is_active=1인 활성화된 지점만)
-    const { data: branchData } = await get("/branch/list", {
-      params: { per_page: 1000, is_active: 1 },
-    });
-    console.log("[PrintA2] 지점 데이터:", branchData);
-    if (branchData?.success && branchData?.data) {
-      showrooms.value = branchData.data.items || [];
-    }
-
-    // 전체 영업사원 목록 로드 (is_act=1인 활성화된 영업사원만)
-    const { data: salesData } = await get("/staff/sales", {
-      params: { per_page: 1000, is_act: 1 },
-    });
-    console.log("[PrintA2] 영업사원 데이터:", salesData);
-    if (salesData?.success && salesData?.data) {
-      salesList.value = salesData.data.items || [];
-    }
-    console.log("[PrintA2] 최종 salesList (활성화만):", salesList.value);
-  };
-
-  onMounted(async () => {
-    await loadData();
-    // 데이터 로드 후 자동 출력
-    setTimeout(() => {
-      window.print();
-    }, 500);
-  });
-</script>
-
-<style scoped>
-  .a2-print-page {
-    padding: 20px;
-    background: white;
-  }
-
-  .a2-header {
-    text-align: center;
-    margin-bottom: 30px;
-    padding-bottom: 20px;
-    border-bottom: 3px solid #000;
-  }
-
-  .a2-header h1 {
-    font-size: 28px;
-    font-weight: bold;
-    margin: 0;
-  }
-
-  .branch-section {
-    margin-bottom: 40px;
-    page-break-inside: avoid;
-  }
-
-  .branch-name {
-    font-size: 24px;
-    font-weight: bold;
-    color: #333;
-    margin-bottom: 20px;
-    padding: 10px;
-    background: #f0f0f0;
-    border-left: 5px solid #000;
-  }
-
-  .team-section {
-    margin-bottom: 30px;
-    page-break-inside: avoid;
-  }
-
-  .team-name {
-    font-size: 20px;
-    font-weight: bold;
-    color: #555;
-    margin-bottom: 15px;
-    padding: 8px;
-    background: #f8f8f8;
-    border-left: 3px solid #666;
-  }
-
-  .staff-grid {
-    display: grid;
-    grid-template-columns: repeat(8, 1fr);
-    gap: 20px;
-    margin-bottom: 20px;
-  }
-
-  .staff-card {
-    border: 1px solid #ddd;
-    padding: 15px;
-    text-align: center;
-    background: white;
-    page-break-inside: avoid;
-  }
-
-  .staff-photo {
-    width: 100%;
-    height: 150px;
-    margin-bottom: 10px;
-    overflow: hidden;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    border: 1px solid #ddd;
-  }
-
-  .staff-photo img {
-    max-width: 100%;
-    max-height: 100%;
-    object-fit: cover;
-  }
-
-  .staff-photo .logo-placeholder {
-    max-width: 80%;
-    max-height: 80%;
-    object-fit: contain;
-  }
-
-  .staff-info {
-    margin-top: 10px;
-  }
-
-  .staff-position {
-    font-size: 16px;
-    font-weight: bold;
-    color: #333;
-    margin-bottom: 5px;
-  }
-
-  .staff-phone {
-    font-size: 13px;
-    color: #888;
-  }
-
-  /* 인쇄 스타일 */
-  @media print {
-    * {
-      margin: 0;
-      padding: 0;
-    }
-
-    body {
-      margin: 0 !important;
-      padding: 0 !important;
-    }
-
-    .a2-print-page {
-      padding: 5mm;
-      margin: 0;
-    }
-
-    .a2-header {
-      margin-top: 0;
-      padding-top: 0;
-      margin-bottom: 20px;
-    }
-
-    .staff-grid {
-      grid-template-columns: repeat(8, 1fr);
-      gap: 10px;
-    }
-
-    .staff-card {
-      padding: 10px;
-    }
-
-    .staff-photo {
-      height: 120px;
-    }
-
-    .staff-position {
-      font-size: 14px;
-    }
-
-    .staff-phone {
-      font-size: 11px;
-    }
-
-    @page {
-      size: A2;
-      margin: 5mm;
-    }
-  }
-</style>

+ 0 - 287
app/pages/site-manager/staff/sales/print/[id].vue

@@ -1,287 +0,0 @@
-<script setup>
-  import { ref, onMounted } from "vue";
-  import { useRoute, useRouter } from "vue-router";
-  const { getMediaUrl } = useImage();
-  const config = useRuntimeConfig();
-  definePageMeta({
-    layout: "blank",
-  });
-
-  const route = useRoute();
-  const router = useRouter();
-  const { get } = useApi();
-  const { getImageUrl } = useImage();
-  const { getTeamName, getPositionName } = useSalesData();
-
-  const staff = ref(null);
-  const showrooms = ref([]);
-
-  // 데이터 로드
-  const loadData = async () => {
-    const id = route.params.id;
-
-    // 영업사원 정보
-    const { data: staffData } = await get(`/staff/sales/${id}`);
-    if (staffData?.success && staffData?.data) {
-      staff.value = staffData.data;
-    }
-
-    // 전시장 리스트
-    const { data: branchData } = await get("/branch/list", { per_page: 1000 });
-    if (branchData?.success && branchData?.data) {
-      showrooms.value = branchData.data.items || [];
-    }
-  };
-
-  // 전시장 이름 가져오기
-  const getShowroomName = (showroomId) => {
-    const showroom = showrooms.value.find((s) => s.id === showroomId);
-    return showroom ? showroom.name : "-";
-  };
-
-  // 프린트
-  const handlePrint = () => {
-    window.print();
-  };
-
-  // 닫기
-  const handleClose = () => {
-    window.close();
-  };
-
-  onMounted(() => {
-    loadData();
-  });
-</script>
-
-<template>
-  <div class="sales--print-page">
-    <div class="sales--print-container">
-      <!-- 헤더 -->
-      <div class="sales--print-header">
-        <h1>아우디 공식딜러 포드코리아</h1>
-        <div class="FORDKOREA--logo"><img :src="getMediaUrl('/img/FORDKOREA_logo.png')" /></div>
-      </div>
-
-      <!-- 메인 콘텐츠 -->
-      <div class="sales--print-content">
-        <!-- 사진 영역 -->
-        <div class="sales--print-photo">
-          <img
-            v-if="staff?.photo_url"
-            :src="getImageUrl(staff.photo_url)"
-            :alt="staff.name"
-          />
-          <div v-else class="sales--print-photo-empty">사진없음</div>
-        </div>
-
-        <!-- 정보 영역 -->
-        <div class="sales--print-info">
-          <div class="sales--print-row">
-            <div class="sales--print-label">전시장</div>
-            <div class="sales--print-value">{{ getShowroomName(staff?.showroom) }}</div>
-          </div>
-
-          <div class="sales--print-row">
-            <div class="sales--print-label">팀</div>
-            <div class="sales--print-value">{{ getTeamName(staff?.team) }}</div>
-          </div>
-
-          <div class="sales--print-row">
-            <div class="sales--print-label">이름</div>
-            <div class="sales--print-value sales--print-name">{{ staff?.name }}</div>
-          </div>
-
-          <div class="sales--print-row">
-            <div class="sales--print-label">직책</div>
-            <div class="sales--print-value">{{ getPositionName(staff?.position) }}</div>
-          </div>
-
-          <!-- <div class="sales--print-row">
-            <div class="sales--print-label">대표번호</div>
-            <div class="sales--print-value">{{ staff?.main_phone }}</div>
-          </div> -->
-
-          <div class="sales--print-row">
-            <div class="sales--print-label">핸드폰</div>
-            <div v-if="config.public.company == 'w'" class="sales--print-value">
-              {{ staff?.main_phone }}
-            </div>
-            <div v-else class="sales--print-value">{{ staff?.direct_phone }}</div>
-          </div>
-
-          <div class="sales--print-row">
-            <div class="sales--print-label">이메일</div>
-            <div class="sales--print-value">{{ staff?.email }}</div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 프린트 버튼 (프린트 시 숨김) -->
-      <div class="sales--print-actions no-print">
-        <button class="sales--print-btn" @click="handlePrint">프린트</button>
-        <button class="sales--print-btn sales--print-btn-secondary" @click="handleClose">
-          닫기
-        </button>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style scoped>
-  .FORDKOREA--logo {
-    img {
-      max-height: 50px;
-    }
-  }
-  .sales--print-page {
-    min-height: 100vh;
-    background: #f5f5f5;
-    padding: 20px;
-  }
-
-  .sales--print-container {
-    max-width: 800px;
-    margin: 0 auto;
-    background: white;
-    padding: 40px;
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
-  }
-
-  .sales--print-header {
-    text-align: center;
-    margin-bottom: 40px;
-    padding-bottom: 20px;
-    border-bottom: 2px solid #333;
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-  }
-
-  .sales--print-header h1 {
-    font-size: 28px;
-    font-weight: bold;
-    color: #000;
-    margin: 0;
-  }
-
-  .sales--print-content {
-    display: flex;
-    gap: 40px;
-    margin-bottom: 40px;
-  }
-
-  .sales--print-photo {
-    flex-shrink: 0;
-    width: 200px;
-    height: 300px;
-    border: 1px solid #ddd;
-    border-radius: 8px;
-    overflow: hidden;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    background: #f9f9f9;
-  }
-
-  .sales--print-photo img {
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-  }
-
-  .sales--print-photo-empty {
-    color: #999;
-    font-size: 14px;
-  }
-
-  .sales--print-info {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    gap: 20px;
-  }
-
-  .sales--print-row {
-    display: flex;
-    align-items: center;
-    padding-bottom: 12px;
-    border-bottom: 1px solid #e0e0e0;
-  }
-
-  .sales--print-label {
-    width: 120px;
-    font-weight: bold;
-    color: #333;
-    font-size: 15px;
-  }
-
-  .sales--print-value {
-    flex: 1;
-    color: #555;
-    font-size: 15px;
-  }
-
-  .sales--print-name {
-    font-size: 18px;
-    font-weight: bold;
-    color: #000;
-  }
-
-  .sales--print-actions {
-    display: flex;
-    justify-content: center;
-    gap: 12px;
-    margin-top: 40px;
-  }
-
-  .sales--print-btn {
-    padding: 12px 32px;
-    font-size: 16px;
-    font-weight: bold;
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    background: #000;
-    color: white;
-    transition: background 0.2s;
-  }
-
-  .sales--print-btn:hover {
-    background: #333;
-  }
-
-  .sales--print-btn-secondary {
-    background: #666;
-  }
-
-  .sales--print-btn-secondary:hover {
-    background: #888;
-  }
-
-  /* 프린트 스타일 */
-  @media print {
-    .sales--print-page {
-      background: white;
-      padding: 0;
-    }
-
-    .sales--print-container {
-      max-width: none;
-      box-shadow: none;
-      padding: 20px;
-    }
-
-    .no-print {
-      display: none !important;
-    }
-
-    .sales--print-header {
-      margin-bottom: 30px;
-    }
-
-    .sales--print-content {
-      margin-bottom: 0;
-    }
-  }
-</style>

+ 81 - 8
info.md

@@ -26,10 +26,36 @@
 - 로그인 후 `/site-manager/dashboard` 로 이동
 
 ### 👤 관리자 관리
-- **페이지**: `/site-manager/admins`
-- **API**: `GET /api/admin`, `POST/PUT/DELETE /api/admin/:id`, 비밀번호 변경, 잠금 해제, 아이디 중복 확인
-- 검색(아이디/이름) + 역할·상태 필터 + 페이지네이션
-- 본인 계정 삭제 방지, 5회 실패 시 잠금 표시
+- **테이블**:
+  - `admin_users` (id, username, password, password_changed_at, name, email, phone, role, status, login_attempts, last_failed_login, last_login, deleted_YN, created_at, updated_at)
+  - `admin_permissions` (id, admin_id FK CASCADE, permission VARCHAR, UNIQUE(admin_id, permission)) — 1:N 메뉴 권한
+- **페이지**: `list / create / detail / edit` (`/site-manager/admin/...`)
+- **API**:
+  - `GET /api/admin` (검색 search_field=username/name/email + role/status 필터 + `deleted=1`로 삭제 관리자만 분기)
+  - `GET /api/admin/check-username` (아이디 중복, soft 삭제된 계정 제외 → 재사용 가능)
+  - `GET /api/admin/:id` (permissions 같이 응답)
+  - `POST /api/admin` (생성 + permissions 동기화)
+  - `PUT /api/admin/:id` (role/permissions 변경은 슈퍼관리자만 가드)
+  - `DELETE /api/admin/:id` (soft delete + 토큰 무효화, permissions는 보존)
+  - `POST /api/admin/:id/restore` (복구 — username/email 충돌 검사)
+  - `DELETE /api/admin/:id/hard` (영구 삭제 — 이미 soft 삭제된 계정만, 본인 차단)
+  - `POST /api/admin/:id/password`, `POST /api/admin/:id/unlock`
+- **role enum**: `super_admin`, `admin` (디폴트 `admin`)
+- **status enum**: `active`, `inactive`, `suspended`
+- **권한(`permissions`) enum**: `admin / field / fishing / challenge / quest / item / species / user` (= [admin.vue](app/layouts/admin.vue) menuItems id와 동일)
+  - 슈퍼관리자는 row 박지 않고 응답에 `'all'` 문자열로 반환
+  - 일반 admin은 메뉴 id 배열로 저장/응답 (1개 이상 필수)
+- **주요 기능**:
+  - 검색(아이디/이름/이메일) + 권한·상태 필터 + 페이지네이션
+  - **체크박스 일괄 액션**: 선택 삭제 / 선택 활성 / 선택 휴면 / 선택 정지
+  - **삭제 관리자 관리 모드** (슈퍼관리자 전용): 같은 페이지 토글 → `deleted_YN='Y'`만 보여줌 → 선택 복구 / 선택 영구 삭제
+  - **CSV 내보내기** (현재 필터 그대로 또는 선택된 행만, UTF-8 BOM)
+  - **권한 칩 표시**: 슈퍼관리자는 골드 뱃지, 일반 admin은 메뉴 칩 + `+N` 더보기 (list 최대 2개, detail 전체 표시)
+  - **권한 기반 메뉴 분기** ([useAuth](app/composables/useAuth.js)) — 사이드바 메뉴 + URL 미들웨어 가드 ([middleware/auth.js](app/middleware/auth.js))
+  - **본인/슈퍼관리자 보호**: 체크박스 disabled, 수정/삭제 버튼 숨김, edit 페이지 진입 차단 (일반 admin이 슈퍼관리자 수정 시도 시 detail로 redirect)
+  - **비밀번호 변경 모달** (8자+, 일치 검증)
+  - 5회 실패 시 잠금 + 슈퍼관리자가 해제, 24시간 토큰
+  - 본인 계정 삭제 방지, 본인 계정 edit 시 상태 row 숨김
 
 ### 🎣 낚시분야 관리 (Field)
 - **테이블**: `fishing_field` (id, name, weight, status_YN, deleted_YN, created_at, updated_at)
@@ -41,9 +67,14 @@
 
 ### 🗺️ 낚시지역 관리 (Area)
 - **테이블**: `fishing_area` (id, name, deleted_YN, created_at, updated_at)
-- **페이지**: list / create / detail / edit
-- **API**: `GET /api/area/list`, `GET/POST/PUT/DELETE /api/area/:id`
+- **페이지**: list / create / detail / edit / **places (전체보기)**
+- **API**:
+  - `GET /api/area/list`, `GET/POST/PUT/DELETE /api/area/:id`
+  - `GET /api/area/:id/places` — 해당 지역에 속한 onboard + fishing UNION ALL (필드명 JOIN), `limit` 모드(detail 미리보기 8개) / `page` 모드(전체보기 페이지네이션)
 - 지역명 1~20자, 중복 불가, soft delete
+- **상세 페이지 — 해당 지역의 낚시어선/낚시터 미니 테이블**: 카운트 표시(🎣 낚시터 N / 🚢 낚시어선 M), 최근 등록순 8개 + 전체보기 버튼, 행 클릭 시 onboard/fishing 상세로 이동
+- **삭제 차단**: 해당 지역에 연결된 onboard/fishing이 1건이라도 있으면 백엔드/프론트 모두에서 삭제 거부 (`409 Conflict`)
+- **전체보기 페이지** (`/area/places/:id`): 페이지네이션 + 6컬럼(번호/구분/이름/주소/상태/등록일) + 역순 번호 표시
 
 ### ⚓ 선상 관리 (Onboard)
 - **테이블**: `onboard` (id, field_id, area_id, name, area_detail, tonnage, capacity, zip_code, address, address_detail, address_refer, lat, lng, bank_code, account_number(암호화), account_holder, partnership_YN, status_YN, deleted_YN, created_at, updated_at)
@@ -106,12 +137,46 @@
   - **검색**: 아이템명 입력 + 구분 필터 라디오 버튼 그룹(전체/진출권/포인트/뱃지)
   - 행 클릭 → 상세, "수정" 버튼 → edit, soft delete
 
+### 🐟 어종 관리 (Species)
+세 개의 1뎁스 메뉴(어종구분 / 챌린지 어종관리 / 퀘스트 어종관리), 같은 코드 패턴 공유.
+
+#### 🐠 어종구분 (Species Type)
+- **테이블**: `species_type` (id, name, sort_order, status_YN, deleted_YN, created_at)
+- **페이지**: 단일 list (인라인 추가/수정/삭제 + 일괄 저장)
+- **API**: `GET /api/species/list`, `POST /api/species/bulk-save` (creates + updates + deletes 트랜잭션)
+- 구분명 1~30자, 정렬순서 INT
+
+#### 🏆 챌린지 어종관리 (Species Challenge)
+- **테이블**: `species_challenge` (id, type_id NULL, name, min, max, round1_min/max ~ round5_min/max, deleted_YN, created_at)
+- **페이지**: 단일 list (인라인 추가/수정/삭제 + 일괄 저장)
+- **API**: `GET /api/species-challenge/list` (species_type JOIN, 구분 필터 + 어종명 검색 + 기간 검색), `POST /api/species-challenge/bulk-save`
+- **검증** (프론트 + 백엔드 양방향):
+  - 어종명 필수 50자 이내
+  - 구분(type_id) **선택사항** — 미선택 행 있으면 일괄저장 시 확인 모달
+  - 최소금지 / 최대길이 입력, max ≥ min
+  - 각 라운드 min ≤ max, **모든 라운드 max ≤ 최대길이**
+  - **1라운드 min ≥ 최소금지** (사용자 명시 핵심 룰)
+- 검색 구분 select에 "구분 없음" 옵션 (`type_id IS NULL`)
+
+#### 🏅 퀘스트 어종관리 (Species Quest)
+- **테이블**: `species_quest` (구조 동일)
+- **페이지/API**: 챌린지와 100% 동일 코드 패턴 (테이블만 다름)
+
+#### 공통 패턴 (3개 메뉴 공통)
+- **인라인 추가/수정/삭제** + **일괄 저장** — 행 단위로 즉시 서버 호출 안 하고 변경 카운트 모아 한 번에 트랜잭션 처리
+- **변경 카운트 표시** (`일괄 저장 (3)`), 변경 없으면 버튼 숨김
+- **SVG 체크박스** (흰 바탕 + primary 컬러 체크/대시) — 테이블 전체 통일
+- **토스트 알림** (`Teleport to="body"` + Transition, 하단 중앙, 자동 dismiss)
+- **삭제 표시 모드**: 즉시 삭제 X, 화면에서 사라지고 일괄 저장 시 함께 처리
+- **number input 4자리 제한** (`maxlength`는 number에서 무먹어서 `@input` 핸들러로 자르기) + spinner 화살표 숨김
+
 ---
 
 ## 🟡 작업 예정 (메뉴)
 
 - 📊 대시보드 (현재 빈 페이지)
 - 🎯 챌린지 / 퀘스트 관리 (보상 아이템 M:N 관계)
+- 👥 회원 관리
 
 ---
 
@@ -154,8 +219,16 @@ ERD 새로 짜면서 기존 비즈니스 컨트롤러(Branch, Showroom, Service,
 ## 📌 디자인 정책
 
 - **soft delete vs hard delete**
-  - 마스터성 데이터(분야/지역/선상): **soft delete** (`deleted_YN`)
-  - 종속 데이터(선상 사진): **hard delete** + 파일 직접 삭제
+  - 마스터성 데이터(분야/지역/선상/낚시터/아이템/어종/관리자): **soft delete** (`deleted_YN`)
+  - 종속 데이터(선상/낚시터 사진): **hard delete** + 파일 직접 삭제
+  - 관리자 영구 삭제(`hardDelete`)는 슈퍼관리자가 "삭제 관리자 관리" 모드에서만 수행
 - **TIMESTAMP 사용 시 주의** — MySQL이 첫 TIMESTAMP에 자동으로 `ON UPDATE CURRENT_TIMESTAMP` 부여하므로 `created_at`은 `DEFAULT CURRENT_TIMESTAMP`만, `updated_at`은 `NULL DEFAULT NULL`로 정의
 - **계좌번호 컬럼 길이** — 암호화하면 base64로 ~135자 → `VARCHAR(255)` 권장
 - **신규 SCSS 추가 시** — 어드민 페이지의 `<style scoped>` 안 만들고 [app/assets/scss/admin.scss](app/assets/scss/admin.scss) 에 통합 (일관성 + 관리 단순화)
+- **외래키 제약** — 부모/자식 컬럼 타입(`INT`/`INT UNSIGNED`/`BIGINT`)이 정확히 같아야 함. CHARSET/COLLATE도 동일해야 함
+- **모달 디자인** — `.admin--modal-overlay` + `.admin--alert-modal admin--form-modal` 조합. AdminAlertModal/비밀번호 변경 모달 등 전부 같은 톤(헤더 primary 네이비 배경, 본문 라운드 input, footer `.admin--btn` 체계)
+- **`<Teleport to="body">`** — 부모 컨테이너의 `transform`/`overflow`가 `position: fixed`를 가두는 경우 대비. 알림 모달은 `.admin--alert-overlay { z-index: 10010 }`로 다른 모달 위에 표시
+- **권한 분기** ([useAuth](app/composables/useAuth.js) composable + [middleware/auth.js](app/middleware/auth.js))
+  - `localStorage.admin_user.permissions` 기반으로 사이드바 메뉴 필터링
+  - URL 직접 입력 시 미들웨어가 권한 확인 후 dashboard로 redirect
+  - 백엔드 가드도 동시 적용 (UI 우회 차단)