Selaa lähdekoodia

마이그레이션 데이터 삽입 팝업작업

송용우 1 kuukausi sitten
vanhempi
commit
cd2770f455
36 muutettua tiedostoa jossa 2268 lisäystä ja 726 poistoa
  1. 2 2
      .htaccess
  2. 345 0
      app/components/Popup.vue
  3. 197 0
      app/components/admin/DatePicker.vue
  4. 29 1
      app/components/admin/SunEditor.vue
  5. 1 1
      app/composables/useApi.js
  6. 28 0
      app/composables/useImage.js
  7. 5 4
      app/pages/admin/admins/index.vue
  8. 25 22
      app/pages/admin/basic/popup/create.vue
  9. 55 36
      app/pages/admin/basic/popup/edit/[id].vue
  10. 39 7
      app/pages/admin/basic/popup/index.vue
  11. 39 30
      app/pages/admin/basic/site-info.vue
  12. 15 12
      app/pages/admin/board/event/edit/[id].vue
  13. 123 62
      app/pages/admin/board/event/index.vue
  14. 13 10
      app/pages/admin/board/ir/edit/[id].vue
  15. 123 62
      app/pages/admin/board/ir/index.vue
  16. 13 10
      app/pages/admin/board/news/edit/[id].vue
  17. 123 62
      app/pages/admin/board/news/index.vue
  18. 208 0
      app/pages/admin/branch/create.vue
  19. 242 0
      app/pages/admin/branch/edit/[id].vue
  20. 84 7
      app/pages/admin/branch/list.vue
  21. 4 2
      app/pages/admin/branch/manager/create.vue
  22. 12 7
      app/pages/admin/branch/manager/edit/[id].vue
  23. 28 4
      app/pages/admin/branch/manager/index.vue
  24. 7 3
      app/pages/admin/service/brochure.vue
  25. 7 2
      app/pages/admin/staff/advisor/create.vue
  26. 7 2
      app/pages/admin/staff/advisor/edit/[id].vue
  27. 116 114
      app/pages/admin/staff/advisor/index.vue
  28. 34 35
      app/pages/admin/staff/sales/create.vue
  29. 52 50
      app/pages/admin/staff/sales/edit/[id].vue
  30. 174 176
      app/pages/admin/staff/sales/index.vue
  31. 6 2
      app/pages/index.vue
  32. 49 0
      gojinaudi_key_20251106.pem
  33. 2 1
      nuxt.config.ts
  34. 23 0
      package-lock.json
  35. 2 0
      package.json
  36. 36 0
      public/.htaccess

+ 2 - 2
.htaccess

@@ -29,8 +29,8 @@
       RewriteCond %{REQUEST_FILENAME} -f
       RewriteRule ^ - [L]
 
-      # 5. 나머지 모든 요청은 Nuxt index.html로
+      # 5. 나머지 모든 요청은 Nuxt 200.html로 (SPA fallback)
       RewriteCond %{REQUEST_FILENAME} !-f
       RewriteCond %{REQUEST_FILENAME} !-d
-      RewriteRule ^.*$ /index.html [L]
+      RewriteRule ^.*$ /200.html [L]
   </IfModule>

+ 345 - 0
app/components/Popup.vue

@@ -0,0 +1,345 @@
+<template>
+  <div v-if="visiblePopups.length > 0" class="popup--container">
+    <div
+      v-for="(popup, index) in visiblePopups"
+      :key="popup.id"
+      class="popup--wrapper"
+      :style="{
+        top: popupPositions[index]?.top || popup.position_top + 'px',
+        left: popupPositions[index]?.left || popup.position_left + 'px',
+        width: popup.width + 'px',
+        height: popup.type === 'image' ? 'auto' : popup.height + 'px'
+      }"
+    >
+      <!-- 드래그 헤더 -->
+      <div
+        class="popup--header"
+        @mousedown="startDrag($event, index)"
+        @touchstart="startDrag($event, index)"
+      >
+        <span class="popup--title">{{ popup.title }}</span>
+        <button
+          class="popup--close"
+          @click="closePopup(popup.id)"
+          aria-label="팝업 닫기"
+        >
+          ✕
+        </button>
+      </div>
+
+      <!-- 스크롤 가능한 콘텐츠 영역 -->
+      <div class="popup--content">
+        <!-- HTML 타입 팝업 -->
+        <div v-if="popup.type === 'html'" class="popup--html" v-html="popup.content"></div>
+
+        <!-- 이미지 타입 팝업 -->
+        <div v-else-if="popup.type === 'image'" class="popup--image">
+          <a
+            v-if="popup.link_url"
+            :href="popup.link_url"
+            target="_blank"
+            rel="noopener noreferrer"
+          >
+            <img :src="getImageUrl(popup.image_url)" :alt="popup.title" />
+          </a>
+          <img v-else :src="getImageUrl(popup.image_url)" :alt="popup.title" />
+        </div>
+      </div>
+
+      <!-- 하단 고정 푸터 -->
+      <div v-if="popup.cookie_setting !== 'none'" class="popup--footer">
+        <label class="popup--checkbox">
+          <input
+            type="checkbox"
+            @change="(e) => handleDontShowToday(e, popup.id, popup.cookie_setting)"
+          />
+          <span v-if="popup.cookie_setting === 'today'">오늘 하루 창 띄우지 않음</span>
+          <span v-else-if="popup.cookie_setting === 'forever'">다시는 창을 띄우지 않음</span>
+        </label>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import axios from 'axios'
+
+const API_BASE_URL = 'https://gojinaudi.mycafe24.com/api'
+const visiblePopups = ref([])
+const popupPositions = ref([])
+const { getImageUrl } = useImage()
+
+// 드래그 상태
+const dragState = ref({
+  isDragging: false,
+  currentIndex: null,
+  startX: 0,
+  startY: 0,
+  initialLeft: 0,
+  initialTop: 0
+})
+
+// 쿠키 가져오기
+const getCookie = (name) => {
+  if (typeof document === 'undefined') return null
+  const value = `; ${document.cookie}`
+  const parts = value.split(`; ${name}=`)
+  if (parts.length === 2) return parts.pop().split(';').shift()
+  return null
+}
+
+// 쿠키 설정
+const setCookie = (name, value, days) => {
+  if (typeof document === 'undefined') return
+  const date = new Date()
+  date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
+  const expires = `expires=${date.toUTCString()}`
+  document.cookie = `${name}=${value};${expires};path=/`
+}
+
+// 팝업 데이터 가져오기
+const fetchPopups = async () => {
+  try {
+    const response = await axios.get(`${API_BASE_URL}/popup/active`)
+    if (response.data.success && response.data.data) {
+      // 쿠키 설정 필드 정규화 (cookie_setting 또는 cookie_type 지원)
+      const popups = response.data.data.map(popup => ({
+        ...popup,
+        cookie_setting: popup.cookie_setting || popup.cookie_type || 'none'
+      }))
+
+      // 쿠키로 숨긴 팝업 필터링
+      visiblePopups.value = popups.filter((popup) => {
+        const cookieName = `popup_hide_${popup.id}`
+        return !getCookie(cookieName)
+      })
+
+      // 초기 위치 설정
+      popupPositions.value = visiblePopups.value.map(() => ({}))
+    }
+  } catch (error) {
+    console.error('[Popup] Failed to fetch popups:', error)
+  }
+}
+
+// 드래그 시작
+const startDrag = (event, index) => {
+  event.preventDefault()
+
+  const e = event.touches ? event.touches[0] : event
+  const popup = visiblePopups.value[index]
+
+  dragState.value = {
+    isDragging: true,
+    currentIndex: index,
+    startX: e.clientX,
+    startY: e.clientY,
+    initialLeft: parseInt(popupPositions.value[index]?.left || popup.position_left),
+    initialTop: parseInt(popupPositions.value[index]?.top || popup.position_top)
+  }
+
+  document.addEventListener('mousemove', onDrag)
+  document.addEventListener('mouseup', stopDrag)
+  document.addEventListener('touchmove', onDrag)
+  document.addEventListener('touchend', stopDrag)
+}
+
+// 드래그 중
+const onDrag = (event) => {
+  if (!dragState.value.isDragging) return
+
+  const e = event.touches ? event.touches[0] : event
+  const deltaX = e.clientX - dragState.value.startX
+  const deltaY = e.clientY - dragState.value.startY
+
+  const newLeft = dragState.value.initialLeft + deltaX
+  const newTop = dragState.value.initialTop + deltaY
+
+  popupPositions.value[dragState.value.currentIndex] = {
+    left: newLeft + 'px',
+    top: newTop + 'px'
+  }
+}
+
+// 드래그 종료
+const stopDrag = () => {
+  dragState.value.isDragging = false
+  dragState.value.currentIndex = null
+
+  document.removeEventListener('mousemove', onDrag)
+  document.removeEventListener('mouseup', stopDrag)
+  document.removeEventListener('touchmove', onDrag)
+  document.removeEventListener('touchend', stopDrag)
+}
+
+// 팝업 닫기
+const closePopup = (popupId) => {
+  const index = visiblePopups.value.findIndex(p => p.id === popupId)
+  if (index !== -1) {
+    visiblePopups.value.splice(index, 1)
+    popupPositions.value.splice(index, 1)
+  }
+}
+
+// 오늘 하루 보지 않기
+const handleDontShowToday = (event, popupId, cookieSetting) => {
+  // 체크박스가 체크된 경우에만 실행
+  if (!event.target.checked) return
+
+  let days = 1 // 기본 1일
+  if (cookieSetting === 'forever') days = 365 * 10 // 10년 (사실상 영구)
+  else if (cookieSetting === 'today') days = 1
+
+  const cookieName = `popup_hide_${popupId}`
+  setCookie(cookieName, 'true', days)
+  closePopup(popupId)
+}
+
+onMounted(() => {
+  fetchPopups()
+})
+
+onUnmounted(() => {
+  stopDrag()
+})
+</script>
+
+<style scoped>
+.popup--container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 9999;
+}
+
+.popup--wrapper {
+  position: absolute;
+  pointer-events: auto;
+  background: #1a1a1a;
+  border: 1px solid #333;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
+  border-radius: 8px;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  max-height: 90vh;
+}
+
+.popup--header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  background: linear-gradient(to bottom, #2a2a2a, #1f1f1f);
+  border-bottom: 1px solid #333;
+  cursor: move;
+  user-select: none;
+}
+
+.popup--header:active {
+  cursor: grabbing;
+}
+
+.popup--title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #ffffff;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.popup--close {
+  width: 28px;
+  height: 28px;
+  background: rgba(255, 255, 255, 0.1);
+  color: #ffffff;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 18px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s;
+  flex-shrink: 0;
+  margin-left: 10px;
+}
+
+.popup--close:hover {
+  background: rgba(255, 255, 255, 0.2);
+  border-color: rgba(255, 255, 255, 0.3);
+}
+
+.popup--content {
+  flex: 1;
+  overflow: auto;
+  background: #1a1a1a;
+  min-height: 0;
+}
+
+.popup--html,
+.popup--image {
+  padding: 20px;
+  color: #ffffff;
+}
+
+.popup--html {
+  background: #1a1a1a;
+}
+
+.popup--html :deep(p),
+.popup--html :deep(div),
+.popup--html :deep(span) {
+  color: #ffffff;
+}
+
+.popup--html :deep(a) {
+  color: #4a9eff;
+}
+
+.popup--image {
+  padding: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #0a0a0a;
+}
+
+.popup--image img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+}
+
+.popup--footer {
+  padding: 12px 20px;
+  background: #222222;
+  border-top: 1px solid #333;
+  flex-shrink: 0;
+}
+
+.popup--checkbox {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 14px;
+  cursor: pointer;
+  user-select: none;
+  color: #ffffff;
+}
+
+.popup--checkbox input[type='checkbox'] {
+  cursor: pointer;
+  accent-color: #4a9eff;
+}
+
+.popup--checkbox span {
+  color: #e0e0e0;
+}
+</style>

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

@@ -0,0 +1,197 @@
+<template>
+  <div class="admin--datepicker">
+    <input
+      ref="dateInput"
+      type="text"
+      :value="modelValue"
+      :placeholder="placeholder"
+      :class="['admin--form-input', { 'is-required': required }]"
+      :required="required"
+      readonly
+    >
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import flatpickr from 'flatpickr'
+import { Korean } from 'flatpickr/dist/l10n/ko'
+import 'flatpickr/dist/flatpickr.min.css'
+import 'flatpickr/dist/themes/dark.css'
+
+const props = defineProps({
+  modelValue: {
+    type: String,
+    default: ''
+  },
+  placeholder: {
+    type: String,
+    default: '날짜 선택'
+  },
+  required: {
+    type: Boolean,
+    default: false
+  },
+  mode: {
+    type: String,
+    default: 'single', // single, range
+    validator: (value) => ['single', 'range'].includes(value)
+  },
+  minDate: {
+    type: String,
+    default: null
+  },
+  maxDate: {
+    type: String,
+    default: null
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const dateInput = ref(null)
+let flatpickrInstance = null
+
+onMounted(() => {
+  flatpickrInstance = flatpickr(dateInput.value, {
+    locale: Korean,
+    dateFormat: 'Y-m-d',
+    mode: props.mode,
+    minDate: props.minDate,
+    maxDate: props.maxDate,
+    defaultDate: props.modelValue || null,
+    onChange: (_selectedDates, dateStr) => {
+      emit('update:modelValue', dateStr)
+    },
+    // 다크 테마 설정
+    theme: 'dark',
+    // 월/년 선택 드롭다운 활성화
+    monthSelectorType: 'dropdown',
+    // 시간 선택 비활성화
+    enableTime: false,
+    // 주말 강조
+    onDayCreate: (_dObj, _dStr, _fp, dayElem) => {
+      const day = dayElem.dateObj.getDay()
+      if (day === 0) {
+        dayElem.classList.add('weekend-sunday')
+      } else if (day === 6) {
+        dayElem.classList.add('weekend-saturday')
+      }
+    }
+  })
+})
+
+// modelValue 변경 감지
+watch(() => props.modelValue, (newValue) => {
+  if (flatpickrInstance && newValue !== flatpickrInstance.input.value) {
+    flatpickrInstance.setDate(newValue || null, false)
+  }
+})
+
+// minDate, maxDate 변경 감지
+watch([() => props.minDate, () => props.maxDate], ([newMinDate, newMaxDate]) => {
+  if (flatpickrInstance) {
+    flatpickrInstance.set('minDate', newMinDate)
+    flatpickrInstance.set('maxDate', newMaxDate)
+  }
+})
+
+onBeforeUnmount(() => {
+  if (flatpickrInstance) {
+    flatpickrInstance.destroy()
+  }
+})
+</script>
+
+<style scoped>
+.admin--datepicker {
+  position: relative;
+}
+
+.admin--datepicker .admin--form-input {
+  cursor: pointer;
+  background: var(--admin-bg-tertiary, #252525);
+  color: var(--admin-text-primary, #ffffff) !important;
+  border: 1px solid var(--admin-border-color, #333333);
+}
+
+.admin--datepicker .admin--form-input:read-only {
+  background: var(--admin-bg-tertiary, #252525);
+  color: var(--admin-text-primary, #ffffff) !important;
+}
+
+.admin--datepicker .admin--form-input::placeholder {
+  color: var(--admin-text-muted, #666666);
+}
+
+.admin--datepicker .admin--form-input:focus {
+  outline: none;
+  border-color: var(--admin-accent-primary, #bb0a30);
+  box-shadow: 0 0 0 3px rgba(187, 10, 48, 0.1);
+}
+
+/* 다크 테마 커스터마이징 */
+:deep(.flatpickr-calendar.dark) {
+  background: #2c3e50;
+  border-color: #34495e;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-months) {
+  background: #34495e;
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-current-month .flatpickr-monthDropdown-months) {
+  background: #2c3e50;
+  color: #ecf0f1;
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-day.selected) {
+  background: #3498db;
+  border-color: #2980b9;
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-day.selected:hover) {
+  background: #2980b9;
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-day:hover) {
+  background: #34495e;
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-day.today) {
+  border-color: #3498db;
+}
+
+:deep(.flatpickr-calendar.dark .flatpickr-day.today:hover) {
+  background: #34495e;
+  border-color: #3498db;
+}
+
+/* 주말 색상 */
+:deep(.flatpickr-day.weekend-sunday) {
+  color: #e74c3c !important;
+}
+
+:deep(.flatpickr-day.weekend-saturday) {
+  color: #3498db !important;
+}
+
+:deep(.flatpickr-day.weekend-sunday.selected),
+:deep(.flatpickr-day.weekend-saturday.selected) {
+  color: #fff !important;
+}
+
+/* Range 모드 스타일 */
+:deep(.flatpickr-day.inRange) {
+  background: rgba(52, 152, 219, 0.2);
+  border-color: transparent;
+  box-shadow: -5px 0 0 rgba(52, 152, 219, 0.2), 5px 0 0 rgba(52, 152, 219, 0.2);
+}
+
+:deep(.flatpickr-day.startRange),
+:deep(.flatpickr-day.endRange) {
+  background: #3498db;
+  color: #fff;
+}
+</style>

+ 29 - 1
app/components/admin/SunEditor.vue

@@ -118,9 +118,37 @@ onBeforeUnmount(() => {
 
 .admin--suneditor-wrapper :deep(.se-wrapper-inner) {
   background: var(--admin-bg-tertiary);
+  color: #ffffff;
+}
+
+.admin--suneditor-wrapper :deep(.se-wrapper-inner.se-wrapper-wysiwyg) {
+  background: var(--admin-bg-tertiary);
+  color: #ffffff !important;
+}
+
+/* 에디터 내부 모든 텍스트 요소 - 기본 흰색 */
+.admin--suneditor-wrapper :deep(.se-wrapper-inner p),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner div),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner span),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner h1),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner h2),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner h3),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner h4),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner h5),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner h6),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner li),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner ul),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner ol),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner blockquote),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner pre),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner a),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner strong),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner em),
+.admin--suneditor-wrapper :deep(.se-wrapper-inner code) {
+  color: #ffffff;
 }
 
 .admin--suneditor-wrapper :deep(.se-placeholder) {
-  color: var(--admin-text-muted);
+  color: var(--admin-text-muted) !important;
 }
 </style>

+ 1 - 1
app/composables/useApi.js

@@ -1,7 +1,7 @@
 import axios from 'axios'
 
 // API Base URL (환경변수 또는 기본값)
-const API_BASE_URL = process.env.NUXT_PUBLIC_API_BASE || 'http://gojinaudi.mycafe24.com/api'
+const API_BASE_URL = process.env.NUXT_PUBLIC_API_BASE || 'https://gojinaudi.mycafe24.com/api'
 
 // Axios 인스턴스 생성
 const apiClient = axios.create({

+ 28 - 0
app/composables/useImage.js

@@ -0,0 +1,28 @@
+/**
+ * 이미지 URL 헬퍼 composable
+ */
+export const useImage = () => {
+  const config = useRuntimeConfig()
+
+  /**
+   * 이미지 상대 경로를 절대 URL로 변환
+   * @param {string} path - 이미지 상대 경로 (예: "/uploads/images/abc.jpg")
+   * @returns {string} - 전체 URL (예: "http://gojinaudi.mycafe24.com/uploads/images/abc.jpg")
+   */
+  const getImageUrl = (path) => {
+    if (!path) return ''
+
+    // 이미 전체 URL인 경우 그대로 반환
+    if (path.startsWith('http://') || path.startsWith('https://')) {
+      return path
+    }
+
+    // 상대 경로인 경우 imageBase 붙이기
+    const imageBase = config.public.imageBase
+    return `${imageBase}${path}`
+  }
+
+  return {
+    getImageUrl
+  }
+}

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

@@ -262,10 +262,11 @@ const loadAdmins = async () => {
       return
     }
 
-    if (data) {
-      admins.value = data.items || []
-      totalPages.value = data.total_pages || 1
-      console.log('[Admins] 목록 로드 성공:', data)
+    // API 응답: { success: true, data: { items, total_pages }, message }
+    if (data?.success && data?.data) {
+      admins.value = data.data.items || []
+      totalPages.value = data.data.total_pages || 1
+      console.log('[Admins] 목록 로드 성공:', data.data)
     }
   } finally {
     loading.value = false

+ 25 - 22
app/pages/admin/basic/popup/create.vue

@@ -42,19 +42,19 @@
       <div class="admin--form-group">
         <label class="admin--form-label">시작일/종료일 <span class="admin--required">*</span></label>
         <div class="admin--date-range">
-          <input
+          <DatePicker
             v-model="formData.start_date"
-            type="date"
-            class="admin--form-input"
+            placeholder="시작일 선택"
+            :max-date="formData.end_date"
             required
-          >
+          />
           <span class="admin--date-separator">~</span>
-          <input
+          <DatePicker
             v-model="formData.end_date"
-            type="date"
-            class="admin--form-input"
+            placeholder="종료일 선택"
+            :min-date="formData.start_date"
             required
-          >
+          />
         </div>
       </div>
 
@@ -126,28 +126,28 @@
         <div class="admin--radio-group">
           <label class="admin--radio-label">
             <input
-              v-model="formData.cookie_type"
+              v-model="formData.cookie_setting"
               type="radio"
               value="today"
-              name="cookie_type"
+              name="cookie_setting"
             >
             <span>오늘 하루 창 띄우지 않음</span>
           </label>
           <label class="admin--radio-label">
             <input
-              v-model="formData.cookie_type"
+              v-model="formData.cookie_setting"
               type="radio"
               value="forever"
-              name="cookie_type"
+              name="cookie_setting"
             >
             <span>다시는 창을 띄우지 않음</span>
           </label>
           <label class="admin--radio-label">
             <input
-              v-model="formData.cookie_type"
+              v-model="formData.cookie_setting"
               type="radio"
               value="none"
-              name="cookie_type"
+              name="cookie_setting"
             >
             <span>사용 안 함</span>
           </label>
@@ -229,6 +229,7 @@
 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',
@@ -253,7 +254,7 @@ const formData = ref({
   height: 600,
   position_top: 100,
   position_left: 100,
-  cookie_type: 'none',
+  cookie_setting: 'none',
   content: '',
   image_url: '',
   link_url: ''
@@ -320,17 +321,19 @@ const handleSubmit = async () => {
     // 이미지 업로드 (단일페이지인 경우)
     if (formData.value.type === 'image' && imageFile.value) {
       const formDataImage = new FormData()
-      formDataImage.append('image', imageFile.value)
+      formDataImage.append('file', imageFile.value)
 
       const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
 
-      if (uploadError) {
-        errorMessage.value = '이미지 업로드에 실패했습니다.'
+      console.log('[Popup Create] 이미지 업로드 응답:', { uploadData, uploadError })
+
+      if (uploadError || !uploadData?.success) {
+        errorMessage.value = uploadError?.message || '이미지 업로드에 실패했습니다.'
         isSaving.value = false
         return
       }
 
-      imageUrl = uploadData.url
+      imageUrl = uploadData.data?.url || uploadData.data
     }
 
     // 팝업 등록
@@ -341,10 +344,10 @@ const handleSubmit = async () => {
 
     const { data, error } = await post('/basic/popup', submitData)
 
-    if (error) {
-      errorMessage.value = error.message || '등록에 실패했습니다.'
+    if (error || !data?.success) {
+      errorMessage.value = error?.message || data?.message || '등록에 실패했습니다.'
     } else {
-      successMessage.value = '팝업이 등록되었습니다.'
+      successMessage.value = data.message || '팝업이 등록되었습니다.'
       setTimeout(() => {
         router.push('/admin/basic/popup')
       }, 1000)

+ 55 - 36
app/pages/admin/basic/popup/edit/[id].vue

@@ -46,19 +46,19 @@
       <div class="admin--form-group">
         <label class="admin--form-label">시작일/종료일 <span class="admin--required">*</span></label>
         <div class="admin--date-range">
-          <input
+          <DatePicker
             v-model="formData.start_date"
-            type="date"
-            class="admin--form-input"
+            placeholder="시작일 선택"
+            :max-date="formData.end_date"
             required
-          >
+          />
           <span class="admin--date-separator">~</span>
-          <input
+          <DatePicker
             v-model="formData.end_date"
-            type="date"
-            class="admin--form-input"
+            placeholder="종료일 선택"
+            :min-date="formData.start_date"
             required
-          >
+          />
         </div>
       </div>
 
@@ -130,28 +130,28 @@
         <div class="admin--radio-group">
           <label class="admin--radio-label">
             <input
-              v-model="formData.cookie_type"
+              v-model="formData.cookie_setting"
               type="radio"
               value="today"
-              name="cookie_type"
+              name="cookie_setting"
             >
             <span>오늘 하루 창 띄우지 않음</span>
           </label>
           <label class="admin--radio-label">
             <input
-              v-model="formData.cookie_type"
+              v-model="formData.cookie_setting"
               type="radio"
               value="forever"
-              name="cookie_type"
+              name="cookie_setting"
             >
             <span>다시는 창을 띄우지 않음</span>
           </label>
           <label class="admin--radio-label">
             <input
-              v-model="formData.cookie_type"
+              v-model="formData.cookie_setting"
               type="radio"
               value="none"
-              name="cookie_type"
+              name="cookie_setting"
             >
             <span>사용 안 함</span>
           </label>
@@ -178,7 +178,7 @@
           @change="handleImageUpload"
         >
         <div v-if="imagePreview || formData.image_url" class="admin--image-preview">
-          <img :src="imagePreview || formData.image_url" alt="미리보기">
+          <img :src="imagePreview || getImageUrl(formData.image_url)" alt="미리보기">
           <button
             type="button"
             class="admin--btn-remove-image"
@@ -233,6 +233,7 @@
 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',
@@ -242,6 +243,7 @@ definePageMeta({
 const route = useRoute()
 const router = useRouter()
 const { get, put, upload } = useApi()
+const { getImageUrl } = useImage()
 
 const isLoading = ref(true)
 const isSaving = ref(false)
@@ -259,7 +261,7 @@ const formData = ref({
   height: 600,
   position_top: 100,
   position_left: 100,
-  cookie_type: 'none',
+  cookie_setting: 'none',
   content: '',
   image_url: '',
   link_url: ''
@@ -272,21 +274,36 @@ const loadPopup = async () => {
   const id = route.params.id
   const { data, error } = await get(`/basic/popup/${id}`)
 
-  if (data) {
+  console.log('[Popup Edit] 데이터 로드 응답:', { data, error })
+
+  // API 응답: { success: true, data: {...}, message }
+  if (data?.success && data?.data) {
+    const popup = data.data
     formData.value = {
-      type: data.type || 'html',
-      title: data.title || '',
-      start_date: data.start_date || '',
-      end_date: data.end_date || '',
-      width: data.width || 800,
-      height: data.height || 600,
-      position_top: data.position_top || 100,
-      position_left: data.position_left || 100,
-      cookie_type: data.cookie_type || 'none',
-      content: data.content || '',
-      image_url: data.image_url || '',
-      link_url: data.link_url || ''
+      type: popup.type || 'html',
+      title: popup.title || '',
+      start_date: popup.start_date || '',
+      end_date: popup.end_date || '',
+      width: popup.width || 800,
+      height: popup.height || 600,
+      position_top: popup.position_top || 100,
+      position_left: popup.position_left || 100,
+      cookie_setting: popup.cookie_setting || 'none',
+      content: popup.content || '',
+      image_url: popup.image_url || '',
+      link_url: popup.link_url || ''
     }
+
+    // 이미지 미리보기 설정
+    if (popup.type === 'image' && popup.image_url) {
+      imagePreview.value = popup.image_url
+      console.log('[Popup Edit] 이미지 URL:', popup.image_url)
+      console.log('[Popup Edit] 이미지 미리보기 설정:', imagePreview.value)
+    }
+
+    console.log('[Popup Edit] 데이터 로드 성공:', formData.value)
+  } else {
+    console.log('[Popup Edit] 데이터 로드 실패')
   }
 
   isLoading.value = false
@@ -353,17 +370,19 @@ const handleSubmit = async () => {
     // 이미지 업로드 (새로운 이미지가 있는 경우)
     if (formData.value.type === 'image' && imageFile.value) {
       const formDataImage = new FormData()
-      formDataImage.append('image', imageFile.value)
+      formDataImage.append('file', imageFile.value)
 
       const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
 
-      if (uploadError) {
-        errorMessage.value = '이미지 업로드에 실패했습니다.'
+      console.log('[Popup Edit] 이미지 업로드 응답:', { uploadData, uploadError })
+
+      if (uploadError || !uploadData?.success) {
+        errorMessage.value = uploadError?.message || '이미지 업로드에 실패했습니다.'
         isSaving.value = false
         return
       }
 
-      imageUrl = uploadData.url
+      imageUrl = uploadData.data?.url || uploadData.data
     }
 
     // 팝업 수정
@@ -375,10 +394,10 @@ const handleSubmit = async () => {
     const id = route.params.id
     const { data, error } = await put(`/basic/popup/${id}`, submitData)
 
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
+    if (error || !data?.success) {
+      errorMessage.value = error?.message || data?.message || '수정에 실패했습니다.'
     } else {
-      successMessage.value = '팝업이 수정되었습니다.'
+      successMessage.value = data.message || '팝업이 수정되었습니다.'
       setTimeout(() => {
         router.push('/admin/basic/popup')
       }, 1000)

+ 39 - 7
app/pages/admin/basic/popup/index.vue

@@ -173,10 +173,20 @@ const loadPopups = async () => {
 
   const { data, error } = await get('/basic/popup', params)
 
-  if (data) {
-    popups.value = data.items || []
-    totalCount.value = data.total || 0
+  console.log('[Popup List] API 응답:', { data, error })
+
+  // API 응답: { success: true, data: { items, total, page }, message }
+  if (data?.success && data?.data) {
+    popups.value = data.data.items || []
+    totalCount.value = data.data.total || 0
     totalPages.value = Math.ceil(totalCount.value / perPage.value)
+    console.log('[Popup List] 로드 성공:', {
+      items: popups.value.length,
+      total: totalCount.value,
+      pages: totalPages.value
+    })
+  } else {
+    console.log('[Popup List] 데이터 없음 또는 에러')
   }
 
   isLoading.value = false
@@ -217,12 +227,14 @@ const goToEdit = (id) => {
 const handleDelete = async (id) => {
   if (!confirm('정말 삭제하시겠습니까?')) return
 
-  const { error } = await del(`/basic/popup/${id}`)
+  const { data, error } = await del(`/basic/popup/${id}`)
 
-  if (error) {
-    alert('삭제에 실패했습니다.')
+  console.log('[Popup Delete] API 응답:', { data, error })
+
+  if (error || !data?.success) {
+    alert(error?.message || '삭제에 실패했습니다.')
   } else {
-    alert('삭제되었습니다.')
+    alert(data.message || '삭제되었습니다.')
     loadPopups()
   }
 }
@@ -260,3 +272,23 @@ onMounted(() => {
   loadPopups()
 })
 </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);
+}
+</style>

+ 39 - 30
app/pages/admin/basic/site-info.vue

@@ -197,15 +197,19 @@ const formData = ref({
 const loadSiteInfo = async () => {
   isLoading.value = true
 
-  const { data } = await get('/basic/site-info')
+  const { data, error } = await get('/basic/site-info')
 
-  if (data && data.id) {
-    siteInfoId.value = data.id
+  console.log('[SiteInfo] API 응답:', { data, error })
+
+  // API 응답: { success: true, data: {...}, message }
+  if (data?.success && data?.data) {
+    const siteData = data.data
+    siteInfoId.value = siteData.id
 
     // SMS 발신번호 데이터 변환 (기존 문자열 배열 → 객체 배열)
     let senderNumbers = [{ name: '', number: '' }]
-    if (data.sms_sender_numbers && data.sms_sender_numbers.length > 0) {
-      senderNumbers = data.sms_sender_numbers.map(item => {
+    if (siteData.sms_sender_numbers && siteData.sms_sender_numbers.length > 0) {
+      senderNumbers = siteData.sms_sender_numbers.map(item => {
         // 이미 객체 형태인 경우
         if (typeof item === 'object' && item.name !== undefined) {
           return item
@@ -216,15 +220,15 @@ const loadSiteInfo = async () => {
     }
 
     formData.value = {
-      site_name: data.site_name || '',
-      site_url: data.site_url || '',
-      ceo_name: data.ceo_name || '',
-      ceo_email: data.ceo_email || '',
-      phone: data.phone || '',
-      fax: data.fax || '',
-      address: data.address || '',
+      site_name: siteData.site_name || '',
+      site_url: siteData.site_url || '',
+      ceo_name: siteData.ceo_name || '',
+      ceo_email: siteData.ceo_email || '',
+      phone: siteData.phone || '',
+      fax: siteData.fax || '',
+      address: siteData.address || '',
       sms_sender_numbers: senderNumbers,
-      sms_receiver_number: data.sms_receiver_number || ''
+      sms_receiver_number: siteData.sms_receiver_number || ''
     }
   }
 
@@ -259,21 +263,24 @@ const handleSubmit = async () => {
     }
 
     // POST로 통일 (등록/수정 모두)
-    const result = await post('/basic/site-info', submitData)
+    const { data, error } = await post('/basic/site-info', submitData)
 
-    if (result.error) {
-      errorMessage.value = result.error.message || '저장에 실패했습니다.'
-    } else {
-      successMessage.value = '사이트 정보가 저장되었습니다.'
+    console.log('[SiteInfo] 저장 응답:', { data, error })
+
+    if (error) {
+      errorMessage.value = error.message || '저장에 실패했습니다.'
+    } else if (data?.success) {
+      successMessage.value = data.message || '사이트 정보가 저장되었습니다.'
 
       // 저장된 데이터로 업데이트
-      if (result.data) {
-        siteInfoId.value = result.data.id
+      if (data.data) {
+        const siteData = data.data
+        siteInfoId.value = siteData.id
 
         // SMS 발신번호 데이터 변환 (기존 문자열 배열 → 객체 배열)
         let senderNumbers = [{ name: '', number: '' }]
-        if (result.data.sms_sender_numbers && result.data.sms_sender_numbers.length > 0) {
-          senderNumbers = result.data.sms_sender_numbers.map(item => {
+        if (siteData.sms_sender_numbers && siteData.sms_sender_numbers.length > 0) {
+          senderNumbers = siteData.sms_sender_numbers.map(item => {
             // 이미 객체 형태인 경우
             if (typeof item === 'object' && item.name !== undefined) {
               return item
@@ -284,15 +291,15 @@ const handleSubmit = async () => {
         }
 
         formData.value = {
-          site_name: result.data.site_name || '',
-          site_url: result.data.site_url || '',
-          ceo_name: result.data.ceo_name || '',
-          ceo_email: result.data.ceo_email || '',
-          phone: result.data.phone || '',
-          fax: result.data.fax || '',
-          address: result.data.address || '',
+          site_name: siteData.site_name || '',
+          site_url: siteData.site_url || '',
+          ceo_name: siteData.ceo_name || '',
+          ceo_email: siteData.ceo_email || '',
+          phone: siteData.phone || '',
+          fax: siteData.fax || '',
+          address: siteData.address || '',
           sms_sender_numbers: senderNumbers,
-          sms_receiver_number: result.data.sms_receiver_number || ''
+          sms_receiver_number: siteData.sms_receiver_number || ''
         }
       }
 
@@ -300,6 +307,8 @@ const handleSubmit = async () => {
       setTimeout(() => {
         successMessage.value = ''
       }, 3000)
+    } else {
+      errorMessage.value = '저장에 실패했습니다.'
     }
   } catch (error) {
     errorMessage.value = '서버 오류가 발생했습니다.'

+ 15 - 12
app/pages/admin/board/event/edit/[id].vue

@@ -206,21 +206,24 @@ const loadEvent = async () => {
 
   const id = route.params.id
   const { data, error } = await get(`/board/event/${id}`)
+  console.log('[EventEdit] 데이터 로드:', { data, error })
 
-  if (data) {
+  if (data?.success && data?.data) {
+    const event = data.data
     formData.value = {
-      category: data.category || '',
-      allow_comment: data.allow_comment || false,
-      is_notice: data.is_notice || false,
-      name: data.name || '',
-      email: data.email || '',
-      start_date: data.start_date || '',
-      end_date: data.end_date || '',
-      title: data.title || '',
-      content: data.content || '',
-      file_urls: data.file_urls || []
+      category: event.category || '',
+      allow_comment: event.allow_comment || false,
+      is_notice: event.is_notice || false,
+      name: event.name || '',
+      email: event.email || '',
+      start_date: event.start_date || '',
+      end_date: event.end_date || '',
+      title: event.title || '',
+      content: event.content || '',
+      file_urls: event.file_urls || []
     }
-    existingFiles.value = data.file_urls || []
+    existingFiles.value = event.file_urls || []
+    console.log('[EventEdit] 로드 성공')
   }
 
   isLoading.value = false

+ 123 - 62
app/pages/admin/board/event/index.vue

@@ -14,9 +14,11 @@
           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>
+        <button class="admin--btn admin--btn-secondary" @click="handleReset">
+          초기화
+        </button>
       </div>
       <div class="admin--search-actions">
         <button class="admin--btn admin--btn-primary" @click="goToCreate">+ 등록</button>
@@ -44,15 +46,31 @@
             <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>
+              {{
+                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>
+                <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>
@@ -62,73 +80,116 @@
 
     <!-- 페이지네이션 -->
     <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>
+      <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'
-import { useRouter } from 'vue-router'
+  import { ref, computed, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({ layout: "admin", middleware: ["auth"] });
+
+  const router = useRouter();
+  const { get, del } = useApi();
 
-definePageMeta({ layout: 'admin', middleware: ['auth'] })
+  const isLoading = ref(false);
+  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 router = useRouter()
-const { get, del } = useApi()
+  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 isLoading = ref(false)
-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 loadPosts = async () => {
+    isLoading.value = true;
+    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);
 
-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
-})
+    console.log("[EventBoard] API 응답:", { data, error });
 
-const loadPosts = async () => {
-  isLoading.value = true
-  const params = { page: currentPage.value, per_page: perPage.value }
-  if (searchKeyword.value) {
-    params.search_type = searchType.value
-    params.search_keyword = searchKeyword.value
-  }
-  const { data } = await get('/board/event', params)
-  if (data) {
-    posts.value = data.items || []
-    totalCount.value = data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-  }
-  isLoading.value = false
-}
+    // 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);
+    }
+    isLoading.value = false;
+  };
 
-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() }
-const goToCreate = () => router.push('/admin/board/event/create')
-const goToEdit = (id) => router.push(`/admin/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')}`
-}
+  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();
+  };
+  const goToCreate = () => router.push("/admin/board/event/create");
+  const goToEdit = (id) => router.push(`/admin/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())
+  onMounted(() => loadPosts());
 </script>

+ 13 - 10
app/pages/admin/board/ir/edit/[id].vue

@@ -185,19 +185,22 @@ const loadIR = async () => {
 
   const id = route.params.id
   const { data, error } = await get(`/board/ir/${id}`)
+  console.log('[IREdit] 데이터 로드:', { data, error })
 
-  if (data) {
+  if (data?.success && data?.data) {
+    const ir = data.data
     formData.value = {
-      allow_comment: data.allow_comment || false,
-      is_notice: data.is_notice || false,
-      name: data.name || '',
-      email: data.email || '',
-      url: data.url || '',
-      title: data.title || '',
-      content: data.content || '',
-      file_urls: data.file_urls || []
+      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 = data.file_urls || []
+    existingFiles.value = ir.file_urls || []
+    console.log('[IREdit] 로드 성공')
   }
 
   isLoading.value = false

+ 123 - 62
app/pages/admin/board/ir/index.vue

@@ -14,9 +14,11 @@
           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>
+        <button class="admin--btn admin--btn-secondary" @click="handleReset">
+          초기화
+        </button>
       </div>
       <div class="admin--search-actions">
         <button class="admin--btn admin--btn-primary" @click="goToCreate">+ 등록</button>
@@ -44,15 +46,31 @@
             <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>
+              {{
+                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>
+                <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>
@@ -62,73 +80,116 @@
 
     <!-- 페이지네이션 -->
     <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>
+      <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'
-import { useRouter } from 'vue-router'
+  import { ref, computed, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({ layout: "admin", middleware: ["auth"] });
+
+  const router = useRouter();
+  const { get, del } = useApi();
 
-definePageMeta({ layout: 'admin', middleware: ['auth'] })
+  const isLoading = ref(false);
+  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 router = useRouter()
-const { get, del } = useApi()
+  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 isLoading = ref(false)
-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 loadPosts = async () => {
+    isLoading.value = true;
+    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);
 
-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
-})
+    console.log("[IRBoard] API 응답:", { data, error });
 
-const loadPosts = async () => {
-  isLoading.value = true
-  const params = { page: currentPage.value, per_page: perPage.value }
-  if (searchKeyword.value) {
-    params.search_type = searchType.value
-    params.search_keyword = searchKeyword.value
-  }
-  const { data } = await get('/board/ir', params)
-  if (data) {
-    posts.value = data.items || []
-    totalCount.value = data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-  }
-  isLoading.value = false
-}
+    // 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);
+    }
+    isLoading.value = false;
+  };
 
-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() }
-const goToCreate = () => router.push('/admin/board/ir/create')
-const goToEdit = (id) => router.push(`/admin/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')}`
-}
+  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();
+  };
+  const goToCreate = () => router.push("/admin/board/ir/create");
+  const goToEdit = (id) => router.push(`/admin/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())
+  onMounted(() => loadPosts());
 </script>

+ 13 - 10
app/pages/admin/board/news/edit/[id].vue

@@ -185,19 +185,22 @@ const loadNews = async () => {
 
   const id = route.params.id
   const { data, error } = await get(`/board/news/${id}`)
+  console.log('[NewsEdit] 데이터 로드:', { data, error })
 
-  if (data) {
+  if (data?.success && data?.data) {
+    const news = data.data
     formData.value = {
-      allow_comment: data.allow_comment || false,
-      is_notice: data.is_notice || false,
-      name: data.name || '',
-      email: data.email || '',
-      url: data.url || '',
-      title: data.title || '',
-      content: data.content || '',
-      file_urls: data.file_urls || []
+      allow_comment: news.allow_comment || false,
+      is_notice: news.is_notice || false,
+      name: news.name || '',
+      email: news.email || '',
+      url: news.url || '',
+      title: news.title || '',
+      content: news.content || '',
+      file_urls: news.file_urls || []
     }
-    existingFiles.value = data.file_urls || []
+    existingFiles.value = news.file_urls || []
+    console.log('[NewsEdit] 로드 성공')
   }
 
   isLoading.value = false

+ 123 - 62
app/pages/admin/board/news/index.vue

@@ -14,9 +14,11 @@
           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>
+        <button class="admin--btn admin--btn-secondary" @click="handleReset">
+          초기화
+        </button>
       </div>
       <div class="admin--search-actions">
         <button class="admin--btn admin--btn-primary" @click="goToCreate">+ 등록</button>
@@ -44,15 +46,31 @@
             <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>
+              {{
+                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>
+                <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>
@@ -62,73 +80,116 @@
 
     <!-- 페이지네이션 -->
     <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>
+      <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'
-import { useRouter } from 'vue-router'
+  import { ref, computed, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({ layout: "admin", middleware: ["auth"] });
+
+  const router = useRouter();
+  const { get, del } = useApi();
 
-definePageMeta({ layout: 'admin', middleware: ['auth'] })
+  const isLoading = ref(false);
+  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 router = useRouter()
-const { get, del } = useApi()
+  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 isLoading = ref(false)
-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 loadPosts = async () => {
+    isLoading.value = true;
+    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);
 
-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
-})
+    console.log("[NewsBoard] API 응답:", { data, error });
 
-const loadPosts = async () => {
-  isLoading.value = true
-  const params = { page: currentPage.value, per_page: perPage.value }
-  if (searchKeyword.value) {
-    params.search_type = searchType.value
-    params.search_keyword = searchKeyword.value
-  }
-  const { data } = await get('/board/news', params)
-  if (data) {
-    posts.value = data.items || []
-    totalCount.value = data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-  }
-  isLoading.value = false
-}
+    // 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);
+    }
+    isLoading.value = false;
+  };
 
-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() }
-const goToCreate = () => router.push('/admin/board/news/create')
-const goToEdit = (id) => router.push(`/admin/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')}`
-}
+  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();
+  };
+  const goToCreate = () => router.push("/admin/board/news/create");
+  const goToEdit = (id) => router.push(`/admin/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())
+  onMounted(() => loadPosts());
 </script>

+ 208 - 0
app/pages/admin/branch/create.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="admin--branch-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>
+        <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="number"
+              step="any"
+              class="admin--form-input"
+              placeholder="37.5665"
+            >
+          </div>
+          <div class="admin--coordinate-item">
+            <label>경도</label>
+            <input
+              v-model.number="formData.longitude"
+              type="number"
+              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-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'
+
+definePageMeta({
+  layout: 'admin',
+  middleware: ['auth']
+})
+
+const router = useRouter()
+const { post } = useApi()
+
+const isSaving = ref(false)
+const successMessage = ref('')
+const errorMessage = ref('')
+
+const formData = ref({
+  name: '',
+  phone: '',
+  address: '',
+  detail_address: '',
+  latitude: null,
+  longitude: null,
+  business_hours: ''
+})
+
+// 폼 제출
+const handleSubmit = async () => {
+  successMessage.value = ''
+  errorMessage.value = ''
+
+  // 유효성 검사
+  if (!formData.value.name) {
+    errorMessage.value = '지점명을 입력하세요.'
+    return
+  }
+
+  if (!formData.value.phone) {
+    errorMessage.value = '대표번호를 입력하세요.'
+    return
+  }
+
+  if (!formData.value.address) {
+    errorMessage.value = '주소를 입력하세요.'
+    return
+  }
+
+  isSaving.value = true
+
+  try {
+    const { data, error } = await post('/branch', formData.value)
+
+    if (error || !data?.success) {
+      errorMessage.value = error?.message || data?.message || '등록에 실패했습니다.'
+    } else {
+      successMessage.value = data.message || '지점이 등록되었습니다.'
+      setTimeout(() => {
+        router.push('/admin/branch/list')
+      }, 1000)
+    }
+  } catch (error) {
+    errorMessage.value = '서버 오류가 발생했습니다.'
+    console.error('Save error:', error)
+  } finally {
+    isSaving.value = false
+  }
+}
+
+// 목록으로 이동
+const goToList = () => {
+  router.push('/admin/branch/list')
+}
+</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>

+ 242 - 0
app/pages/admin/branch/edit/[id].vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="admin--branch-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>
+        <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="number"
+              step="any"
+              class="admin--form-input"
+              placeholder="37.5665"
+            >
+          </div>
+          <div class="admin--coordinate-item">
+            <label>경도</label>
+            <input
+              v-model.number="formData.longitude"
+              type="number"
+              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-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 } = useApi()
+
+const isLoading = ref(true)
+const isSaving = ref(false)
+const successMessage = ref('')
+const errorMessage = ref('')
+
+const formData = ref({
+  name: '',
+  phone: '',
+  address: '',
+  detail_address: '',
+  latitude: null,
+  longitude: null,
+  business_hours: ''
+})
+
+// 데이터 로드
+const loadBranch = async () => {
+  isLoading.value = true
+
+  const id = route.params.id
+  const { data, error } = await get(`/branch/${id}`)
+
+  if (data?.success && data?.data) {
+    const branch = data.data
+    formData.value = {
+      name: branch.name || '',
+      phone: branch.phone || '',
+      address: branch.address || '',
+      detail_address: branch.detail_address || '',
+      latitude: branch.latitude || null,
+      longitude: branch.longitude || null,
+      business_hours: branch.business_hours || ''
+    }
+  }
+
+  isLoading.value = false
+}
+
+// 폼 제출
+const handleSubmit = async () => {
+  successMessage.value = ''
+  errorMessage.value = ''
+
+  // 유효성 검사
+  if (!formData.value.name) {
+    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 { data, error } = await put(`/branch/${id}`, formData.value)
+
+    if (error || !data?.success) {
+      errorMessage.value = error?.message || data?.message || '수정에 실패했습니다.'
+    } else {
+      successMessage.value = data.message || '지점이 수정되었습니다.'
+      setTimeout(() => {
+        router.push('/admin/branch/list')
+      }, 1000)
+    }
+  } catch (error) {
+    errorMessage.value = '서버 오류가 발생했습니다.'
+    console.error('Save error:', error)
+  } finally {
+    isSaving.value = false
+  }
+}
+
+// 목록으로 이동
+const goToList = () => {
+  router.push('/admin/branch/list')
+}
+
+onMounted(() => {
+  loadBranch()
+})
+</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>

+ 84 - 7
app/pages/admin/branch/list.vue

@@ -1,5 +1,15 @@
 <template>
   <div class="admin--branch-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">
@@ -9,24 +19,41 @@
             <th>지점명</th>
             <th>대표번호</th>
             <th>주소</th>
+            <th>관리</th>
           </tr>
         </thead>
         <tbody>
           <tr v-if="isLoading">
-            <td colspan="4" class="admin--table-loading">
+            <td colspan="5" class="admin--table-loading">
               데이터를 불러오는 중...
             </td>
           </tr>
           <tr v-else-if="!branches || branches.length === 0">
-            <td colspan="4" class="admin--table-empty">
+            <td colspan="5" class="admin--table-empty">
               등록된 지점이 없습니다.
             </td>
           </tr>
           <tr v-else v-for="(branch, index) in branches" :key="branch.id">
             <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
             <td class="admin--table-title">{{ branch.name }}</td>
-            <td>{{ branch.phone }}</td>
+            <td>{{ branch.main_phone }}</td>
             <td>{{ branch.address }}</td>
+            <td>
+              <div class="admin--table-actions">
+                <button
+                  class="admin--btn-small admin--btn-small-primary"
+                  @click="goToEdit(branch.id)"
+                >
+                  수정
+                </button>
+                <button
+                  class="admin--btn-small admin--btn-small-danger"
+                  @click="deleteBranch(branch.id)"
+                >
+                  삭제
+                </button>
+              </div>
+            </td>
           </tr>
         </tbody>
       </table>
@@ -63,13 +90,15 @@
 
 <script setup>
 import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
 
 definePageMeta({
   layout: 'admin',
   middleware: ['auth']
 })
 
-const { get } = useApi()
+const router = useRouter()
+const { get, del } = useApi()
 
 const isLoading = ref(false)
 const branches = ref([])
@@ -107,10 +136,14 @@ const loadBranches = async () => {
 
   const { data, error } = await get('/branch/list', params)
 
-  if (data) {
-    branches.value = data.items || []
-    totalCount.value = data.total || 0
+  console.log('[BranchList] API 응답:', { data, error })
+
+  // API 응답: { success: true, data: { items, total }, message }
+  if (data?.success && data?.data) {
+    branches.value = data.data.items || []
+    totalCount.value = data.data.total || 0
     totalPages.value = Math.ceil(totalCount.value / perPage.value)
+    console.log('[BranchList] 로드 성공:', branches.value.length)
   }
 
   isLoading.value = false
@@ -123,7 +156,51 @@ const changePage = (page) => {
   loadBranches()
 }
 
+// 지점 등록 페이지로 이동
+const goToCreate = () => {
+  router.push('/admin/branch/create')
+}
+
+// 지점 수정 페이지로 이동
+const goToEdit = (id) => {
+  router.push(`/admin/branch/edit/${id}`)
+}
+
+// 지점 삭제
+const deleteBranch = async (id) => {
+  if (!confirm('정말 삭제하시겠습니까?')) return
+
+  const { data, error } = await del(`/branch/${id}`)
+
+  if (error || !data?.success) {
+    alert(error?.message || data?.message || '삭제에 실패했습니다.')
+  } else {
+    alert(data.message || '지점이 삭제되었습니다.')
+    loadBranches()
+  }
+}
+
 onMounted(() => {
   loadBranches()
 })
 </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);
+}
+</style>

+ 4 - 2
app/pages/admin/branch/manager/create.vue

@@ -158,9 +158,11 @@ const formData = ref({
 // 지점 목록 로드
 const loadBranches = async () => {
   const { data, error } = await get('/branch/list', { per_page: 1000 })
+  console.log('[BranchManagerCreate] API 응답:', { data, error })
 
-  if (data) {
-    branches.value = data.items || []
+  if (data?.success && data?.data) {
+    branches.value = data.data.items || []
+    console.log('[BranchManagerCreate] 지점 목록 로드 성공')
   }
 }
 

+ 12 - 7
app/pages/admin/branch/manager/edit/[id].vue

@@ -166,9 +166,11 @@ const formData = ref({
 // 지점 목록 로드
 const loadBranches = async () => {
   const { data, error } = await get('/branch/list', { per_page: 1000 })
+  console.log('[BranchManagerEdit] API 응답:', { data, error })
 
-  if (data) {
-    branches.value = data.items || []
+  if (data?.success && data?.data) {
+    branches.value = data.data.items || []
+    console.log('[BranchManagerEdit] 지점 목록 로드 성공')
   }
 }
 
@@ -178,16 +180,19 @@ const loadManager = async () => {
 
   const id = route.params.id
   const { data, error } = await get(`/branch/manager/${id}`)
+  console.log('[BranchManagerEdit] 데이터 로드:', { data, error })
 
-  if (data) {
+  if (data?.success && data?.data) {
+    const manager = data.data
     formData.value = {
-      branch_id: data.branch_id || '',
-      user_id: data.user_id || '',
+      branch_id: manager.branch_id || '',
+      user_id: manager.user_id || '',
       password: '',
       password_confirm: '',
-      name: data.name || '',
-      email: data.email || ''
+      name: manager.name || '',
+      email: manager.email || ''
     }
+    console.log('[BranchManagerEdit] 로드 성공')
   }
 
   isLoading.value = false

+ 28 - 4
app/pages/admin/branch/manager/index.vue

@@ -58,7 +58,7 @@
             <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
             <td>{{ manager.branch_name }}</td>
             <td class="admin--table-title">{{ manager.name }}</td>
-            <td>{{ manager.user_id }}</td>
+            <td>{{ manager.username }}</td>
             <td>{{ manager.email }}</td>
             <td>
               <div class="admin--table-actions">
@@ -165,10 +165,14 @@ const loadManagers = async () => {
 
   const { data, error } = await get('/branch/manager', params)
 
-  if (data) {
-    managers.value = data.items || []
-    totalCount.value = data.total || 0
+  console.log('[BranchManager] API 응답:', { data, error })
+
+  // API 응답: { success: true, data: { items, total }, message }
+  if (data?.success && data?.data) {
+    managers.value = data.data.items || []
+    totalCount.value = data.data.total || 0
     totalPages.value = Math.ceil(totalCount.value / perPage.value)
+    console.log('[BranchManager] 로드 성공:', managers.value.length)
   }
 
   isLoading.value = false
@@ -223,3 +227,23 @@ onMounted(() => {
   loadManagers()
 })
 </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);
+}
+</style>

+ 7 - 3
app/pages/admin/service/brochure.vue

@@ -162,10 +162,14 @@ const loadBrochures = async () => {
 
   const { data, error } = await get('/service/brochure', params)
 
-  if (data) {
-    brochures.value = data.items || []
-    totalCount.value = data.total || 0
+  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

+ 7 - 2
app/pages/admin/staff/advisor/create.vue

@@ -133,8 +133,13 @@ const formData = ref({
 })
 
 const loadServiceCenters = async () => {
-  const { data } = await get('/staff/service-centers')
-  if (data) serviceCenters.value = data
+  const { data, error } = await get('/staff/service-centers')
+  console.log('[AdvisorCreate] API 응답:', { data, error })
+
+  if (data?.success && data?.data) {
+    serviceCenters.value = data.data
+    console.log('[AdvisorCreate] 서비스센터 로드 성공')
+  }
 }
 
 const handlePhotoUpload = (event) => {

+ 7 - 2
app/pages/admin/staff/advisor/edit/[id].vue

@@ -133,8 +133,13 @@ const formData = ref({
 })
 
 const loadServiceCenters = async () => {
-  const { data } = await get('/staff/service-centers')
-  if (data) serviceCenters.value = data
+  const { data, error } = await get('/staff/service-centers')
+  console.log('[AdvisorEdit] API 응답:', { data, error })
+
+  if (data?.success && data?.data) {
+    serviceCenters.value = data.data
+    console.log('[AdvisorEdit] 서비스센터 로드 성공')
+  }
 }
 
 const handlePhotoUpload = (event) => {

+ 116 - 114
app/pages/admin/staff/advisor/index.vue

@@ -14,10 +14,8 @@
           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-primary" @click="handleSearch">검색</button>
         <button class="admin--btn admin--btn-secondary" @click="handleReset">
           초기화
         </button>
@@ -45,20 +43,20 @@
         </thead>
         <tbody>
           <tr v-if="isLoading">
-            <td colspan="7" class="admin--table-loading">
-              데이터를 불러오는 중...
-            </td>
+            <td colspan="7" class="admin--table-loading">데이터를 불러오는 중...</td>
           </tr>
           <tr v-else-if="!advisors || advisors.length === 0">
-            <td colspan="7" class="admin--table-empty">
-              등록된 어드바이저가 없습니다.
-            </td>
+            <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="advisor.photo_url" :alt="advisor.name">
+                <img
+                  v-if="advisor.photo_url"
+                  :src="advisor.photo_url"
+                  :alt="advisor.name"
+                />
                 <div v-else class="admin--table-photo-empty">사진없음</div>
               </div>
             </td>
@@ -117,107 +115,111 @@
 </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 isLoading = ref(false)
-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 () => {
-  isLoading.value = true
-
-  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)
-
-  if (data) {
-    advisors.value = data.items || []
-    totalCount.value = data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-  }
-
-  isLoading.value = false
-}
-
-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()
-}
-
-const goToCreate = () => {
-  router.push('/admin/staff/advisor/create')
-}
-
-const goToEdit = (id) => {
-  router.push(`/admin/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()
-})
+  import { ref, computed, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({
+    layout: "admin",
+    middleware: ["auth"],
+  });
+
+  const router = useRouter();
+  const { get, del } = useApi();
+
+  const isLoading = ref(false);
+  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 () => {
+    isLoading.value = true;
+
+    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);
+    }
+
+    isLoading.value = false;
+  };
+
+  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();
+  };
+
+  const goToCreate = () => {
+    router.push("/admin/staff/advisor/create");
+  };
+
+  const goToEdit = (id) => {
+    router.push(`/admin/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>

+ 34 - 35
app/pages/admin/staff/sales/create.vue

@@ -12,17 +12,6 @@
         </select>
       </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>
@@ -49,15 +38,14 @@
       <!-- 직책 -->
       <div class="admin--form-group">
         <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
-        <select v-model="formData.position" class="admin--form-select" required>
+        <select v-model.number="formData.position" class="admin--form-select" required>
           <option value="">직책을 선택하세요</option>
-          <option value="팀장">팀장</option>
-          <option value="마스터">마스터</option>
-          <option value="차장">차장</option>
-          <option value="과장">과장</option>
-          <option value="대리">대리</option>
-          <option value="주임">주임</option>
-          <option value="사원">사원</option>
+          <option :value="10">팀장</option>
+          <option :value="15">마스터</option>
+          <option :value="20">차장</option>
+          <option :value="30">과장</option>
+          <option :value="40">대리</option>
+          <option :value="60">사원</option>
         </select>
       </div>
 
@@ -214,12 +202,24 @@ const photoPreview = ref(null)
 const photoFile = ref(null)
 
 const showrooms = ref([])
-const branches = ref([])
-const teams = ref([])
+
+// 영업팀 수동 리스트 (0번째는 마스터팀)
+const teams = ref([
+  { id: 0, name: '마스터팀' },
+  { id: 1, name: '1팀' },
+  { id: 2, name: '2팀' },
+  { id: 3, name: '3팀' },
+  { id: 4, name: '4팀' },
+  { id: 5, name: '5팀' },
+  { id: 6, name: '6팀' },
+  { id: 7, name: '7팀' },
+  { id: 8, name: '8팀' },
+  { id: 9, name: '9팀' },
+  { id: 10, name: '10팀' }
+])
 
 const formData = ref({
   showroom_id: '',
-  branch_id: '',
   team_id: '',
   name: '',
   position: '',
@@ -235,14 +235,13 @@ const formData = ref({
 
 // 필터 데이터 로드
 const loadFilters = async () => {
-  const { data: showroomData } = await get('/staff/showrooms')
-  if (showroomData) showrooms.value = showroomData
-
-  const { data: branchData } = await get('/branch/list', { per_page: 1000 })
-  if (branchData) branches.value = branchData.items || []
-
-  const { data: teamData } = await get('/staff/teams')
-  if (teamData) teams.value = teamData
+  // 전시장 리스트 (지점 목록)
+  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] 전시장(지점) 로드 성공')
+  }
 }
 
 // 사진 업로드
@@ -276,8 +275,8 @@ const handleSubmit = async () => {
   successMessage.value = ''
   errorMessage.value = ''
 
-  if (!formData.value.showroom_id || !formData.value.branch_id || !formData.value.team_id) {
-    errorMessage.value = '전시장, 지점, 영업팀을 선택하세요.'
+  if (!formData.value.showroom_id || !formData.value.team_id) {
+    errorMessage.value = '전시장, 영업팀을 선택하세요.'
     return
   }
 
@@ -294,9 +293,9 @@ const handleSubmit = async () => {
     // 사진 업로드
     if (photoFile.value) {
       const formDataImage = new FormData()
-      formDataImage.append('image', photoFile.value)
+      formDataImage.append('file', photoFile.value)
 
-      const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
+      const { data: uploadData, error: uploadError } = await upload('/upload/staff-image', formDataImage)
 
       if (uploadError) {
         errorMessage.value = '사진 업로드에 실패했습니다.'
@@ -304,7 +303,7 @@ const handleSubmit = async () => {
         return
       }
 
-      photoUrl = uploadData.url
+      photoUrl = uploadData.data?.url || uploadData.url
     }
 
     const submitData = {

+ 52 - 50
app/pages/admin/staff/sales/edit/[id].vue

@@ -16,17 +16,6 @@
         </select>
       </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>
@@ -53,15 +42,14 @@
       <!-- 직책 -->
       <div class="admin--form-group">
         <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
-        <select v-model="formData.position" class="admin--form-select" required>
+        <select v-model.number="formData.position" class="admin--form-select" required>
           <option value="">직책을 선택하세요</option>
-          <option value="팀장">팀장</option>
-          <option value="마스터">마스터</option>
-          <option value="차장">차장</option>
-          <option value="과장">과장</option>
-          <option value="대리">대리</option>
-          <option value="주임">주임</option>
-          <option value="사원">사원</option>
+          <option :value="10">팀장</option>
+          <option :value="15">마스터</option>
+          <option :value="20">차장</option>
+          <option :value="30">과장</option>
+          <option :value="40">대리</option>
+          <option :value="60">사원</option>
         </select>
       </div>
 
@@ -120,7 +108,7 @@
           @change="handlePhotoUpload"
         >
         <div v-if="photoPreview || formData.photo_url" class="admin--image-preview">
-          <img :src="photoPreview || formData.photo_url" alt="미리보기">
+          <img :src="photoPreview || getImageUrl(formData.photo_url)" alt="미리보기">
           <button type="button" class="admin--btn-remove-image" @click="removePhoto">
             삭제
           </button>
@@ -211,6 +199,7 @@ definePageMeta({
 const route = useRoute()
 const router = useRouter()
 const { get, put, upload } = useApi()
+const { getImageUrl } = useImage()
 
 const isLoading = ref(true)
 const isSaving = ref(false)
@@ -220,12 +209,24 @@ const photoPreview = ref(null)
 const photoFile = ref(null)
 
 const showrooms = ref([])
-const branches = ref([])
-const teams = ref([])
+
+// 영업팀 수동 리스트 (0번째는 마스터팀)
+const teams = ref([
+  { id: 0, name: '마스터팀' },
+  { id: 1, name: '1팀' },
+  { id: 2, name: '2팀' },
+  { id: 3, name: '3팀' },
+  { id: 4, name: '4팀' },
+  { id: 5, name: '5팀' },
+  { id: 6, name: '6팀' },
+  { id: 7, name: '7팀' },
+  { id: 8, name: '8팀' },
+  { id: 9, name: '9팀' },
+  { id: 10, name: '10팀' }
+])
 
 const formData = ref({
   showroom_id: '',
-  branch_id: '',
   team_id: '',
   name: '',
   position: '',
@@ -241,14 +242,13 @@ const formData = ref({
 
 // 필터 데이터 로드
 const loadFilters = async () => {
-  const { data: showroomData } = await get('/staff/showrooms')
-  if (showroomData) showrooms.value = showroomData
-
-  const { data: branchData } = await get('/branch/list', { per_page: 1000 })
-  if (branchData) branches.value = branchData.items || []
-
-  const { data: teamData } = await get('/staff/teams')
-  if (teamData) teams.value = teamData
+  // 전시장 리스트 (지점 목록)
+  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] 전시장(지점) 로드 성공')
+  }
 }
 
 // 데이터 로드
@@ -257,23 +257,25 @@ const loadSales = async () => {
 
   const id = route.params.id
   const { data, error } = await get(`/staff/sales/${id}`)
+  console.log('[SalesEdit] 데이터 로드:', { data, error })
 
-  if (data) {
+  if (data?.success && data?.data) {
+    const sales = data.data
     formData.value = {
-      showroom_id: data.showroom_id || '',
-      branch_id: data.branch_id || '',
-      team_id: data.team_id || '',
-      name: data.name || '',
-      position: data.position || '',
-      main_phone: data.main_phone || '',
-      direct_phone: data.direct_phone || '',
-      mobile: data.mobile || '',
-      email: data.email || '',
-      photo_url: data.photo_url || '',
-      is_sact: data.is_sact || false,
-      is_top30: data.is_top30 || false,
-      display_order: data.display_order || 0
+      showroom_id: sales.showroom_id || '',
+      team_id: sales.team_id || '',
+      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 || false,
+      is_top30: sales.is_top30 || false,
+      display_order: sales.display_order || 0
     }
+    console.log('[SalesEdit] 로드 성공')
   }
 
   isLoading.value = false
@@ -310,8 +312,8 @@ const handleSubmit = async () => {
   successMessage.value = ''
   errorMessage.value = ''
 
-  if (!formData.value.showroom_id || !formData.value.branch_id || !formData.value.team_id) {
-    errorMessage.value = '전시장, 지점, 영업팀을 선택하세요.'
+  if (!formData.value.showroom_id || !formData.value.team_id) {
+    errorMessage.value = '전시장, 영업팀을 선택하세요.'
     return
   }
 
@@ -328,9 +330,9 @@ const handleSubmit = async () => {
     // 새 사진 업로드
     if (photoFile.value) {
       const formDataImage = new FormData()
-      formDataImage.append('image', photoFile.value)
+      formDataImage.append('file', photoFile.value)
 
-      const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
+      const { data: uploadData, error: uploadError } = await upload('/upload/staff-image', formDataImage)
 
       if (uploadError) {
         errorMessage.value = '사진 업로드에 실패했습니다.'
@@ -338,7 +340,7 @@ const handleSubmit = async () => {
         return
       }
 
-      photoUrl = uploadData.url
+      photoUrl = uploadData.data?.url || uploadData.url
     }
 
     const submitData = {

+ 174 - 176
app/pages/admin/staff/sales/index.vue

@@ -12,14 +12,6 @@
             </option>
           </select>
 
-          <label class="admin--filter-label">지점</label>
-          <select v-model="filters.branch" class="admin--form-select">
-            <option value="">전체</option>
-            <option v-for="branch in branches" :key="branch.id" :value="branch.id">
-              {{ branch.name }}
-            </option>
-          </select>
-
           <label class="admin--filter-label">팀</label>
           <select v-model="filters.team" class="admin--form-select">
             <option value="">전체</option>
@@ -37,7 +29,7 @@
             class="admin--form-input"
             placeholder="이름으로 검색"
             @keyup.enter="handleSearch"
-          >
+          />
           <button class="admin--btn admin--btn-primary" @click="handleSearch">
             검색
           </button>
@@ -68,7 +60,6 @@
             <th>NO</th>
             <th>사진</th>
             <th>전시장</th>
-            <th>지점</th>
             <th>팀</th>
             <th>이름</th>
             <th>직책</th>
@@ -80,25 +71,20 @@
         </thead>
         <tbody>
           <tr v-if="isLoading">
-            <td colspan="11" class="admin--table-loading">
-              데이터를 불러오는 중...
-            </td>
+            <td colspan="10" class="admin--table-loading">데이터를 불러오는 중...</td>
           </tr>
           <tr v-else-if="!salesList || salesList.length === 0">
-            <td colspan="11" class="admin--table-empty">
-              등록된 영업사원이 없습니다.
-            </td>
+            <td colspan="10" 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="sales.photo_url" :alt="sales.name">
+                <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>{{ sales.showroom_name }}</td>
-            <td>{{ sales.branch_name }}</td>
             <td>{{ sales.team_name }}</td>
             <td class="admin--table-title">{{ sales.name }}</td>
             <td>{{ sales.position }}</td>
@@ -168,162 +154,174 @@
 </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 isLoading = ref(false)
-const salesList = ref([])
-const showrooms = ref([])
-const branches = ref([])
-const teams = ref([])
-
-const filters = ref({
-  showroom: '',
-  branch: '',
-  team: '',
-  keyword: ''
-})
-
-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 loadFilters = async () => {
-  // 전시장 리스트
-  const { data: showroomData } = await get('/staff/showrooms')
-  if (showroomData) showrooms.value = showroomData
-
-  // 지점 리스트
-  const { data: branchData } = await get('/branch/list', { per_page: 1000 })
-  if (branchData) branches.value = branchData.items || []
-
-  // 팀 리스트
-  const { data: teamData } = await get('/staff/teams')
-  if (teamData) teams.value = teamData
-}
-
-// 데이터 로드
-const loadSales = async () => {
-  isLoading.value = true
-
-  const params = {
-    page: currentPage.value,
-    per_page: perPage.value,
-    ...filters.value
-  }
-
-  const { data, error } = await get('/staff/sales', params)
-
-  if (data) {
-    salesList.value = data.items || []
-    totalCount.value = data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-  }
-
-  isLoading.value = false
-}
-
-// 검색
-const handleSearch = () => {
-  currentPage.value = 1
-  loadSales()
-}
-
-// 초기화
-const handleReset = () => {
-  filters.value = {
-    showroom: '',
-    branch: '',
-    team: '',
-    keyword: ''
-  }
-  currentPage.value = 1
-  loadSales()
-}
-
-// 페이지 변경
-const changePage = (page) => {
-  if (page < 1 || page > totalPages.value) return
-  currentPage.value = page
-  loadSales()
-}
-
-// 엑셀 다운로드 (전체)
-const handleExcelDownload = async () => {
-  const params = { ...filters.value }
-  window.open(`/api/staff/sales/excel?${new URLSearchParams(params)}`, '_blank')
-}
-
-// A2 출력
-const handleA2Print = () => {
-  const params = { ...filters.value }
-  window.open(`/api/staff/sales/print-a2?${new URLSearchParams(params)}`, '_blank')
-}
-
-// 개별 프린트
-const handlePrint = (id) => {
-  window.open(`/api/staff/sales/${id}/print`, '_blank')
-}
-
-// 개별 엑셀 출력
-const handleExcelExport = (id) => {
-  window.open(`/api/staff/sales/${id}/excel`, '_blank')
-}
-
-// 등록 페이지로 이동
-const goToCreate = () => {
-  router.push('/admin/staff/sales/create')
-}
-
-// 수정 페이지로 이동
-const goToEdit = (id) => {
-  router.push(`/admin/staff/sales/edit/${id}`)
-}
-
-// 삭제
-const handleDelete = async (id) => {
-  if (!confirm('정말 삭제하시겠습니까?')) return
-
-  const { error } = await del(`/staff/sales/${id}`)
-
-  if (error) {
-    alert('삭제에 실패했습니다.')
-  } else {
-    alert('삭제되었습니다.')
-    loadSales()
-  }
-}
-
-onMounted(() => {
-  loadFilters()
-  loadSales()
-})
+  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 isLoading = ref(false);
+  const salesList = ref([]);
+  const showrooms = ref([]);
+
+  // 영업팀 수동 리스트 (0번째는 마스터팀)
+  const teams = ref([
+    { id: 0, name: '마스터팀' },
+    { id: 1, name: '1팀' },
+    { id: 2, name: '2팀' },
+    { id: 3, name: '3팀' },
+    { id: 4, name: '4팀' },
+    { id: 5, name: '5팀' },
+    { id: 6, name: '6팀' },
+    { id: 7, name: '7팀' },
+    { id: 8, name: '8팀' },
+    { id: 9, name: '9팀' },
+    { id: 10, name: '10팀' }
+  ]);
+
+  const filters = ref({
+    showroom: "",
+    team: "",
+    keyword: "",
+  });
+
+  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 loadFilters = async () => {
+    // 전시장 리스트 (지점 목록)
+    const { data: branchData, error: branchError } = await get("/branch/list", {
+      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 () => {
+    isLoading.value = true;
+
+    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] 영업사원 목록 로드 성공");
+    }
+
+    isLoading.value = false;
+  };
+
+  // 검색
+  const handleSearch = () => {
+    currentPage.value = 1;
+    loadSales();
+  };
+
+  // 초기화
+  const handleReset = () => {
+    filters.value = {
+      showroom: "",
+      team: "",
+      keyword: "",
+    };
+    currentPage.value = 1;
+    loadSales();
+  };
+
+  // 페이지 변경
+  const changePage = (page) => {
+    if (page < 1 || page > totalPages.value) return;
+    currentPage.value = page;
+    loadSales();
+  };
+
+  // 엑셀 다운로드 (전체)
+  const handleExcelDownload = async () => {
+    const params = { ...filters.value };
+    window.open(`/api/staff/sales/excel?${new URLSearchParams(params)}`, "_blank");
+  };
+
+  // A2 출력
+  const handleA2Print = () => {
+    const params = { ...filters.value };
+    window.open(`/api/staff/sales/print-a2?${new URLSearchParams(params)}`, "_blank");
+  };
+
+  // 개별 프린트
+  const handlePrint = (id) => {
+    window.open(`/api/staff/sales/${id}/print`, "_blank");
+  };
+
+  // 개별 엑셀 출력
+  const handleExcelExport = (id) => {
+    window.open(`/api/staff/sales/${id}/excel`, "_blank");
+  };
+
+  // 등록 페이지로 이동
+  const goToCreate = () => {
+    router.push("/admin/staff/sales/create");
+  };
+
+  // 수정 페이지로 이동
+  const goToEdit = (id) => {
+    router.push(`/admin/staff/sales/edit/${id}`);
+  };
+
+  // 삭제
+  const handleDelete = async (id) => {
+    if (!confirm("정말 삭제하시겠습니까?")) return;
+
+    const { error } = await del(`/staff/sales/${id}`);
+
+    if (error) {
+      alert("삭제에 실패했습니다.");
+    } else {
+      alert("삭제되었습니다.");
+      loadSales();
+    }
+  };
+
+  onMounted(() => {
+    loadFilters();
+    loadSales();
+  });
 </script>

+ 6 - 2
app/pages/index.vue

@@ -1,7 +1,11 @@
 <template>
-  <main><carouselModels /></main>
+  <main>
+    <carouselModels />
+    <Popup />
+  </main>
 </template>
 
 <script setup>
-  import carouselModels from "~/components/block/carouselModels.vue";
+import carouselModels from "~/components/block/carouselModels.vue"
+import Popup from "~/components/Popup.vue"
 </script>

+ 49 - 0
gojinaudi_key_20251106.pem

@@ -0,0 +1,49 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAgEAsLXHKvi3PhGLOZBWH1igTzsx+eJyCaiVj1YVebX2FSFyD40PaLaI
+u5T+e093hVopDMD66Qh5E0HStfe4nuAXrOblsh3ec2TobUQiN7+h0NuIEuAQZ/qAdoEw/V
+AQx4LZXdgHc3CQg8PXNuhrqxJqSLKwN7rbPqB4Ft/TiXRgP57UL4z6TZWsZ2ykO3i9MGaZ
+HPPdAfM6HklBiLFhjHxoTia/7MQQl5zWjQgfXqacKnYqc/vvsa/0/rCNXWsqfML3py00ui
+olw4UIiO/RfTuJtA7f7Nkznz3+hlE/P1eA1ci3+5GozCJ8Z5dsNpI5lBXWv91ItsvlX+qK
+4hqIGsPR6g1YsltJhEZV+2Wsx6JtRG4QybXmUeVME31ufbRYfhPTrQSEw8cXkCyZj3T6SO
+saPTvx5ZVdwJ7MTUi67MArcA4ISZXzifcwRUY/Ht/x4cp4IColVnH3H/12HHRTuWY2u4v1
+fuUbDthuR9KCTM+cUn3u839DObYbU8/XBw+ztCYbnD6YNN7s845bLvly11MwOiXbvUpqEd
+4tDERKNAcK7U+LUALwFki4aMWsgEtaRYu4rsx9/OLm6SO3YnIE0B/1ptLrNqs80H2BgPSH
+0m44PrQ0pTrXdxhSdfJ69atHm83B5RS/T41J3thFrotkC4jDU8QQypOWuTuY/KaaXh8qJ0
+MAAAdYawhZC2sIWQsAAAAHc3NoLXJzYQAAAgEAsLXHKvi3PhGLOZBWH1igTzsx+eJyCaiV
+j1YVebX2FSFyD40PaLaIu5T+e093hVopDMD66Qh5E0HStfe4nuAXrOblsh3ec2TobUQiN7
++h0NuIEuAQZ/qAdoEw/VAQx4LZXdgHc3CQg8PXNuhrqxJqSLKwN7rbPqB4Ft/TiXRgP57U
+L4z6TZWsZ2ykO3i9MGaZHPPdAfM6HklBiLFhjHxoTia/7MQQl5zWjQgfXqacKnYqc/vvsa
+/0/rCNXWsqfML3py00uiolw4UIiO/RfTuJtA7f7Nkznz3+hlE/P1eA1ci3+5GozCJ8Z5ds
+NpI5lBXWv91ItsvlX+qK4hqIGsPR6g1YsltJhEZV+2Wsx6JtRG4QybXmUeVME31ufbRYfh
+PTrQSEw8cXkCyZj3T6SOsaPTvx5ZVdwJ7MTUi67MArcA4ISZXzifcwRUY/Ht/x4cp4ICol
+VnH3H/12HHRTuWY2u4v1fuUbDthuR9KCTM+cUn3u839DObYbU8/XBw+ztCYbnD6YNN7s84
+5bLvly11MwOiXbvUpqEd4tDERKNAcK7U+LUALwFki4aMWsgEtaRYu4rsx9/OLm6SO3YnIE
+0B/1ptLrNqs80H2BgPSH0m44PrQ0pTrXdxhSdfJ69atHm83B5RS/T41J3thFrotkC4jDU8
+QQypOWuTuY/KaaXh8qJ0MAAAADAQABAAACABIf8vfDXvgs0HztAwhgDMFTrwKUaWH4Oq7j
+A3ziXwU30v0pWMVCw6+JzrhTJE03PDKksJeqWNDS1Yv4hqU1EviXDkRAsAph9T0P2fqh2z
+US71gQR16C4R5GjgHNbosoLqdjexAqIYiCU9a77B812lTujwiIT+iSiP6/onDc0Op1ngnq
+idnfWjmZeRbogW8vdtDzal3C1tk4ZlJg70J7mC875j+gtJr4aUE57g3FRQtN53jSBHnTNG
+vTLAzC6y60yLYK+veFTy5IvOFex7vymWMwi2M9u/+/WhXoy3XxwbMrzUMuY4Pcnan6bA0E
+3ocD3mz7g3PMYhB+fBRI6GDmaOdPHLHr+gMBZHLotEbojtXOI9Y5oCCTBWjCV/jty3xmss
+Mjuz6SA6Lzqv/0OWu5q02EE4ju9yE+XkLBYzBTuTTCZVZKBlmDQNpfikwm0RUISYbKhb9W
+Df7wDIlBOBHDLR2WgMo4St3TljivrlKsX709qeGzIuLZvJ8oyPJqMaTccq6UQ1QZLgU7zj
+QAixeou7z8Xv5VRb62VVKYatq7G3HVEqxWN7RRYlkn9JwTR+guZLTUPSB+3ff0B9hnX3v1
+foCEoD73hlKrSLdneujp9wbVrJqkYUgqUE5V4gA+QQY38ctHOUqoYISZbGK/1MQU1j8Uqr
+VQF1mufh1i9Tt5HW7hAAABAEClcUNpTvL3zStId5Jidp8YTBqY6Mq6IfepAYpJCdvG2RW6
+6OjNxRRbUUrjHaF+kI16Zh22yN/qd0bQviy3Rj6D1aj/Hz3KpdEBEQ5bwDfynVBiyOucje
+Kghf0cwfU1L4mNv4jzRqAnYuYmoCvI8sBFZA1vk7MSB5zbtVMHqkrOhcLSbpFwqB9nY3q0
+hsNHWoMxAAtixoFRkWsRGgb1WlpKTZZnmTf9+fuJNWDf9PNEhRaI4TZO+qpC11nTuisa8U
+B9vwPpDxutUa+NBWJ6Y4TVvLwXpPPpzpDj23frG0nKyhqtkNro3G0H3kx6FLXdnYL2Vs6q
+EhZuKPDmmYDHjZEAAAEBAN8ffQ0uiK6FD9BkAH2gLr7zLS8DzNn6AwBJrlAXgBIv9PHGh7
+wgriu6LEX3Hz28B+ibqPw1Skn4kGAqZwhRY69otzmES+OW3s4+u6aOu7PnEGmSJciDB6iw
+vLKSdbNb4suXIjFjpxPsLCtWSZL1T/Z5LKX8J32bqztOFCZAm3m2TOcNbo3mrxawdWuP1j
+VS2oF86FGA9uECzpCRRcOIs5ZPjRv5tTArwk2YgmYfHf60wZMc4/Tnj9sh3n/jhKGXS342
+eCsGA59BycWT7QfNX2ExKwr4KN0yDI+Vxacp6n/ZveHlmE/1wVEaH1bAYqjwVTbqcpmtUo
+PQQydZ5tTciBkAAAEBAMq/h2NvVt1gyi1/oiuqfZ2v1vfUCktNWbINdE3EwUssXskmfyzG
+Zz9VAULqfIGbyG3hINqo8wmEGYq6WW4QG+xKsde51BDrXv1FBspJo2iAffcXMbxeWXPxk/
+DttYWnZ4EiiviH30H0bO2DNhcpgAARLu5uEdDuOFTayJhnoYsguEPk8Y96QVnoH5c2tH+c
+acSql5IoP8ZmJlQTmlQHyby3yPYxxRARxWGxlPC7q4nIUR7MY82IRGnghi84jTBubX31ux
+T/ML03KJb8aXEVTjKp5FPTjhMyXM+562ERkFzpU0LS8PzFCPcF7Qk9+p4UMnaJNdKVeRSQ
+cPHI3bkERbsAAAAhZ29qaW5hdWRpQHV3czgtd3BtLTEwMC5jYWZlMjQuY29tAQI=
+-----END OPENSSH PRIVATE KEY-----

+ 2 - 1
nuxt.config.ts

@@ -59,7 +59,8 @@ export default defineNuxtConfig({
   },
   runtimeConfig: {
     public: {
-      //apiBase: process.env.NUXT_PUBLIC_API_BASE
+      apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://gojinaudi.mycafe24.com',
+      imageBase: process.env.NUXT_PUBLIC_IMAGE_BASE || 'http://gojinaudi.mycafe24.com'
     }
   }
 })

+ 23 - 0
package-lock.json

@@ -12,11 +12,13 @@
         "@nuxt/ui": "^4.1.0",
         "axios": "^1.13.2",
         "eslint": "^9.38.0",
+        "flatpickr": "^4.6.13",
         "nuxt": "^4.1.3",
         "suneditor": "^2.47.8",
         "suneditor-react": "^3.6.1",
         "swiper": "^12.0.3",
         "vue": "^3.5.22",
+        "vue-flatpickr-component": "^12.0.0",
         "vue-router": "^4.6.3"
       },
       "devDependencies": {
@@ -6543,6 +6545,12 @@
         "node": ">=16"
       }
     },
+    "node_modules/flatpickr": {
+      "version": "4.6.13",
+      "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
+      "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==",
+      "license": "MIT"
+    },
     "node_modules/flatted": {
       "version": "3.3.3",
       "license": "ISC"
@@ -13507,6 +13515,21 @@
         "eslint": "^8.57.0 || ^9.0.0"
       }
     },
+    "node_modules/vue-flatpickr-component": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/vue-flatpickr-component/-/vue-flatpickr-component-12.0.0.tgz",
+      "integrity": "sha512-CJ5jrgTaeD66Z4mjEocSTAdB/n6IGSlUICwdBanpyCI8hswq5rwXvEYQ5IKA3K3uVjP5pBlY9Rg6o3xoszTPpA==",
+      "license": "MIT",
+      "dependencies": {
+        "flatpickr": "^4.6.13"
+      },
+      "engines": {
+        "node": ">=14.13.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.6.3",
       "license": "MIT",

+ 2 - 0
package.json

@@ -15,11 +15,13 @@
     "@nuxt/ui": "^4.1.0",
     "axios": "^1.13.2",
     "eslint": "^9.38.0",
+    "flatpickr": "^4.6.13",
     "nuxt": "^4.1.3",
     "suneditor": "^2.47.8",
     "suneditor-react": "^3.6.1",
     "swiper": "^12.0.3",
     "vue": "^3.5.22",
+    "vue-flatpickr-component": "^12.0.0",
     "vue-router": "^4.6.3"
   },
   "devDependencies": {

+ 36 - 0
public/.htaccess

@@ -0,0 +1,36 @@
+  # CORS Headers - 모든 요청에 대해 CORS 헤더 추가
+  <IfModule mod_headers.c>
+      Header always set Access-Control-Allow-Origin "*"
+      Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
+      Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, Accept"
+      Header always set Access-Control-Max-Age "3600"
+  </IfModule>
+
+  <IfModule mod_rewrite.c>
+      RewriteEngine On
+
+      # OPTIONS 요청 처리 (Preflight)
+      RewriteCond %{REQUEST_METHOD} OPTIONS
+      RewriteRule ^(.*)$ $1 [R=200,L]
+
+      # 1. /writable/uploads/item/thumb/ 경로는 rewrite 안 함
+      RewriteCond %{REQUEST_URI} ^/writable/uploads/item/thumb/
+      RewriteRule ^ - [L]
+
+      # 2. API 요청은 CodeIgniter index.php로
+      RewriteCond %{REQUEST_URI} ^/api
+      RewriteRule ^api/(.*)$ index.php/api/$1 [L]
+
+      # 3. 관리자 API 요청
+      RewriteCond %{REQUEST_URI} ^/admin/api
+      RewriteRule ^admin/api/(.*)$ index.php/admin/api/$1 [L]
+
+      # 4. Nuxt 정적 파일 (_nuxt, assets 등)
+      RewriteCond %{REQUEST_FILENAME} -f
+      RewriteRule ^ - [L]
+
+      # 5. 나머지 모든 요청은 Nuxt 200.html로 (SPA fallback)
+      RewriteCond %{REQUEST_FILENAME} !-f
+      RewriteCond %{REQUEST_FILENAME} !-d
+      RewriteRule ^.*$ /200.html [L]
+  </IfModule>