Parcourir la source

[선상관리] 목록, 등록, 삭제 기능 1차 완료 / 첨부파일 진행중

DESKTOP-T61HUSC\user il y a 1 mois
Parent
commit
9bb7c8791a

+ 51 - 20
app/assets/scss/admin.scss

@@ -1241,6 +1241,7 @@ footer {
 :root {
   --admin-yellow: #e8b546;
   --admin-red: #E85D3F;
+  --admin-blue: #3c80f2;
   --admin-bg-navy: #1a2332;
   --admin-bg-primary: rgba(246, 247, 248, 1);
   --admin-bg-secondary: #ffffff;
@@ -2043,6 +2044,13 @@ footer {
 
   .admin--form-select {
     cursor: pointer;
+    appearance: none;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    padding-right: 36px;
+    background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%23666B75' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+    background-repeat: no-repeat;
+    background-position: right 12px center;
   }
 
   // 다중 입력 필드
@@ -2496,35 +2504,29 @@ footer {
 // 페이지네이션
 .admin--pagination {
   display: flex;
-  justify-content: center;
+  justify-content: flex-end;
   align-items: center;
   gap: 8px;
   margin-top: 24px;
 
   .admin--pagination-btn {
-    min-width: 36px;
-    height: 36px;
-    padding: 0 12px;
-    background: var(--admin-bg-secondary);
-    border: 1px solid var(--admin-border-color);
+    min-width: 32px;
+    height: 32px;
+    max-width: 32px;
     border-radius: 6px;
-    color: var(--admin-text-secondary);
-    font-size: 14px;
-    font-weight: 500;
     cursor: pointer;
     transition: all 0.3s ease;
-    font-family: 'FORDKOREAType', sans-serif;
-
-    &:hover:not(:disabled) {
-      background: var(--admin-bg-tertiary);
-      border-color: var(--admin-accent-primary);
-      color: var(--admin-text-primary);
-    }
+    background-color: #fff;
+    border: 1px solid #e8eaef;
+    font-size: 11px;
+    font-weight: 400;
+    color: #666b75;
 
     &.is-active {
-      background: var(--admin-accent-primary);
-      border-color: var(--admin-accent-primary);
-      color: var(--admin-text-primary);
+      color: #fff;
+      font-weight: 700;
+      background-color: var(--admin-blue);
+      pointer-events: none;
     }
 
     &:disabled {
@@ -2715,6 +2717,13 @@ footer {
   font-style: italic;
 }
 
+// 폼 경고 (좌표 미검출 등)
+.admin--form-warning {
+  margin: 8px 0 0 0;
+  font-size: 13px;
+  color: var(--admin-red);
+}
+
 // 검색 박스 (큰 버전 - 여러 필터)
 .admin--search-box-large {
   .admin--search-filters {
@@ -7668,7 +7677,7 @@ footer {
       display: flex;
       align-items: center;
       gap: 12px;
-      input{
+      input, select{
         width: 320px;
         &[type=radio]{
           width: auto;
@@ -7679,11 +7688,33 @@ footer {
         &.w--120{
           width: 120px;
         }
+        &.w--160{
+          width: 160px;
+        }
+        &.w--200{
+          width: 200px;
+        }
+        &.w--240{
+          width: 240px;
+        }
+        &.w--300{
+          width: 300px;
+        }
       }
     }
   }
 }
 
+.admin--form-section-title{
+  font-size: 15px;
+  font-weight: 700;
+  color: #1a2b4a;
+  margin: 28px 0 12px;
+  &:first-child{
+    margin-top: 0;
+  }
+}
+
 .admin--info--box,
 .admin--inf--box{
   background-color: #fff8de;

+ 4 - 4
app/layouts/admin.vue

@@ -44,13 +44,13 @@
       children: [
         {
           title: "선상관리",
-          path: "/site-manager/fishing/list",
-          pattern: /^\/site-manager\/fishing\/list|create|edit/,
+          path: "/site-manager/onboard/list",
+          pattern: /^\/site-manager\/onboard\/(list|create|edit|detail)/,
         },
         {
           title: "낚시터관리",
-          path: "/site-manager/fishing/event",
-          pattern: /^\/site-manager\/fishing\/event/,
+          path: "/site-manager/fishing/list",
+          pattern: /^\/site-manager\/fishing\/(list|create|edit|detail)/,
         }
       ],
     },

+ 22 - 2
app/pages/site-manager/area/list.vue

@@ -65,12 +65,22 @@
 
     <!-- 페이지네이션 -->
     <div v-if="totalPages > 1" class="admin--pagination">
+      <button
+        v-if="totalPages > 2"
+        class="admin--pagination-btn"
+        :disabled="currentPage === 1"
+        @click="changePage(1)"
+        title="처음"
+      >
+        ◀◀
+      </button>
       <button
         class="admin--pagination-btn"
         :disabled="currentPage === 1"
         @click="changePage(currentPage - 1)"
+        title="이전"
       >
-        이전
+        
       </button>
       <button
         v-for="page in visiblePages"
@@ -85,8 +95,18 @@
         class="admin--pagination-btn"
         :disabled="currentPage === totalPages"
         @click="changePage(currentPage + 1)"
+        title="다음"
+      >
+        ▶
+      </button>
+      <button
+        v-if="totalPages > 2"
+        class="admin--pagination-btn"
+        :disabled="currentPage === totalPages"
+        @click="changePage(totalPages)"
+        title="끝"
       >
-        다음
+        ▶▶
       </button>
     </div>
   </div>

+ 23 - 3
app/pages/site-manager/field/list.vue

@@ -4,7 +4,7 @@
     <div class="admin--search-box">
       <div class="admin--search-form">
         <select v-model="filterStatus" @change="onSearch" class="admin--form-select admin--search-select">
-          <option value="">전체 상태</option>
+          <option value="">전체</option>
           <option value="Y">사용중</option>
           <option value="N">미사용</option>
         </select>
@@ -73,12 +73,22 @@
 
     <!-- 페이지네이션 -->
     <div v-if="totalPages > 1" class="admin--pagination">
+      <button
+        v-if="totalPages > 2"
+        class="admin--pagination-btn"
+        :disabled="currentPage === 1"
+        @click="changePage(1)"
+        title="처음"
+      >
+        ◀◀
+      </button>
       <button
         class="admin--pagination-btn"
         :disabled="currentPage === 1"
         @click="changePage(currentPage - 1)"
+        title="이전"
       >
-        이전
+        
       </button>
       <button
         v-for="page in visiblePages"
@@ -93,8 +103,18 @@
         class="admin--pagination-btn"
         :disabled="currentPage === totalPages"
         @click="changePage(currentPage + 1)"
+        title="다음"
+      >
+        ▶
+      </button>
+      <button
+        v-if="totalPages > 2"
+        class="admin--pagination-btn"
+        :disabled="currentPage === totalPages"
+        @click="changePage(totalPages)"
+        title="끝"
       >
-        다음
+        ▶▶
       </button>
     </div>
   </div>

+ 155 - 275
app/pages/site-manager/fishing/list.vue

@@ -1,12 +1,25 @@
 <template>
-  <div class="admin--showroom-list">
-    <!-- 상단 버튼 -->
+  <div class="admin--field-list">
+    <!-- 상단 검색/액션 영역 -->
     <div class="admin--search-box">
-      <div class="admin--search-form"></div>
+      <div class="admin--search-form">
+        <select v-model="filterStatus" @change="onSearch" class="admin--form-select admin--search-select">
+          <option value="">전체</option>
+          <option value="Y">사용중</option>
+          <option value="N">미사용</option>
+        </select>
+        <input
+          v-model="searchQuery"
+          type="text"
+          placeholder="선상명, 지역명으로 검색"
+          @keyup.enter="onSearch"
+          class="admin--form-input admin--search-input"
+        />
+        <button @click="onSearch" class="admin--btn-small admin--btn-small-primary">검색</button>
+        <button @click="resetSearch" class="admin--btn-small admin--btn-small-secondary">초기화</button>
+      </div>
       <div class="admin--search-actions">
-        <button class="admin--btn admin--btn-primary" @click="goToCreate">
-          + 전시장 등록
-        </button>
+        <button class="admin--btn-add" @click="goToCreate">+ 새 선상 추가</button>
       </div>
     </div>
 
@@ -15,55 +28,44 @@
       <table class="admin--table">
         <thead>
           <tr>
-            <th>NO</th>
-            <th>전시장명</th>
-            <th>소속명</th>
-            <th>대표번호</th>
-            <th>주소</th>
+            <th style="width: 80px;">번호</th>
+            <th style="width: 40%;">분야</th>
+            <th>지역명</th>
+            <th>선상명</th>
+            <th>제휴업체</th>
             <th>상태</th>
-            <th>관리</th>
+            <th>등록일</th>
+            <th style="width: 120px;">관리</th>
           </tr>
         </thead>
         <tbody>
           <tr v-if="isLoading">
-            <td colspan="7" class="admin--table-loading">
-              데이터를 불러오는 중...
-            </td>
+            <td colspan="6" class="admin--table-loading">데이터를 불러오는 중...</td>
           </tr>
-          <tr v-else-if="!showrooms || showrooms.length === 0">
-            <td colspan="7" class="admin--table-empty">
-              등록된 전시장이 없습니다.
-            </td>
+          <tr v-else-if="!fields || fields.length === 0">
+            <td colspan="6" class="admin--table-empty">등록된 선상이 없습니다.</td>
           </tr>
-          <tr v-else v-for="(showroom, index) in showrooms" :key="showroom.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td class="admin--table-title">{{ showroom.name }}</td>
-            <td>{{ showroom.branch_name || '-' }}</td>
-            <td>{{ showroom.main_phone }}</td>
-            <td>{{ showroom.address }}</td>
+          <tr
+            v-else
+            v-for="(field, index) in fields"
+            :key="field.id"
+            class="admin--table-row-clickable"
+            @click="goToDetail(field.id)"
+          >
+            <td class="date">{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
+            <td class="admin--table-title">{{ field.name }}</td>
+            <td class="color--yellow">{{ field.weight }}</td>
+            <td class="date">{{ formatDate(field.created_at) }}</td>
             <td>
-              <button
-                class="admin--toggle-btn"
-                :class="{ 'is-active': showroom.is_active == 1 }"
-                @click="toggleActive(showroom.id, showroom.is_active)"
-              >
-                {{ showroom.is_active == 1 ? '사용' : '비사용' }}
-              </button>
+              <span :class="['admin--badge', getStatusBadgeClass(field.status_YN)]">
+                {{ getStatusLabel(field.status_YN) }}
+              </span>
             </td>
             <td>
               <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(showroom.id)"
-                >
+                <button class="admin--btn-small admin--btn-blue" @click.stop="goToEdit(field.id)">
                   수정
                 </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="deleteShowroom(showroom.id)"
-                >
-                  삭제
-                </button>
               </div>
             </td>
           </tr>
@@ -97,241 +99,119 @@
         다음
       </button>
     </div>
-
-    <!-- 알림 모달 -->
-    <AdminAlertModal
-      v-if="alertModal.show"
-      :title="alertModal.title"
-      :message="alertModal.message"
-      :type="alertModal.type"
-      @confirm="handleAlertConfirm"
-      @cancel="handleAlertCancel"
-      @close="closeAlertModal"
-    />
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-import AdminAlertModal from '~/components/admin/AdminAlertModal.vue'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { get, del, post } = useApi()
-
-const isLoading = ref(false)
-const showrooms = ref([])
-const currentPage = ref(1)
-const perPage = ref(10)
-const totalCount = ref(0)
-const totalPages = ref(0)
-
-// 알림 모달
-const alertModal = ref({
-  show: false,
-  title: '알림',
-  message: '',
-  type: 'alert',
-  onConfirm: null
-})
-
-// 알림 모달 표시
-const showAlert = (message, title = '알림') => {
-  alertModal.value = {
-    show: true,
-    title,
-    message,
-    type: 'alert',
-    onConfirm: null
-  }
-}
-
-// 확인 모달 표시
-const showConfirm = (message, onConfirm, title = '확인') => {
-  alertModal.value = {
-    show: true,
-    title,
-    message,
-    type: 'confirm',
-    onConfirm
-  }
-}
-
-// 알림 모달 닫기
-const closeAlertModal = () => {
-  alertModal.value.show = false
-}
-
-// 알림 모달 확인
-const handleAlertConfirm = () => {
-  if (alertModal.value.onConfirm) {
-    alertModal.value.onConfirm()
-  }
-  closeAlertModal()
-}
-
-// 알림 모달 취소
-const handleAlertCancel = () => {
-  closeAlertModal()
-}
-
-// 보이는 페이지 번호 계산
-const visiblePages = computed(() => {
-  const pages = []
-  const maxVisible = 5
-  let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
-  let end = Math.min(totalPages.value, start + maxVisible - 1)
-
-  if (end - start < maxVisible - 1) {
-    start = Math.max(1, end - maxVisible + 1)
-  }
-
-  for (let i = start; i <= end; i++) {
-    pages.push(i)
-  }
-
-  return pages
-})
-
-// 데이터 로드
-const loadShowrooms = async () => {
-  isLoading.value = true
-
-  const params = {
-    page: currentPage.value,
-    per_page: perPage.value
-  }
-
-  const { data, error } = await get('/showroom/list', { params })
-
-  console.log('[ShowroomList] API 응답:', { data, error })
-
-  if (data?.success && data?.data) {
-    showrooms.value = data.data.items || []
-    totalCount.value = data.data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-    console.log('[ShowroomList] 로드 성공:', showrooms.value.length)
-  }
-
-  isLoading.value = false
-}
-
-// 페이지 변경
-const changePage = (page) => {
-  if (page < 1 || page > totalPages.value) return
-  currentPage.value = page
-  loadShowrooms()
-  window.scrollTo({ top: 0, behavior: 'smooth' })
-}
-
-// 전시장 등록 페이지로 이동
-const goToCreate = () => {
-  router.push('/site-manager/showroom/create')
-}
-
-// 전시장 수정 페이지로 이동
-const goToEdit = (id) => {
-  router.push(`/site-manager/showroom/edit/${id}`)
-}
-
-// 전시장 삭제
-const deleteShowroom = (id) => {
-  showConfirm(
-    '정말 삭제하시겠습니까?',
-    async () => {
-      const { data, error } = await del(`/showroom/${id}`)
-
-      if (error || !data?.success) {
-        showAlert(error?.message || data?.message || '삭제에 실패했습니다.', '오류')
-      } else {
-        showAlert(data.message || '전시장이 삭제되었습니다.', '성공')
-        loadShowrooms()
-      }
-    },
-    '전시장 삭제'
-  )
-}
-
-// 사용/비사용 토글
-const toggleActive = (id, currentStatus) => {
-  console.log('[toggleActive] 호출:', { id, currentStatus })
-  const statusText = currentStatus == 1 ? '비사용' : '사용'
-
-  showConfirm(
-    `전시장을 ${statusText} 상태로 변경하시겠습니까?`,
-    async () => {
-      console.log('[toggleActive] API 호출:', `/showroom/${id}/toggle-active`)
-
-      const { data, error } = await post(`/showroom/${id}/toggle-active`)
-
-      console.log('[toggleActive] API 응답:', { data, error })
-
-      if (error || !data?.success) {
-        console.error('[toggleActive] 에러 상세:', error)
-        showAlert(error?.message || data?.message || '상태 변경에 실패했습니다.', '오류')
-      } else {
-        showAlert(data.message || '전시장 상태가 변경되었습니다.', '성공')
-        loadShowrooms()
-      }
-    },
-    '상태 변경'
-  )
-}
-
-onMounted(() => {
-  loadShowrooms()
-})
+  import { ref, computed, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({
+    layout: "admin",
+    middleware: ["auth"],
+  });
+
+  const router = useRouter();
+  const { get } = useApi();
+
+  const isLoading = ref(false);
+  const fields = ref([]);
+  const currentPage = ref(1);
+  const perPage = ref(10);
+  const totalCount = ref(0);
+  const totalPages = ref(0);
+
+  const searchQuery = ref("");
+  const filterStatus = ref("");
+
+  // 보이는 페이지 번호 계산
+  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 loadFields = async () => {
+    isLoading.value = true;
+
+    const params = {
+      page: currentPage.value,
+      per_page: perPage.value,
+    };
+    if (searchQuery.value) params.search = searchQuery.value;
+    if (filterStatus.value) params.status = filterStatus.value;
+
+    const { data, error } = await get("/field/list", { params });
+
+    if (error) {
+      console.error("[FieldList] 목록 로드 실패:", error);
+      fields.value = [];
+      totalCount.value = 0;
+      totalPages.value = 0;
+    } else if (data?.success && data?.data) {
+      fields.value = data.data.items || [];
+      totalCount.value = data.data.total || 0;
+      totalPages.value = data.data.total_pages || 0;
+    }
+
+    isLoading.value = false;
+  };
+
+  // 검색
+  const onSearch = () => {
+    currentPage.value = 1;
+    loadFields();
+  };
+
+  // 검색 초기화
+  const resetSearch = () => {
+    searchQuery.value = "";
+    filterStatus.value = "";
+    currentPage.value = 1;
+    loadFields();
+  };
+
+  // 페이지 변경
+  const changePage = (page) => {
+    if (page < 1 || page > totalPages.value) return;
+    currentPage.value = page;
+    loadFields();
+    window.scrollTo({ top: 0, behavior: "smooth" });
+  };
+
+  // 이동
+  const goToCreate = () => router.push("/site-manager/field/create");
+  const goToDetail = (id) => router.push(`/site-manager/field/detail/${id}`);
+  const goToEdit = (id) => router.push(`/site-manager/field/edit/${id}`);
+
+  // 상태 라벨 / 뱃지 클래스
+  const getStatusLabel = (status) => (status === "Y" ? "사용중" : "미사용");
+  const getStatusBadgeClass = (status) =>
+    status === "Y" ? "admin--badge-active" : "admin--badge-ended";
+
+  // 날짜 포맷
+  const formatDate = (dateString) => {
+    if (!dateString) return "-";
+    const date = new Date(dateString.replace(" ", "T"));
+    if (isNaN(date.getTime())) return dateString;
+    return date.toLocaleDateString("ko-KR", {
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+    });
+  };
+
+  onMounted(() => {
+    loadFields();
+  });
 </script>
-
-<style scoped>
-.admin--search-actions .admin--btn-primary {
-  background: var(--admin-accent-primary);
-  color: white;
-  border-color: var(--admin-accent-primary);
-  font-weight: 500;
-  padding: 8px 18px;
-  font-size: 13px;
-  border-radius: 8px;
-  transition: all 0.3s ease;
-}
-
-.admin--search-actions .admin--btn-primary:hover {
-  background: var(--admin-accent-hover);
-  border-color: var(--admin-accent-hover);
-  transform: translateY(-1px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-.admin--toggle-btn {
-  padding: 6px 16px;
-  font-size: 12px;
-  border-radius: 20px;
-  border: 1px solid #ddd;
-  background: #f5f5f5;
-  color: #666;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  font-weight: 500;
-}
-
-.admin--toggle-btn:hover {
-  border-color: #bbb;
-  background: #e8e8e8;
-}
-
-.admin--toggle-btn.is-active {
-  background: var(--admin-accent-primary);
-  color: white;
-  border-color: var(--admin-accent-primary);
-}
-
-.admin--toggle-btn.is-active:hover {
-  background: var(--admin-accent-hover);
-  border-color: var(--admin-accent-hover);
-}
-</style>

+ 376 - 0
app/pages/site-manager/onboard/create.vue

@@ -0,0 +1,376 @@
+<template>
+  <div class="admin--page-content">
+    <div class="admin--form">
+      <form @submit.prevent="handleSubmit">
+        <table class="admin--form--table">
+          <colgroup>
+            <col style="width: 140px;">
+            <col>
+          </colgroup>
+          <tbody>
+            <tr>
+              <th><div>분야 <span class="admin--required">*</span></div></th>
+              <td>
+                <div class="input--wrap">
+                  <select v-model="formData.field_id" class="admin--form-select w--240" required>
+                    <option value="">선택하세요</option>
+                    <option v-for="f in fieldOptions" :key="f.id" :value="f.id">{{ f.name }}</option>
+                  </select>
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>지역 <span class="admin--required">*</span></div></th>
+              <td>
+                <div class="input--wrap">
+                  <select v-model="formData.area_id" class="admin--form-select w--240" required>
+                    <option value="">선택하세요</option>
+                    <option v-for="a in areaOptions" :key="a.id" :value="a.id">{{ a.name }}</option>
+                  </select>
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>선상명 <span class="admin--required">*</span></div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.name" type="text" class="admin--form-input" placeholder="예: 파이럿호" required />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>낚시지역</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.area_detail" type="text" class="admin--form-input" placeholder="예: 모슬포항" />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>중량(톤수)</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.tonnage" type="text" class="admin--form-input w--240" placeholder="예: 9.77톤" />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>탑승인원</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.capacity" type="text" class="admin--form-input w--240" placeholder="예: 20인승, 낚시 전용선" />
+                </div>
+              </td>
+            </tr>  
+            <tr>
+              <th><div>우편번호</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.zip_code" type="text" class="admin--form-input w--160" placeholder="우편번호" readonly />
+                  <button type="button" class="admin--btn-small admin--btn-blue" @click="openPostcode">
+                    우편번호 검색
+                  </button>
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>주소</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.address" type="text" class="admin--form-input w--full" placeholder="우편번호 검색 시 자동 입력" readonly />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>상세주소</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.address_detail" type="text" class="admin--form-input w--full" placeholder="" />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>참고항목</div></th>
+              <td>
+                <div class="input--wrap">
+                  <input v-model="formData.address_refer" type="text" class="admin--form-input w--full" placeholder="예: 선착장 입구 우측" />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th><div>좌표</div></th>
+              <td>
+                <div class="input--wrap admin--inline-group">
+                  <input v-model="formData.lat" type="text" class="admin--form-input w--200" placeholder="위도 (lat)" />
+                  <input v-model="formData.lng" type="text" class="admin--form-input w--200" placeholder="경도 (lng)" />
+                </div>
+                <p v-if="coordError" class="">{{ coordError }}</p>
+                <p v-else class="mt--10">주소를 검색하면 위도·경도가 자동 입력됩니다.</p>
+              </td>
+            </tr>
+            <tr>
+              <th><div>제휴 여부</div></th>
+              <td>
+                <div class="input--wrap">
+                  <label class="admin--radio-label">
+                    <input type="radio" v-model="formData.partnership_YN" value="Y" /> 제휴
+                  </label>
+                  <label class="admin--radio-label ml--16">
+                    <input type="radio" v-model="formData.partnership_YN" value="N" /> 비제휴
+                  </label>
+                </div>
+              </td>
+            </tr>
+            <tr v-if="isPartner">
+              <th><div>계좌번호</div></th>
+              <td>
+                <div class="input--wrap">
+                  <select v-model="formData.bank_code" class="admin--form-select w--120">
+                    <option value="">은행명</option>
+                    <option v-for="b in bankOptions" :key="b.code" :value="b.code">{{ b.name }}</option>
+                  </select>
+                  <input v-model="formData.account_number" type="text" class="admin--form-input w--300" placeholder="'-' 없이 숫자만 입력" />
+                  <input v-model="formData.account_holder" type="text" class="admin--form-input w--240" placeholder="예금주명" />
+                </div>
+                <p class="mt--10">정산 입금 계좌 (제휴 업체용)</p>
+              </td>
+            </tr>
+            <tr>
+              <th><div>상태</div></th>
+              <td>
+                <div class="input--wrap">
+                  <label class="admin--radio-label">
+                    <input type="radio" v-model="formData.status_YN" value="Y" /> 사용중
+                  </label>
+                  <label class="admin--radio-label ml--16">
+                    <input type="radio" v-model="formData.status_YN" value="N" /> 미사용
+                  </label>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+
+        <!-- 버튼 영역 -->
+        <div class="admin--form-actions">
+          <button type="button" class="admin--btn" @click="goToList">
+            ← 목록으로
+          </button>
+          <button type="submit" class="admin--btn admin--btn-red ml--auto" :disabled="isSaving">
+            {{ isSaving ? "저장 중..." : "저장" }}
+          </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>
+  </div>
+</template>
+
+<script setup>
+  import { ref, computed, watch, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({
+    layout: "admin",
+    middleware: ["auth"],
+  });
+
+  const router = useRouter();
+  const config = useRuntimeConfig();
+  const { get, post } = useApi();
+
+  const isSaving = ref(false);
+  const successMessage = ref("");
+  const errorMessage = ref("");
+  const coordError = ref("");
+
+  // 분야 / 지역 select 옵션
+  const fieldOptions = ref([]);
+  const areaOptions = ref([]);
+
+  // 주요 은행 목록 (금융결제원 표준 코드)
+  const bankOptions = [
+    { code: "002", name: "산업은행" },
+    { code: "003", name: "기업은행" },
+    { code: "004", name: "국민은행" },
+    { code: "007", name: "수협은행" },
+    { code: "011", name: "농협은행" },
+    { code: "020", name: "우리은행" },
+    { code: "023", name: "SC제일은행" },
+    { code: "031", name: "대구은행" },
+    { code: "032", name: "부산은행" },
+    { code: "034", name: "광주은행" },
+    { code: "035", name: "제주은행" },
+    { code: "037", name: "전북은행" },
+    { code: "039", name: "경남은행" },
+    { code: "045", name: "새마을금고" },
+    { code: "071", name: "우체국" },
+    { code: "081", name: "하나은행" },
+    { code: "088", name: "신한은행" },
+    { code: "089", name: "케이뱅크" },
+    { code: "090", name: "카카오뱅크" },
+    { code: "092", name: "토스뱅크" },
+  ];
+
+  const formData = ref({
+    field_id: "",
+    area_id: "",
+    name: "",
+    area_detail: "",
+    tonnage: "",
+    capacity: "",
+    zip_code: "",
+    address: "",
+    address_detail: "",
+    address_refer: "",
+    lat: "",
+    lng: "",
+    bank_code: "",
+    account_number: "",
+    account_holder: "",
+    partnership_YN: "N",
+    status_YN: "Y",
+  });
+
+  // 제휴 여부 — 비제휴면 계좌 입력 비활성화
+  const isPartner = computed(() => formData.value.partnership_YN === "Y");
+
+  // 비제휴로 전환 시 입력했던 계좌 정보 초기화
+  watch(
+    () => formData.value.partnership_YN,
+    (val) => {
+      if (val === "N") {
+        formData.value.bank_code = "";
+        formData.value.account_number = "";
+        formData.value.account_holder = "";
+      }
+    }
+  );
+
+  // 분야 / 지역 옵션 로드
+  const loadOptions = async () => {
+    const [fieldRes, areaRes] = await Promise.all([
+      get("/field/list", { params: { per_page: 1000 } }),
+      get("/area/list", { params: { per_page: 1000 } }),
+    ]);
+
+    // API는 id DESC(최신순)로 주므로 뒤집어서 먼저 등록한 순(수도권 등)이 위로 오게
+    if (fieldRes.data?.success) fieldOptions.value = (fieldRes.data.data.items || []).reverse();
+    if (areaRes.data?.success) areaOptions.value = (areaRes.data.data.items || []).reverse();
+  };
+
+  // 외부 스크립트 동적 로드
+  const loadScript = (src) =>
+    new Promise((resolve, reject) => {
+      if (document.querySelector(`script[src="${src}"]`)) {
+        resolve();
+        return;
+      }
+      const s = document.createElement("script");
+      s.src = src;
+      s.onload = () => resolve();
+      s.onerror = () => reject(new Error(`스크립트 로드 실패: ${src}`));
+      document.head.appendChild(s);
+    });
+
+  // 주소 → 위도/경도 변환 (Google Geocoding API)
+  const searchCoords = async (address) => {
+    coordError.value = "";
+
+    const key = config.public.googleMapKey;
+    if (!key) {
+      coordError.value = "좌표를 자동으로 가져올 수 없습니다. 위도/경도를 직접 입력하세요.";
+      return;
+    }
+
+    try {
+      const url = new URL("https://maps.googleapis.com/maps/api/geocode/json");
+      url.searchParams.set("address", address);
+      url.searchParams.set("key", key);
+      url.searchParams.set("language", "ko");
+      url.searchParams.set("region", "kr");
+
+      const res = await fetch(url);
+      const data = await res.json();
+
+      if (data.status === "OK" && data.results?.[0]) {
+        const loc = data.results[0].geometry.location;
+        formData.value.lat = String(loc.lat);
+        formData.value.lng = String(loc.lng);
+      } else {
+        formData.value.lat = "";
+        formData.value.lng = "";
+        coordError.value = "좌표를 찾지 못했습니다. 직접 입력해 주세요.";
+      }
+    } catch (e) {
+      console.error("Geocoding error:", e);
+      formData.value.lat = "";
+      formData.value.lng = "";
+      coordError.value = "좌표 조회 중 오류가 발생했습니다. 위도/경도를 직접 입력해주세요.";
+    }
+  };
+
+  // 우편번호 검색 (Daum Postcode)
+  const openPostcode = async () => {
+    coordError.value = "";
+    try {
+      await loadScript("https://t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js");
+    } catch (e) {
+      errorMessage.value = "우편번호 서비스를 불러오지 못했습니다.";
+      return;
+    }
+
+    new window.daum.Postcode({
+      oncomplete: (data) => {
+        formData.value.zip_code = data.zonecode;
+        formData.value.address = data.roadAddress || data.jibunAddress;
+        // 선택한 주소로 좌표 자동 조회
+        searchCoords(formData.value.address);
+      },
+    }).open();
+  };
+
+  // 폼 제출
+  const handleSubmit = async () => {
+    successMessage.value = "";
+    errorMessage.value = "";
+
+    // 필수값 검증
+    if (!formData.value.field_id) return (errorMessage.value = "분야를 선택하세요.");
+    if (!formData.value.area_id) return (errorMessage.value = "지역을 선택하세요.");
+    if (!formData.value.name.trim()) return (errorMessage.value = "선상명을 입력하세요.");
+
+    isSaving.value = true;
+    try {
+      const { data, error } = await post("/onboard", { ...formData.value });
+
+      if (error || !data?.success) {
+        errorMessage.value = error?.message || data?.message || "등록에 실패했습니다.";
+      } else {
+        successMessage.value = data.message || "선상이 등록되었습니다.";
+        setTimeout(() => {
+          router.push("/site-manager/onboard/list");
+        }, 1000);
+      }
+    } catch (e) {
+      errorMessage.value = "서버 오류가 발생했습니다.";
+      console.error("Save error:", e);
+    } finally {
+      isSaving.value = false;
+    }
+  };
+
+  // 목록으로 이동
+  const goToList = () => router.push("/site-manager/onboard/list");
+
+  onMounted(() => {
+    loadOptions();
+  });
+</script>

+ 261 - 0
app/pages/site-manager/onboard/detail/[id].vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="admin--page-content">
+    <div class="admin--form">
+      <table class="admin--form--table">
+        <colgroup>
+          <col style="width: 140px;">
+          <col>
+        </colgroup>
+        <tbody>
+          <tr>
+            <th><div>분야</div></th>
+            <td>{{ data.field_name || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>지역</div></th>
+            <td>{{ data.area_name || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>선상명</div></th>
+            <td class="admin--table-title">{{ data.name || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>낚시지역</div></th>
+            <td>{{ data.area_detail || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>중량(톤수)</div></th>
+            <td>{{ data.tonnage || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>탑승인원</div></th>
+            <td>{{ data.capacity || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>우편번호</div></th>
+            <td>{{ data.zip_code || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>주소</div></th>
+            <td>{{ data.address || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>상세주소</div></th>
+            <td>{{ data.address_detail || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>참고항목</div></th>
+            <td>{{ data.address_refer || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>좌표</div></th>
+            <td>
+              <template v-if="data.lat && data.lng">{{ data.lat }}, {{ data.lng }}</template>
+              <template v-else>-</template>
+            </td>
+          </tr>
+          <tr>
+            <th><div>제휴 여부</div></th>
+            <td>
+              <span :class="['admin--badge', data.partnership_YN === 'Y' ? 'admin--badge-active' : 'admin--badge-ended']">
+                {{ data.partnership_YN === "Y" ? "제휴" : "비제휴" }}
+              </span>
+            </td>
+          </tr>
+          <tr v-if="data.partnership_YN === 'Y'">
+            <th><div>계좌번호</div></th>
+            <td>
+              <template v-if="data.account_number">
+                {{ bankName(data.bank_code) }} {{ data.account_number }}<template v-if="data.account_holder"> ({{ data.account_holder }})</template>
+              </template>
+              <template v-else>-</template>
+            </td>
+          </tr>
+          <tr>
+            <th><div>상태</div></th>
+            <td>
+              <span :class="['admin--badge', data.status_YN === 'Y' ? 'admin--badge-active' : 'admin--badge-ended']">
+                {{ data.status_YN === "Y" ? "사용중" : "미사용" }}
+              </span>
+            </td>
+          </tr>
+          <tr>
+            <th><div>등록일</div></th>
+            <td>{{ formatDateTime(data.created_at) }}</td>
+          </tr>
+          <tr>
+            <th><div>최근 수정</div></th>
+            <td>{{ formatDateTime(data.updated_at) }}</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <!-- 버튼 영역 -->
+      <div class="admin--form-actions">
+        <button type="button" class="admin--btn" @click="goToList">
+          ← 목록으로
+        </button>
+        <button type="button" class="admin--btn admin--btn-red-border ml--auto" @click="handleDelete">
+          삭제
+        </button>
+        <button type="button" class="admin--btn admin--btn-red" @click="goToEdit">
+          수정
+        </button>
+      </div>
+
+      <!-- 알림 모달 -->
+      <AdminAlertModal
+        v-if="alertModal.show"
+        :title="alertModal.title"
+        :message="alertModal.message"
+        :type="alertModal.type"
+        @confirm="handleAlertConfirm"
+        @cancel="handleAlertCancel"
+        @close="closeAlertModal"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, onMounted } from "vue";
+  import { useRoute, useRouter } from "vue-router";
+  import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
+
+  definePageMeta({
+    layout: "admin",
+    middleware: ["auth"],
+  });
+
+  const route = useRoute();
+  const router = useRouter();
+  const { get, del } = useApi();
+
+  const onboardId = route.params.id;
+
+  const data = ref({
+    field_name: "",
+    area_name: "",
+    name: "",
+    area_detail: "",
+    tonnage: "",
+    capacity: "",
+    zip_code: "",
+    address: "",
+    address_detail: "",
+    address_refer: "",
+    lat: "",
+    lng: "",
+    bank_code: "",
+    account_number: "",
+    account_holder: "",
+    partnership_YN: "N",
+    status_YN: "Y",
+    created_at: "",
+    updated_at: "",
+  });
+
+  // 은행 코드 → 은행명
+  const bankMap = {
+    "002": "산업은행", "003": "기업은행", "004": "국민은행", "007": "수협은행",
+    "011": "농협은행", "020": "우리은행", "023": "SC제일은행", "031": "대구은행",
+    "032": "부산은행", "034": "광주은행", "035": "제주은행", "037": "전북은행",
+    "039": "경남은행", "045": "새마을금고", "071": "우체국", "081": "하나은행",
+    "088": "신한은행", "089": "케이뱅크", "090": "카카오뱅크", "092": "토스뱅크",
+  };
+  const bankName = (code) => bankMap[code] || (code ? code : "-");
+
+  // 알림 모달
+  const alertModal = ref({
+    show: false,
+    title: "알림",
+    message: "",
+    type: "alert",
+    onConfirm: null,
+  });
+
+  const showAlert = (message, title = "알림") => {
+    alertModal.value = { show: true, title, message, type: "alert", onConfirm: null };
+  };
+  const showConfirm = (message, onConfirm, title = "확인") => {
+    alertModal.value = { show: true, title, message, type: "confirm", onConfirm };
+  };
+  const closeAlertModal = () => { alertModal.value.show = false; };
+  const handleAlertConfirm = () => {
+    if (alertModal.value.onConfirm) alertModal.value.onConfirm();
+    closeAlertModal();
+  };
+  const handleAlertCancel = () => closeAlertModal();
+
+  // 상세 조회
+  const loadDetail = async () => {
+    const { data: res, error } = await get(`/onboard/${onboardId}`);
+
+    if (error || !res?.success) {
+      showAlert(error?.message || res?.message || "조회에 실패했습니다.", "오류");
+      return;
+    }
+
+    const row = res.data || {};
+    data.value = {
+      field_name: row.field_name ?? "",
+      area_name: row.area_name ?? "",
+      name: row.name ?? "",
+      area_detail: row.area_detail ?? "",
+      tonnage: row.tonnage ?? "",
+      capacity: row.capacity ?? "",
+      zip_code: row.zip_code ?? "",
+      address: row.address ?? "",
+      address_detail: row.address_detail ?? "",
+      address_refer: row.address_refer ?? "",
+      lat: row.lat ?? "",
+      lng: row.lng ?? "",
+      bank_code: row.bank_code ?? "",
+      account_number: row.account_number ?? "",
+      account_holder: row.account_holder ?? "",
+      partnership_YN: row.partnership_YN ?? "N",
+      status_YN: row.status_YN ?? "Y",
+      created_at: row.created_at ?? "",
+      updated_at: row.updated_at ?? "",
+    };
+  };
+
+  // 삭제
+  const handleDelete = () => {
+    showConfirm(
+      `'${data.value.name}' 선상을 삭제하시겠습니까?`,
+      async () => {
+        const { data: res, error } = await del(`/onboard/${onboardId}`);
+        if (error || !res?.success) {
+          showAlert(error?.message || res?.message || "삭제에 실패했습니다.", "오류");
+        } else {
+          showAlert(res.message || "삭제되었습니다.", "성공");
+          setTimeout(() => router.push("/site-manager/onboard/list"), 800);
+        }
+      },
+      "선상 삭제"
+    );
+  };
+
+  // 이동
+  const goToList = () => router.push("/site-manager/onboard/list");
+  const goToEdit = () => router.push(`/site-manager/onboard/edit/${onboardId}`);
+
+  // 일시 포맷
+  const formatDateTime = (dateString) => {
+    if (!dateString) return "-";
+    const date = new Date(dateString.replace(" ", "T"));
+    if (isNaN(date.getTime())) return dateString;
+    return date.toLocaleString("ko-KR", {
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+      hour: "2-digit",
+      minute: "2-digit",
+    });
+  };
+
+  onMounted(() => {
+    loadDetail();
+  });
+</script>

+ 337 - 0
app/pages/site-manager/onboard/edit/[id].vue

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

+ 243 - 0
app/pages/site-manager/onboard/list.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="admin--field-list">
+    <!-- 상단 검색/액션 영역 -->
+    <div class="admin--search-box">
+      <div class="admin--search-form">
+        <select v-model="filterStatus" @change="onSearch" class="admin--form-select admin--search-select">
+          <option value="">전체</option>
+          <option value="Y">사용중</option>
+          <option value="N">미사용</option>
+        </select>
+        <input
+          v-model="searchQuery"
+          type="text"
+          placeholder="선상명, 지역명으로 검색"
+          @keyup.enter="onSearch"
+          class="admin--form-input admin--search-input"
+        />
+        <button @click="onSearch" class="admin--btn-small admin--btn-small-primary">검색</button>
+        <button @click="resetSearch" class="admin--btn-small admin--btn-small-secondary">초기화</button>
+      </div>
+      <div class="admin--search-actions">
+        <button class="admin--btn-add" @click="goToCreate">+ 새 선상 추가</button>
+      </div>
+    </div>
+
+    <!-- 테이블 -->
+    <div class="admin--table-wrapper">
+      <table class="admin--table">
+        <thead>
+          <tr>
+            <th style="width: 80px;">번호</th>
+            <th style="width: 140px;">분야</th>
+            <th style="width: 140px;">지역명</th>
+            <th>선상명</th>
+            <th style="width: 100px;">제휴업체</th>
+            <th style="width: 100px;">상태</th>
+            <th style="width: 120px;">등록일</th>
+            <th style="width: 120px;">관리</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-if="isLoading">
+            <td colspan="8" class="admin--table-loading">데이터를 불러오는 중...</td>
+          </tr>
+          <tr v-else-if="!onboards || onboards.length === 0">
+            <td colspan="8" class="admin--table-empty">등록된 선상이 없습니다.</td>
+          </tr>
+          <tr
+            v-else
+            v-for="(item, index) in onboards"
+            :key="item.id"
+            class="admin--table-row-clickable"
+            @click="goToDetail(item.id)"
+          >
+            <td class="date">{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
+            <td>{{ item.field_name || "-" }}</td>
+            <td>{{ item.area_name || "-" }}</td>
+            <td class="admin--table-title">{{ item.name }}</td>
+            <td>
+              <span :class="['admin--badge', item.partnership_YN === 'Y' ? 'admin--badge-active' : 'admin--badge-ended']">
+                {{ item.partnership_YN === "Y" ? "제휴" : "비제휴" }}
+              </span>
+            </td>
+            <td>
+              <span :class="['admin--badge', getStatusBadgeClass(item.status_YN)]">
+                {{ getStatusLabel(item.status_YN) }}
+              </span>
+            </td>
+            <td class="date">{{ formatDate(item.created_at) }}</td>
+            <td>
+              <div class="admin--table-actions">
+                <button class="admin--btn-small admin--btn-blue" @click.stop="goToEdit(item.id)">
+                  수정
+                </button>
+              </div>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+
+    <!-- 페이지네이션 -->
+    <div v-if="totalPages > 1" class="admin--pagination">
+      <button
+        v-if="totalPages > 2"
+        class="admin--pagination-btn"
+        :disabled="currentPage === 1"
+        @click="changePage(1)"
+        title="처음"
+      >
+        ◀◀
+      </button>
+      <button
+        class="admin--pagination-btn"
+        :disabled="currentPage === 1"
+        @click="changePage(currentPage - 1)"
+        title="이전"
+      >
+        ◀
+      </button>
+      <button
+        v-for="page in visiblePages"
+        :key="page"
+        class="admin--pagination-btn"
+        :class="{ 'is-active': page === currentPage }"
+        @click="changePage(page)"
+      >
+        {{ page }}
+      </button>
+      <button
+        class="admin--pagination-btn"
+        :disabled="currentPage === totalPages"
+        @click="changePage(currentPage + 1)"
+        title="다음"
+      >
+        ▶
+      </button>
+      <button
+        v-if="totalPages > 2"
+        class="admin--pagination-btn"
+        :disabled="currentPage === totalPages"
+        @click="changePage(totalPages)"
+        title="끝"
+      >
+        ▶▶
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { ref, computed, onMounted } from "vue";
+  import { useRouter } from "vue-router";
+
+  definePageMeta({
+    layout: "admin",
+    middleware: ["auth"],
+  });
+
+  const router = useRouter();
+  const { get } = useApi();
+
+  const isLoading = ref(false);
+  const onboards = ref([]);
+  const currentPage = ref(1);
+  const perPage = ref(10);
+  const totalCount = ref(0);
+  const totalPages = ref(0);
+
+  const searchQuery = ref("");
+  const filterStatus = ref("");
+
+  // 보이는 페이지 번호 계산
+  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 loadOnboards = async () => {
+    isLoading.value = true;
+
+    const params = {
+      page: currentPage.value,
+      per_page: perPage.value,
+    };
+    if (searchQuery.value) params.search = searchQuery.value;
+    if (filterStatus.value) params.status = filterStatus.value;
+
+    const { data, error } = await get("/onboard/list", { params });
+
+    if (error) {
+      console.error("[OnboardList] 목록 로드 실패:", error);
+      onboards.value = [];
+      totalCount.value = 0;
+      totalPages.value = 0;
+    } else if (data?.success && data?.data) {
+      onboards.value = data.data.items || [];
+      totalCount.value = data.data.total || 0;
+      totalPages.value = data.data.total_pages || 0;
+    }
+
+    isLoading.value = false;
+  };
+
+  // 검색
+  const onSearch = () => {
+    currentPage.value = 1;
+    loadOnboards();
+  };
+
+  // 검색 초기화
+  const resetSearch = () => {
+    searchQuery.value = "";
+    filterStatus.value = "";
+    currentPage.value = 1;
+    loadOnboards();
+  };
+
+  // 페이지 변경
+  const changePage = (page) => {
+    if (page < 1 || page > totalPages.value) return;
+    currentPage.value = page;
+    loadOnboards();
+    window.scrollTo({ top: 0, behavior: "smooth" });
+  };
+
+  // 이동
+  const goToCreate = () => router.push("/site-manager/onboard/create");
+  const goToDetail = (id) => router.push(`/site-manager/onboard/detail/${id}`);
+  const goToEdit = (id) => router.push(`/site-manager/onboard/edit/${id}`);
+
+  // 상태 라벨 / 뱃지 클래스
+  const getStatusLabel = (status) => (status === "Y" ? "사용중" : "미사용");
+  const getStatusBadgeClass = (status) =>
+    status === "Y" ? "admin--badge-active" : "admin--badge-ended";
+
+  // 날짜 포맷
+  const formatDate = (dateString) => {
+    if (!dateString) return "-";
+    const date = new Date(dateString.replace(" ", "T"));
+    if (isNaN(date.getTime())) return dateString;
+    return date.toLocaleDateString("ko-KR", {
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+    });
+  };
+
+  onMounted(() => {
+    loadOnboards();
+  });
+</script>

+ 6 - 0
backend/app/Config/Routes.php

@@ -43,6 +43,12 @@ $routes->post('api/area', 'Api\FishingAreaController::create');
 $routes->put('api/area/(:num)', 'Api\FishingAreaController::update/$1');
 $routes->delete('api/area/(:num)', 'Api\FishingAreaController::delete/$1');
 
+// Onboard (선상)
+$routes->get('api/onboard/list', 'Api\OnboardController::index');
+$routes->get('api/onboard/(:num)', 'Api\OnboardController::show/$1');
+$routes->post('api/onboard', 'Api\OnboardController::create');
+$routes->delete('api/onboard/(:num)', 'Api\OnboardController::delete/$1');
+
 // File Upload
 $routes->post('api/upload/file', 'Api\UploadController::uploadFile');
 $routes->post('api/upload/image', 'Api\UploadController::uploadImage');

+ 281 - 0
backend/app/Controllers/Api/OnboardController.php

@@ -0,0 +1,281 @@
+<?php
+
+namespace App\Controllers\Api;
+
+use CodeIgniter\HTTP\ResponseInterface;
+
+class OnboardController extends BaseApiController
+{
+    protected $format = 'json';
+    protected $table = 'onboard';
+
+    /**
+     * 선상 목록
+     * GET /api/onboard/list
+     */
+    public function index()
+    {
+        $auth = $this->requireAuth();
+        if ($auth instanceof ResponseInterface) {
+            return $auth;
+        }
+
+        try {
+            $page = (int) ($this->request->getGet('page') ?? 1);
+            $perPage = (int) ($this->request->getGet('per_page') ?? 10);
+            if ($page < 1) $page = 1;
+            if ($perPage < 1) $perPage = 10;
+            $offset = ($page - 1) * $perPage;
+
+            $search = trim((string) $this->request->getGet('search'));
+            $status = trim((string) $this->request->getGet('status'));
+
+            $db = $this->getDB();
+            $builder = $db->table($this->table . ' o');
+            $builder->join('fishing_field f', 'f.id = o.field_id', 'left');
+            $builder->join('fishing_area a', 'a.id = o.area_id', 'left');
+            $builder->where('o.deleted_YN', 'N');
+
+            if ($search !== '') {
+                $builder->groupStart()
+                    ->like('o.name', $search)
+                    ->orLike('a.name', $search)
+                    ->groupEnd();
+            }
+            if ($status === 'Y' || $status === 'N') {
+                $builder->where('o.status_YN', $status);
+            }
+
+            $total = $builder->countAllResults(false);
+
+            // 계좌번호는 목록에서 제외 (민감정보)
+            $items = $builder
+                ->select('o.id, o.name, o.field_id, o.area_id, o.area_detail, o.partnership_YN, o.status_YN, o.created_at, f.name as field_name, a.name as area_name')
+                ->orderBy('o.id', 'DESC')
+                ->limit($perPage, $offset)
+                ->get()
+                ->getResult();
+
+            return $this->respondSuccess([
+                'items'       => $items,
+                'total'       => $total,
+                'page'        => $page,
+                'per_page'    => $perPage,
+                'total_pages' => (int) ceil($total / $perPage),
+            ]);
+        } catch (\Exception $e) {
+            log_message('error', 'OnboardController index error: ' . $e->getMessage());
+            return $this->respondError('목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 선상 등록
+     * POST /api/onboard
+     */
+    public function create()
+    {
+        $auth = $this->requireAuth();
+        if ($auth instanceof ResponseInterface) {
+            return $auth;
+        }
+
+        try {
+            $payload = $this->request->getJSON(true);
+            if (!is_array($payload) || empty($payload)) {
+                $payload = $this->request->getPost() ?? [];
+            }
+
+            $fieldId = (int) ($payload['field_id'] ?? 0);
+            $areaId  = (int) ($payload['area_id'] ?? 0);
+            $name    = trim((string) ($payload['name'] ?? ''));
+
+            // 필수값 검증
+            if ($fieldId <= 0) {
+                return $this->respondError('분야를 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+            if ($areaId <= 0) {
+                return $this->respondError('지역을 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+            if ($name === '') {
+                return $this->respondError('선상명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+            if (mb_strlen($name) > 100) {
+                return $this->respondError('선상명은 100자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $db = $this->getDB();
+
+            // 분야 / 지역 존재 확인
+            $fieldExists = $db->table('fishing_field')
+                ->where('id', $fieldId)->where('deleted_YN', 'N')->countAllResults();
+            if ($fieldExists === 0) {
+                return $this->respondError('존재하지 않는 분야입니다.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $areaExists = $db->table('fishing_area')
+                ->where('id', $areaId)->where('deleted_YN', 'N')->countAllResults();
+            if ($areaExists === 0) {
+                return $this->respondError('존재하지 않는 지역입니다.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            // Y/N 정규화
+            $partnership = (($payload['partnership_YN'] ?? 'N') === 'Y') ? 'Y' : 'N';
+            $status      = (($payload['status_YN'] ?? 'Y') === 'N') ? 'N' : 'Y';
+
+            $insertData = [
+                'field_id'       => $fieldId,
+                'area_id'        => $areaId,
+                'name'           => $name,
+                'area_detail'    => trim((string) ($payload['area_detail'] ?? '')),
+                'tonnage'        => trim((string) ($payload['tonnage'] ?? '')),
+                'capacity'       => trim((string) ($payload['capacity'] ?? '')),
+                'zip_code'       => trim((string) ($payload['zip_code'] ?? '')),
+                'address'        => trim((string) ($payload['address'] ?? '')),
+                'address_detail' => trim((string) ($payload['address_detail'] ?? '')),
+                'address_refer'  => trim((string) ($payload['address_refer'] ?? '')),
+                'lat'            => trim((string) ($payload['lat'] ?? '')),
+                'lng'            => trim((string) ($payload['lng'] ?? '')),
+                'partnership_YN' => $partnership,
+                'status_YN'      => $status,
+                'created_at'     => date('Y-m-d H:i:s'),
+            ];
+
+            // 제휴인 경우에만 계좌 정보 저장 (비제휴면 빈 값)
+            // 계좌번호는 양방향 암호화하여 저장
+            if ($partnership === 'Y') {
+                $insertData['bank_code']      = trim((string) ($payload['bank_code'] ?? ''));
+                $insertData['account_number'] = $this->encryptValue(trim((string) ($payload['account_number'] ?? '')));
+                $insertData['account_holder'] = trim((string) ($payload['account_holder'] ?? ''));
+            } else {
+                $insertData['bank_code']      = '';
+                $insertData['account_number'] = '';
+                $insertData['account_holder'] = '';
+            }
+
+            if (!$db->table($this->table)->insert($insertData)) {
+                return $this->respondError('등록에 실패했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+            }
+
+            $newId = $db->insertID();
+            $row = $db->table($this->table)->where('id', $newId)->get()->getRow();
+            // 응답 시 계좌번호 복호화
+            if ($row) {
+                $row->account_number = $this->decryptValue($row->account_number);
+            }
+
+            return $this->respondSuccess($row, '선상이 등록되었습니다.', ResponseInterface::HTTP_CREATED);
+        } catch (\Exception $e) {
+            log_message('error', 'OnboardController create error: ' . $e->getMessage());
+            return $this->respondError('등록 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 선상 상세 조회
+     * GET /api/onboard/:id
+     */
+    public function show($id = null)
+    {
+        $auth = $this->requireAuth();
+        if ($auth instanceof ResponseInterface) {
+            return $auth;
+        }
+
+        if (empty($id)) {
+            return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
+        }
+
+        try {
+            $row = $this->getDB()->table($this->table . ' o')
+                ->select('o.*, f.name as field_name, a.name as area_name')
+                ->join('fishing_field f', 'f.id = o.field_id', 'left')
+                ->join('fishing_area a', 'a.id = o.area_id', 'left')
+                ->where('o.id', (int) $id)
+                ->where('o.deleted_YN', 'N')
+                ->get()
+                ->getRow();
+
+            if (!$row) {
+                return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
+            }
+
+            // 계좌번호 복호화
+            $row->account_number = $this->decryptValue($row->account_number);
+
+            return $this->respondSuccess($row);
+        } catch (\Exception $e) {
+            log_message('error', 'OnboardController show error: ' . $e->getMessage());
+            return $this->respondError('조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 선상 삭제 (soft delete)
+     * DELETE /api/onboard/:id
+     */
+    public function delete($id = null)
+    {
+        $auth = $this->requireAuth();
+        if ($auth instanceof ResponseInterface) {
+            return $auth;
+        }
+
+        if (empty($id)) {
+            return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
+        }
+
+        try {
+            $db = $this->getDB();
+
+            $exists = $db->table($this->table)
+                ->where('id', (int) $id)
+                ->where('deleted_YN', 'N')
+                ->countAllResults();
+            if ($exists === 0) {
+                return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
+            }
+
+            $db->table($this->table)
+                ->where('id', (int) $id)
+                ->update([
+                    'deleted_YN' => 'Y',
+                    'updated_at' => date('Y-m-d H:i:s'),
+                ]);
+
+            return $this->respondSuccess(null, '선상이 삭제되었습니다.');
+        } catch (\Exception $e) {
+            log_message('error', 'OnboardController delete error: ' . $e->getMessage());
+            return $this->respondError('삭제 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 값 암호화 (빈 값은 그대로 빈 문자열)
+     */
+    private function encryptValue(string $plain): string
+    {
+        if ($plain === '') {
+            return '';
+        }
+        $encrypter = \Config\Services::encrypter();
+        return base64_encode($encrypter->encrypt($plain));
+    }
+
+    /**
+     * 값 복호화 (실패/빈 값이면 빈 문자열)
+     */
+    private function decryptValue(?string $cipher): string
+    {
+        if (empty($cipher)) {
+            return '';
+        }
+        try {
+            $encrypter = \Config\Services::encrypter();
+            return $encrypter->decrypt(base64_decode($cipher));
+        } catch (\Exception $e) {
+            log_message('error', 'Account decrypt error: ' . $e->getMessage());
+            return '';
+        }
+    }
+}

+ 993 - 15
db.vuerd.json

@@ -5,11 +5,11 @@
     "width": 2000,
     "height": 2000,
     "scrollTop": 0,
-    "scrollLeft": -21,
-    "zoomLevel": 1,
+    "scrollLeft": -430.3514,
+    "zoomLevel": 0.97,
     "show": 431,
     "database": 4,
-    "databaseName": "",
+    "databaseName": "piratezone",
     "canvasType": "ERD",
     "language": 1,
     "tableNameCase": 4,
@@ -32,9 +32,15 @@
   "doc": {
     "tableIds": [
       "FISm81kEr0UVuk9sKYz4j",
-      "U1M3DNhPb8li5y4zccrUR"
+      "U1M3DNhPb8li5y4zccrUR",
+      "zUvkqgDCrDrdAi4llCexp",
+      "cH4_5N71LdebT2IHo_Dd9"
+    ],
+    "relationshipIds": [
+      "02Rf0D1riQbaw0LqkaD6r",
+      "lo9HDcBWr2MulQ_cz9_mS",
+      "UyIJVtBW8x18iGTJ7LZuv"
     ],
-    "relationshipIds": [],
     "indexIds": [],
     "memoIds": []
   },
@@ -51,7 +57,8 @@
           "LTAAwKOsboOXo0QloDDKe",
           "ynAe4Kk4ZBjsyu2n7LhYc",
           "xpp-hXcIZidnwzPwrdRBc",
-          "4hL25cYYWJqKXtSE-Ko-J"
+          "4hL25cYYWJqKXtSE-Ko-J",
+          "EcLhQU28k3MJy4eQUZuni"
         ],
         "seqColumnIds": [
           "tVtTK-A1GhU8WRdiZT89G",
@@ -60,18 +67,19 @@
           "LTAAwKOsboOXo0QloDDKe",
           "ynAe4Kk4ZBjsyu2n7LhYc",
           "xpp-hXcIZidnwzPwrdRBc",
-          "4hL25cYYWJqKXtSE-Ko-J"
+          "4hL25cYYWJqKXtSE-Ko-J",
+          "EcLhQU28k3MJy4eQUZuni"
         ],
         "ui": {
-          "x": 117,
-          "y": 95,
+          "x": 54.1134,
+          "y": 31.0825,
           "zIndex": 2,
           "widthName": 65,
           "widthComment": 60,
           "color": ""
         },
         "meta": {
-          "updateAt": 1779862680279,
+          "updateAt": 1779935483367,
           "createAt": 1779771222669
         }
       },
@@ -94,17 +102,125 @@
           "GwpDGG7bv-oliz1jukS1a"
         ],
         "ui": {
-          "x": 552,
-          "y": 97,
+          "x": 56.1237,
+          "y": 322.8041,
           "zIndex": 13,
           "widthName": 65,
           "widthComment": 60,
           "color": ""
         },
         "meta": {
-          "updateAt": 1779869237859,
+          "updateAt": 1779935571357,
           "createAt": 1779771453475
         }
+      },
+      "zUvkqgDCrDrdAi4llCexp": {
+        "id": "zUvkqgDCrDrdAi4llCexp",
+        "name": "onboard_photos",
+        "comment": "선상 이미지",
+        "columnIds": [
+          "PEfFzfWPRBTnIavlgYDLV",
+          "9zEjG5L24xZPSGCUtmP49",
+          "GQs2k36PQXHxlLWKV8Q7q",
+          "V60hlgQ2zV3DaAUf6BfHF",
+          "UQyabqT4xnXd0GooElNR4",
+          "19xZKDJkKe-gDRGWuu_Q-",
+          "mYiDsFVxYSWctIUhkVyQm",
+          "1HYmycZuB25fEiOyrXIF1",
+          "Beuabg-oNe9lJGCqsnueZ",
+          "r6kbmoYCKRWTw8RVXBdAd",
+          "xvJg59PiQl2fgtFQxgLY9"
+        ],
+        "seqColumnIds": [
+          "PEfFzfWPRBTnIavlgYDLV",
+          "9zEjG5L24xZPSGCUtmP49",
+          "GQs2k36PQXHxlLWKV8Q7q",
+          "KsPSMYeiJEPVqGy2Crt5Q",
+          "V60hlgQ2zV3DaAUf6BfHF",
+          "UQyabqT4xnXd0GooElNR4",
+          "19xZKDJkKe-gDRGWuu_Q-",
+          "mYiDsFVxYSWctIUhkVyQm",
+          "1HYmycZuB25fEiOyrXIF1",
+          "Beuabg-oNe9lJGCqsnueZ",
+          "r6kbmoYCKRWTw8RVXBdAd",
+          "xvJg59PiQl2fgtFQxgLY9"
+        ],
+        "ui": {
+          "x": 1002.8465,
+          "y": 30.6351,
+          "zIndex": 20,
+          "widthName": 89,
+          "widthComment": 65,
+          "color": ""
+        },
+        "meta": {
+          "updateAt": 1779935567355,
+          "createAt": 1779933385317
+        }
+      },
+      "cH4_5N71LdebT2IHo_Dd9": {
+        "id": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "onboard",
+        "comment": "선상",
+        "columnIds": [
+          "T0k9O_Ol_BAHTkKFWaaVb",
+          "zH8U7DiFE0_b0k7y88KoF",
+          "jOU5il8WO13kfZaWytFCs",
+          "Y5h2fFm6BvWDbhza6tFG7",
+          "R7cMwHvfGjtFLLP8J37tZ",
+          "H5EX7G8tviLeh3KTptlxE",
+          "r6aAybLLZzQCn9aAzCMQ2",
+          "eiIOKSUZ84EyBvZG7TD6S",
+          "7wUGgxG1un_mWhi55brcu",
+          "HZ3vG1kb-x0JqTF70-6Y_",
+          "T3pHkdpwDVFDV7FvAL6SB",
+          "_T-oPpR9IImHaAbX_m2XM",
+          "pZC1QQWLojP2A1YYclGDg",
+          "peqHWMbRbNkjZRX0xwPr2",
+          "5Vj5PmwE2jOifWrtsb8xV",
+          "pziu8u2M4u8bstW_SD_V2",
+          "OfwQq9W0iDpLQ_9lb9QeG",
+          "FBWfJa0SYgeGwp7F7XhxG",
+          "AUGMP-wRSRQJjtTsWuTN7",
+          "QSEyEfejN_SOlC_FRSi74",
+          "rRY6HzigSqa-1-KNKehqG"
+        ],
+        "seqColumnIds": [
+          "T0k9O_Ol_BAHTkKFWaaVb",
+          "2LDOou1BWBPliDIlbVueI",
+          "zH8U7DiFE0_b0k7y88KoF",
+          "jOU5il8WO13kfZaWytFCs",
+          "Y5h2fFm6BvWDbhza6tFG7",
+          "R7cMwHvfGjtFLLP8J37tZ",
+          "H5EX7G8tviLeh3KTptlxE",
+          "r6aAybLLZzQCn9aAzCMQ2",
+          "eiIOKSUZ84EyBvZG7TD6S",
+          "7wUGgxG1un_mWhi55brcu",
+          "HZ3vG1kb-x0JqTF70-6Y_",
+          "T3pHkdpwDVFDV7FvAL6SB",
+          "_T-oPpR9IImHaAbX_m2XM",
+          "pZC1QQWLojP2A1YYclGDg",
+          "peqHWMbRbNkjZRX0xwPr2",
+          "5Vj5PmwE2jOifWrtsb8xV",
+          "pziu8u2M4u8bstW_SD_V2",
+          "OfwQq9W0iDpLQ_9lb9QeG",
+          "FBWfJa0SYgeGwp7F7XhxG",
+          "AUGMP-wRSRQJjtTsWuTN7",
+          "QSEyEfejN_SOlC_FRSi74",
+          "rRY6HzigSqa-1-KNKehqG"
+        ],
+        "ui": {
+          "x": 544.9502,
+          "y": 20.9896,
+          "zIndex": 142,
+          "widthName": 60,
+          "widthComment": 60,
+          "color": ""
+        },
+        "meta": {
+          "updateAt": 1779942361366,
+          "createAt": 1779934492094
+        }
       }
     },
     "tableColumnEntities": {
@@ -347,10 +463,872 @@
           "updateAt": 1779869255332,
           "createAt": 1779869237859
         }
+      },
+      "PEfFzfWPRBTnIavlgYDLV": {
+        "id": "PEfFzfWPRBTnIavlgYDLV",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "id",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 10,
+        "ui": {
+          "keys": 1,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935172896,
+          "createAt": 1779934007015
+        }
+      },
+      "KsPSMYeiJEPVqGy2Crt5Q": {
+        "id": "KsPSMYeiJEPVqGy2Crt5Q",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "onboard_id",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 62,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934044513,
+          "createAt": 1779934023551
+        }
+      },
+      "GQs2k36PQXHxlLWKV8Q7q": {
+        "id": "GQs2k36PQXHxlLWKV8Q7q",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "original_name",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 76,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934180668,
+          "createAt": 1779934026588
+        }
+      },
+      "V60hlgQ2zV3DaAUf6BfHF": {
+        "id": "V60hlgQ2zV3DaAUf6BfHF",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "stored_name",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 70,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934181207,
+          "createAt": 1779934028247
+        }
+      },
+      "UQyabqT4xnXd0GooElNR4": {
+        "id": "UQyabqT4xnXd0GooElNR4",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "file_path",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934181988,
+          "createAt": 1779934028749
+        }
+      },
+      "19xZKDJkKe-gDRGWuu_Q-": {
+        "id": "19xZKDJkKe-gDRGWuu_Q-",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "file_size",
+        "comment": "",
+        "dataType": "BIGINT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934182760,
+          "createAt": 1779934029206
+        }
+      },
+      "mYiDsFVxYSWctIUhkVyQm": {
+        "id": "mYiDsFVxYSWctIUhkVyQm",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "mime_type",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934184560,
+          "createAt": 1779934029739
+        }
+      },
+      "1HYmycZuB25fEiOyrXIF1": {
+        "id": "1HYmycZuB25fEiOyrXIF1",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "width",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934177360,
+          "createAt": 1779934030549
+        }
+      },
+      "Beuabg-oNe9lJGCqsnueZ": {
+        "id": "Beuabg-oNe9lJGCqsnueZ",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "height",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934177938,
+          "createAt": 1779934031073
+        }
+      },
+      "r6kbmoYCKRWTw8RVXBdAd": {
+        "id": "r6kbmoYCKRWTw8RVXBdAd",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "sort_order",
+        "comment": "",
+        "dataType": "INT",
+        "default": "'0'",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934191153,
+          "createAt": 1779934031884
+        }
+      },
+      "xvJg59PiQl2fgtFQxgLY9": {
+        "id": "xvJg59PiQl2fgtFQxgLY9",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "created_at",
+        "comment": "",
+        "dataType": "TIMESTAMP",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 65,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779934312992,
+          "createAt": 1779934032355
+        }
+      },
+      "T0k9O_Ol_BAHTkKFWaaVb": {
+        "id": "T0k9O_Ol_BAHTkKFWaaVb",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "id",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 10,
+        "ui": {
+          "keys": 1,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935179638,
+          "createAt": 1779934913196
+        }
+      },
+      "2LDOou1BWBPliDIlbVueI": {
+        "id": "2LDOou1BWBPliDIlbVueI",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "field_id",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935166167,
+          "createAt": 1779935142409
+        }
+      },
+      "EcLhQU28k3MJy4eQUZuni": {
+        "id": "EcLhQU28k3MJy4eQUZuni",
+        "tableId": "FISm81kEr0UVuk9sKYz4j",
+        "name": "id",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935435996,
+          "createAt": 1779935435996
+        }
+      },
+      "zH8U7DiFE0_b0k7y88KoF": {
+        "id": "zH8U7DiFE0_b0k7y88KoF",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "field_id",
+        "comment": "분야",
+        "dataType": "INT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 2,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935618303,
+          "createAt": 1779935483921
+        }
+      },
+      "9zEjG5L24xZPSGCUtmP49": {
+        "id": "9zEjG5L24xZPSGCUtmP49",
+        "tableId": "zUvkqgDCrDrdAi4llCexp",
+        "name": "onboard_id",
+        "comment": "",
+        "dataType": "INT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 2,
+          "widthName": 62,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935546116,
+          "createAt": 1779935535175
+        }
+      },
+      "jOU5il8WO13kfZaWytFCs": {
+        "id": "jOU5il8WO13kfZaWytFCs",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "area_id",
+        "comment": "지역",
+        "dataType": "INT",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 2,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935622398,
+          "createAt": 1779935590724
+        }
+      },
+      "Y5h2fFm6BvWDbhza6tFG7": {
+        "id": "Y5h2fFm6BvWDbhza6tFG7",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "name",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779936053604,
+          "createAt": 1779935625450
+        }
+      },
+      "R7cMwHvfGjtFLLP8J37tZ": {
+        "id": "R7cMwHvfGjtFLLP8J37tZ",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "area_detail",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935785600,
+          "createAt": 1779935636884
+        }
+      },
+      "H5EX7G8tviLeh3KTptlxE": {
+        "id": "H5EX7G8tviLeh3KTptlxE",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "tonnage",
+        "comment": "중량",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779941624873,
+          "createAt": 1779935664917
+        }
+      },
+      "r6aAybLLZzQCn9aAzCMQ2": {
+        "id": "r6aAybLLZzQCn9aAzCMQ2",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "capacity",
+        "comment": "탑승인원",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935794449,
+          "createAt": 1779935691604
+        }
+      },
+      "eiIOKSUZ84EyBvZG7TD6S": {
+        "id": "eiIOKSUZ84EyBvZG7TD6S",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "zip_code",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935795897,
+          "createAt": 1779935751511
+        }
+      },
+      "7wUGgxG1un_mWhi55brcu": {
+        "id": "7wUGgxG1un_mWhi55brcu",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "address",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935966439,
+          "createAt": 1779935797682
+        }
+      },
+      "HZ3vG1kb-x0JqTF70-6Y_": {
+        "id": "HZ3vG1kb-x0JqTF70-6Y_",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "address_detail",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 77,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779936050102,
+          "createAt": 1779935816650
+        }
+      },
+      "T3pHkdpwDVFDV7FvAL6SB": {
+        "id": "T3pHkdpwDVFDV7FvAL6SB",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "address_refer",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 73,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935961677,
+          "createAt": 1779935838924
+        }
+      },
+      "_T-oPpR9IImHaAbX_m2XM": {
+        "id": "_T-oPpR9IImHaAbX_m2XM",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "lat",
+        "comment": "위도",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935959697,
+          "createAt": 1779935845772
+        }
+      },
+      "pZC1QQWLojP2A1YYclGDg": {
+        "id": "pZC1QQWLojP2A1YYclGDg",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "lng",
+        "comment": "경도",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935958174,
+          "createAt": 1779935863779
+        }
+      },
+      "FBWfJa0SYgeGwp7F7XhxG": {
+        "id": "FBWfJa0SYgeGwp7F7XhxG",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "status_YN",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "'Y'",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779936099309,
+          "createAt": 1779935898047
+        }
+      },
+      "AUGMP-wRSRQJjtTsWuTN7": {
+        "id": "AUGMP-wRSRQJjtTsWuTN7",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "deleted_YN",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "'N'",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 63,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779936101581,
+          "createAt": 1779935899570
+        }
+      },
+      "QSEyEfejN_SOlC_FRSi74": {
+        "id": "QSEyEfejN_SOlC_FRSi74",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "created_at",
+        "comment": "",
+        "dataType": "TIMESTAMP",
+        "default": "",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 65,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779936107439,
+          "createAt": 1779935921274
+        }
+      },
+      "rRY6HzigSqa-1-KNKehqG": {
+        "id": "rRY6HzigSqa-1-KNKehqG",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "updated_at",
+        "comment": "",
+        "dataType": "TIMESTAMP",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 62,
+          "widthComment": 60,
+          "widthDataType": 65,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779935946375,
+          "createAt": 1779935927041
+        }
+      },
+      "peqHWMbRbNkjZRX0xwPr2": {
+        "id": "peqHWMbRbNkjZRX0xwPr2",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "partnership_YN",
+        "comment": "제휴여부",
+        "dataType": "VARCHAR",
+        "default": "'N'",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 83,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779936096653,
+          "createAt": 1779935997660
+        }
+      },
+      "5Vj5PmwE2jOifWrtsb8xV": {
+        "id": "5Vj5PmwE2jOifWrtsb8xV",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "bank_code",
+        "comment": "은행코드",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779942341431,
+          "createAt": 1779942294125
+        }
+      },
+      "pziu8u2M4u8bstW_SD_V2": {
+        "id": "pziu8u2M4u8bstW_SD_V2",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "account_number",
+        "comment": "계좌번호",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 90,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779942344569,
+          "createAt": 1779942313139
+        }
+      },
+      "OfwQq9W0iDpLQ_9lb9QeG": {
+        "id": "OfwQq9W0iDpLQ_9lb9QeG",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "name": "account_holder",
+        "comment": "예금주명",
+        "dataType": "VARCHAR",
+        "default": "",
+        "options": 0,
+        "ui": {
+          "keys": 0,
+          "widthName": 83,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779942347514,
+          "createAt": 1779942328026
+        }
+      }
+    },
+    "relationshipEntities": {
+      "PE78A013FGrCpojrvbnsz": {
+        "id": "PE78A013FGrCpojrvbnsz",
+        "identification": false,
+        "relationshipType": 8,
+        "startRelationshipType": 2,
+        "start": {
+          "tableId": "cH4_5N71LdebT2IHo_Dd9",
+          "columnIds": [
+            "T0k9O_Ol_BAHTkKFWaaVb"
+          ],
+          "x": 215.07909999999998,
+          "y": 388,
+          "direction": 4
+        },
+        "end": {
+          "tableId": "FISm81kEr0UVuk9sKYz4j",
+          "columnIds": [
+            "EcLhQU28k3MJy4eQUZuni"
+          ],
+          "x": 240.6134,
+          "y": 280.1134,
+          "direction": 8
+        },
+        "meta": {
+          "updateAt": 1779935435996,
+          "createAt": 1779935435996
+        }
+      },
+      "02Rf0D1riQbaw0LqkaD6r": {
+        "id": "02Rf0D1riQbaw0LqkaD6r",
+        "identification": false,
+        "relationshipType": 8,
+        "startRelationshipType": 2,
+        "start": {
+          "tableId": "FISm81kEr0UVuk9sKYz4j",
+          "columnIds": [
+            "tVtTK-A1GhU8WRdiZT89G"
+          ],
+          "x": 427.1134,
+          "y": 155.0825,
+          "direction": 2
+        },
+        "end": {
+          "tableId": "cH4_5N71LdebT2IHo_Dd9",
+          "columnIds": [
+            "zH8U7DiFE0_b0k7y88KoF"
+          ],
+          "x": 544.9502,
+          "y": 160.9896,
+          "direction": 1
+        },
+        "meta": {
+          "updateAt": 1779935483922,
+          "createAt": 1779935483922
+        }
+      },
+      "lo9HDcBWr2MulQ_cz9_mS": {
+        "id": "lo9HDcBWr2MulQ_cz9_mS",
+        "identification": false,
+        "relationshipType": 8,
+        "startRelationshipType": 2,
+        "start": {
+          "tableId": "cH4_5N71LdebT2IHo_Dd9",
+          "columnIds": [
+            "T0k9O_Ol_BAHTkKFWaaVb"
+          ],
+          "x": 944.9502,
+          "y": 300.9896,
+          "direction": 2
+        },
+        "end": {
+          "tableId": "zUvkqgDCrDrdAi4llCexp",
+          "columnIds": [
+            "9zEjG5L24xZPSGCUtmP49"
+          ],
+          "x": 1002.8465,
+          "y": 190.6351,
+          "direction": 1
+        },
+        "meta": {
+          "updateAt": 1779935535175,
+          "createAt": 1779935535175
+        }
+      },
+      "UyIJVtBW8x18iGTJ7LZuv": {
+        "id": "UyIJVtBW8x18iGTJ7LZuv",
+        "identification": false,
+        "relationshipType": 8,
+        "startRelationshipType": 2,
+        "start": {
+          "tableId": "U1M3DNhPb8li5y4zccrUR",
+          "columnIds": [
+            "9O46Rt9JdNS_X3fayZRJu"
+          ],
+          "x": 429.1237,
+          "y": 410.8041,
+          "direction": 2
+        },
+        "end": {
+          "tableId": "cH4_5N71LdebT2IHo_Dd9",
+          "columnIds": [
+            "jOU5il8WO13kfZaWytFCs"
+          ],
+          "x": 544.9502,
+          "y": 440.9896,
+          "direction": 1
+        },
+        "meta": {
+          "updateAt": 1779935590724,
+          "createAt": 1779935590724
+        }
+      }
+    },
+    "indexEntities": {
+      "-MCTIFN9XEQ-y6MQi7vTE": {
+        "id": "-MCTIFN9XEQ-y6MQi7vTE",
+        "name": "",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "indexColumnIds": [],
+        "seqIndexColumnIds": [],
+        "unique": false,
+        "meta": {
+          "updateAt": 1779935189255,
+          "createAt": 1779935189255
+        }
+      },
+      "8Qk67gKfrATzMVhIHZ5ew": {
+        "id": "8Qk67gKfrATzMVhIHZ5ew",
+        "name": "",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "indexColumnIds": [],
+        "seqIndexColumnIds": [],
+        "unique": false,
+        "meta": {
+          "updateAt": 1779935190160,
+          "createAt": 1779935190160
+        }
+      },
+      "Od9vkpiwxuNNbPvVAVfNF": {
+        "id": "Od9vkpiwxuNNbPvVAVfNF",
+        "name": "",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "indexColumnIds": [],
+        "seqIndexColumnIds": [],
+        "unique": false,
+        "meta": {
+          "updateAt": 1779935203751,
+          "createAt": 1779935203751
+        }
+      },
+      "GiW8Tmldcc4cpFJ1vHGJk": {
+        "id": "GiW8Tmldcc4cpFJ1vHGJk",
+        "name": "",
+        "tableId": "cH4_5N71LdebT2IHo_Dd9",
+        "indexColumnIds": [],
+        "seqIndexColumnIds": [],
+        "unique": false,
+        "meta": {
+          "updateAt": 1779935204830,
+          "createAt": 1779935204830
+        }
       }
     },
-    "relationshipEntities": {},
-    "indexEntities": {},
     "indexColumnEntities": {},
     "memoEntities": {}
   }

+ 1 - 0
nuxt.config.ts

@@ -69,6 +69,7 @@ if(window.wcs) {
       apiBase: process.env.NUXT_PUBLIC_API_BASE,
       imageBase: process.env.NUXT_PUBLIC_IMAGE_BASE,
       mediaBase: process.env.NUXT_PUBLIC_MEDIA_BASE,
+      googleMapKey: process.env.NUXT_PUBLIC_GOOGLE_MAP_KEY,
     }
   }
 })