فهرست منبع

낚시분야 완료

DESKTOP-T61HUSC\user 1 ماه پیش
والد
کامیت
21f4da7a32

+ 65 - 31
app/assets/scss/admin.scss

@@ -2010,12 +2010,12 @@ footer {
   .admin--form-textarea,
   .admin--form-select {
     width: 100%;
-    padding: 12px 16px;
+    padding: 12px;
     background: var(--admin-bg-tertiary);
     border: 1px solid var(--admin-border-color);
     border-radius: 6px;
     color: var(--admin-text-primary);
-    font-size: 14px;
+    font-size: 13px;
     font-family: 'FORDKOREAType', sans-serif;
     transition: all 0.3s ease;
 
@@ -2140,6 +2140,11 @@ footer {
     color: #fff;
   }
 
+  .admin--btn-blue{
+    background-color: #3c80f2;
+    color: #fff;
+  }
+
   .admin--btn-red-border{
     border-color: #e85d3f;
     color: #e85d3f;
@@ -2257,13 +2262,15 @@ footer {
   .admin--form-select,
   .admin--form-input,
   .admin--search-input {
-    border: 1px solid var(--admin-border-color) !important;
+    border: 1px solid #e8eaef !important;
     border-radius: 4px;
-    height: 33px !important;
-    padding: 6px 14px !important;
+    padding: 12px;
     font-size: 13px !important;
-    background: var(--admin-bg-tertiary) !important;
+    background-color: #fff;
     line-height: normal;
+    &::placeholder{
+      color: #a8adb8;
+    }
   }
 
   .admin--search-select,
@@ -2321,12 +2328,6 @@ footer {
   }
 }
 
-// 검색 input 강제 스타일 (두 클래스 함께 사용될 때)
-.admin--form-input.admin--search-input {
-  border: 1px solid var(--admin-border-color) !important;
-  background: var(--admin-bg-tertiary) !important;
-}
-
 // 테이블
 .admin--table-wrapper {
   background: var(--admin-bg-secondary);
@@ -2340,22 +2341,23 @@ footer {
   border-collapse: collapse;
 
   thead {
-    background: var(--admin-bg-tertiary);
-    border-bottom: 1px solid var(--admin-border-color);
+    background: #F8F9FB;
+    border-bottom: 1px solid #E8EAEF;
 
     th {
       padding: 14px 16px;
-      text-align: left;
-      font-size: 14px;
+      text-align: center;
+      font-size: 12px;
       font-weight: 600;
-      color: var(--admin-text-primary);
+      color: #666b75;
       white-space: nowrap;
+      letter-spacing: 0.36px;
     }
   }
 
   tbody {
     tr {
-      border-bottom: 1px solid var(--admin-border-color);
+      border-bottom: 1px solid #E8EAEF;
       transition: background 0.3s ease;
 
       &:hover {
@@ -2365,13 +2367,35 @@ footer {
       &:last-child {
         border-bottom: none;
       }
+
+      &.admin--table-row-clickable {
+        cursor: pointer;
+
+        &:hover {
+          background: var(--admin-bg-primary);
+        }
+      }
     }
 
     td {
       padding: 14px 16px;
       font-size: 14px;
-      color: var(--admin-text-secondary);
+      font-weight: 500;
+      color: #1a2b4a;
       vertical-align: middle;
+      text-align: center;
+      &.left{
+        text-align: left;
+      }
+      &.color--yellow{
+        color: var(--admin-yellow);
+        font-weight: 700;
+      }
+      &.date{
+        color: #666b75;
+        font-size: 12px;
+        font-weight: 400;
+      }
     }
 
     .admin--table-title {
@@ -2394,6 +2418,11 @@ footer {
   .admin--table-actions {
     display: flex;
     gap: 8px;
+    .admin--btn-blue{
+      background-color: #3c80f2;
+      color: #fff;
+      font-size: 12px;
+    }
   }
 }
 
@@ -2516,8 +2545,8 @@ footer {
   align-items: center;
   gap: 8px;
   cursor: pointer;
-  font-size: 14px;
-  color: var(--admin-text-secondary);
+  font-size: 13px;
+  color: #666b75;
   transition: color 0.3s ease;
 
   input[type="radio"] {
@@ -2749,15 +2778,14 @@ footer {
 }
 
 // 작은 버튼 (secondary 추가)
-.admin--btn-small-secondary {
-  background: var(--admin-bg-tertiary);
-  color: var(--admin-text-secondary);
-  border: 1px solid var(--admin-border-color);
-
-  &:hover {
-    background: var(--admin-bg-primary);
-    border-color: var(--admin-accent-primary);
-    color: var(--admin-text-primary);
+.admin--btn-small{
+  &.admin--btn-small-secondary {
+    border-radius: 6px;
+    border: 1px solid #E8EAEF;
+    background: #FFF;
+    &:hover{
+      border: 1px solid #e8eaef!important;
+    }
   }
 }
 
@@ -7608,6 +7636,9 @@ footer {
 
 .admin--form--table{
   width: 100%;
+  color: #666b75;
+  font-weight: 400;
+  font-size: 12px;
   p{
     color: #666b75;
     font-weight: 400;
@@ -7638,6 +7669,9 @@ footer {
       gap: 12px;
       input{
         width: 320px;
+        &[type=radio]{
+          width: auto;
+        }
         &.w--full{
           width: 100%;
         }
@@ -7654,7 +7688,7 @@ footer {
   border-radius: 6px;
   margin-top: 32px;
   padding: 20px;
-  h2{
+  h3{
     color: #e8b547;
     font-size: 14px;
     font-weight: 600;

+ 21 - 7
app/layouts/admin.vue

@@ -1,5 +1,5 @@
 <script setup>
-  import { ref, computed } from "vue";
+  import { ref, computed, watch } from "vue";
   import { useRoute, useRouter } from "vue-router";
   import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
   import AdminModal from "~/components/admin/AdminModal.vue";
@@ -29,7 +29,7 @@
         {
           title: "낚시분야",
           path: "/site-manager/field/list",
-          pattern: /^\/site-manager\/field\/(list|create|edit)/,
+          pattern: /^\/site-manager\/field\/(list|create|edit|detail)/,
         },
         {
           title: "지역관리",
@@ -66,11 +66,17 @@
     }
   };
 
-  // 현재 활성 라우트 체크
+  // 현재 활성 라우트 체크 (단일 path 메뉴 — children 없는 메뉴용)
   const isActiveRoute = (path) => {
     return route.path === path;
   };
 
+  // 서브메뉴 활성 여부 (pattern 우선, 없으면 path 정확 일치)
+  const isSubmenuActive = (item) => {
+    if (item?.pattern && item.pattern.test(route.path)) return true;
+    return route.path === item?.path;
+  };
+
   // 현재 경로에 맞는 메뉴 찾기
   const findCurrentMenu = () => {
     const currentPath = route.path;
@@ -102,6 +108,16 @@
     return { menu: null, child: null };
   };
 
+  // 라우트 변경 시 현재 경로에 해당하는 토글만 열고 나머지는 모두 접기
+  watch(
+    () => route.path,
+    () => {
+      const { menu } = findCurrentMenu();
+      openMenus.value = menu?.children ? [menu.id] : [];
+    },
+    { immediate: true }
+  );
+
   // 페이지 타이틀 계산
   const pageTitle = computed(() => {
     const currentPath = route.path;
@@ -366,7 +382,7 @@
                     v-for="submenu in menu.children"
                     :key="submenu.path"
                     class="admin--gnb-item"
-                    :class="{ 'is-active': isActiveRoute(submenu.path) }"
+                    :class="{ 'is-active': isSubmenuActive(submenu) }"
                   >
                     <NuxtLink :to="submenu.path" class="admin--gnb-link">
                       {{ submenu.title }}
@@ -382,9 +398,7 @@
       <!-- Content Area -->
       <main class="admin--main">
         <!-- Page Content -->
-        <div class="admin--page-content">
-          <slot />
-        </div>
+        <slot />
       </main>
     </div>
 

+ 101 - 83
app/pages/site-manager/field/create.vue

@@ -1,83 +1,85 @@
 <template>
-  <div class="">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 낚시분야 -->
-      <table class="admin--form--table">
-        <colgroup>
-          <col style="width: 120px;">
-          <col>
-        </colgroup>
-        <tbody>
-          <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
-                />
-                <p>낚시 종류 분류명. 1~30자 이내. 중복 불가</p>
-              </div>
-            </td>
-          </tr>
-          <tr>
-            <th>
-              <div>
-                가중치 <span class="admin--required">*</span>
-              </div>
-            </th>
-            <td>
-              <div class="input--wrap">
-                <input
-                v-model="formData.weight"
-                type="text"
-                class="admin--form-input"
-                placeholder=""
-                required
-                />
-                <p>0.0 ~ 1.0 사이 소수점 1자리 (최대값 1.0 = 100% 확률). 물고기 잡을 때 아이템 지급 확률</p>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-
-      <div class="admin--info--box">
-        <h2>💡 가중치 안내</h2>
-        <ul>
-          <li>가중치 = 해당 분야에서 물고기 잡았을 때 아이템이 지급될 확률 (최대값 1.0 = 100%)</li>
-          <li>예: 0.8 = 80% 확률로 아이템 드롭. 분야별 난이도/희소성에 맞춰 신중히 설정</li>
-        </ul>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <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="">
-          삭제
-        </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 class="admin--page-content">
+    <div class="admin--form">
+      <form @submit.prevent="handleSubmit" class="">
+        <!-- 낚시분야 -->
+        <table class="admin--form--table">
+          <colgroup>
+            <col style="width: 120px;">
+            <col>
+          </colgroup>
+          <tbody>
+            <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
+                  />
+                  <p>낚시 종류 분류명. 1~30자 이내. 중복 불가</p>
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th>
+                <div>
+                  가중치 <span class="admin--required">*</span>
+                </div>
+              </th>
+              <td>
+                <div class="input--wrap">
+                  <input
+                  v-model="formData.weight"
+                  type="text"
+                  class="admin--form-input w--120"
+                  placeholder="0.0 ~ 1.0"
+                  required
+                  />
+                  <p>0.0 ~ 1.0 사이 소수점 1자리 (최대값 1.0 = 100% 확률). 물고기 잡을 때 아이템 지급 확률</p>
+                </div>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+  
+        <div class="admin--info--box">
+          <h3>💡 가중치 안내</h3>
+          <ul>
+            <li>가중치 = 해당 분야에서 물고기 잡았을 때 아이템이 지급될 확률 (최대값 1.0 = 100%)</li>
+            <li>예: 0.8 = 80% 확률로 아이템 드롭. 분야별 난이도/희소성에 맞춰 신중히 설정</li>
+          </ul>
+        </div>
+  
+        <!-- 버튼 영역 -->
+        <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="">
+            삭제
+          </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>
 
@@ -99,33 +101,49 @@
 
   const formData = ref({
     name: "",
+    weight: "",
   });
 
+  // 가중치 형식: 0.0 ~ 1.0, 소수점 1자리
+  const WEIGHT_PATTERN = /^(0(\.\d)?|1(\.0)?)$/;
+
   // 폼 제출
   const handleSubmit = async () => {
     successMessage.value = "";
     errorMessage.value = "";
 
+    const name = formData.value.name.trim();
+    const weight = formData.value.weight.trim();
+
     // 유효성 검사
-    if (!formData.value.name) {
+    if (!name) {
       errorMessage.value = "분야명을 입력하세요.";
       return;
     }
-    if (!formData.value.weight) {
+    if (name.length > 30) {
+      errorMessage.value = "분야명은 30자 이내로 입력하세요.";
+      return;
+    }
+    if (!weight) {
       errorMessage.value = "가중치를 입력하세요.";
       return;
     }
+    if (!WEIGHT_PATTERN.test(weight)) {
+      errorMessage.value = "가중치는 0.0 ~ 1.0 사이 소수점 1자리로 입력하세요.";
+      return;
+    }
+
     isSaving.value = true;
 
     try {
-      const { data, error } = await post("/branch", formData.value);
+      const { data, error } = await post("/field", { name, weight });
 
       if (error || !data?.success) {
         errorMessage.value = error?.message || data?.message || "등록에 실패했습니다.";
       } else {
         successMessage.value = data.message || "낚시분야가 등록되었습니다.";
         setTimeout(() => {
-          router.push("/site-manager/branch/list");
+          router.push("/site-manager/field/list");
         }, 1000);
       }
     } catch (error) {

+ 176 - 0
app/pages/site-manager/field/detail/[id].vue

@@ -0,0 +1,176 @@
+<template>
+  <div class="admin--page-content">
+    <div class="admin--form">
+      <!-- 낚시분야 상세 -->
+      <table class="admin--form--table">
+        <colgroup>
+          <col style="width: 120px;">
+          <col>
+        </colgroup>
+        <tbody>
+          <tr>
+            <th><div>분야</div></th>
+            <td>{{ formData.name || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>가중치</div></th>
+            <td class="color--yellow">{{ formData.weight || "-" }}</td>
+          </tr>
+          <tr>
+            <th><div>상태</div></th>
+            <td>
+              <span :class="['admin--badge', formData.status_YN === 'Y' ? 'admin--badge-active' : 'admin--badge-ended']">
+                {{ formData.status_YN === "Y" ? "사용중" : "미사용" }}
+              </span>
+            </td>
+          </tr>
+          <tr>
+            <th><div>등록일</div></th>
+            <td>{{ formatDateTime(formData.created_at) }}</td>
+          </tr>
+          <tr>
+            <th><div>최근 수정</div></th>
+            <td>{{ formatDateTime(formData.updated_at) }}</td>
+          </tr>
+          <tr>
+            <th><div>사용 챌린지</div></th>
+            <td></td>
+          </tr>
+          <tr>
+            <th><div>사용 퀘스트</div></th>
+            <td></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 fieldId = route.params.id;
+
+  const formData = ref({
+    name: "",
+    weight: "",
+    status_YN: "Y",
+    created_at: "",
+    updated_at: "",
+  });
+
+  // 알림 모달
+  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, error } = await get(`/field/${fieldId}`);
+
+    if (error || !data?.success) {
+      showAlert(error?.message || data?.message || "조회에 실패했습니다.", "오류");
+      return;
+    }
+
+    const row = data.data || {};
+    formData.value = {
+      name: row.name ?? "",
+      weight: row.weight ?? "",
+      status_YN: row.status_YN ?? "Y",
+      created_at: row.created_at ?? "",
+      updated_at: row.updated_at ?? "",
+    };
+  };
+
+  // 삭제
+  const handleDelete = () => {
+    showConfirm(
+      `'${formData.value.name}' 낚시분야를 삭제하시겠습니까?`,
+      async () => {
+        const { data, error } = await del(`/field/${fieldId}`);
+        if (error || !data?.success) {
+          showAlert(error?.message || data?.message || "삭제에 실패했습니다.", "오류");
+        } else {
+          showAlert(data.message || "삭제되었습니다.", "성공");
+          setTimeout(() => router.push("/site-manager/field/list"), 800);
+        }
+      },
+      "낚시분야 삭제"
+    );
+  };
+
+  // 이동
+  const goToList = () => router.push("/site-manager/field/list");
+  const goToEdit = () => router.push(`/site-manager/field/edit/${fieldId}`);
+
+  // 일시 포맷
+  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>

+ 110 - 168
app/pages/site-manager/field/edit/[id].vue

@@ -1,106 +1,82 @@
 <template>
-  <div class="admin--branch-form">
-    <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 지점명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >지점명 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="지점명을 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 대표번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >대표번호 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.main_phone"
-          type="tel"
-          class="admin--form-input"
-          placeholder="02-1234-5678"
-          required
-        />
-      </div>
-
-      <!-- 주소 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label"
-          >주소 <span class="admin--required">*</span></label
-        >
-        <input
-          v-model="formData.address"
-          type="text"
-          class="admin--form-input"
-          placeholder="주소를 입력하세요"
-          required
-        />
-      </div>
-
-      <!-- 상세주소 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">상세주소</label>
-        <input
-          v-model="formData.detail_address"
-          type="text"
-          class="admin--form-input"
-          placeholder="상세주소를 입력하세요"
-        />
-      </div>
-
-      <!-- 위도/경도 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">위치 좌표</label>
-        <div class="admin--coordinate-group">
-          <div class="admin--coordinate-item">
-            <label>위도</label>
-            <input
-              v-model.number="formData.latitude"
-              type="number"
-              step="any"
-              class="admin--form-input"
-              placeholder="37.5665"
-            />
-          </div>
-          <div class="admin--coordinate-item">
-            <label>경도</label>
-            <input
-              v-model.number="formData.longitude"
-              type="number"
-              step="any"
-              class="admin--form-input"
-              placeholder="126.9780"
-            />
-          </div>
-        </div>
-      </div>
-
-      <!-- 영업시간 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">영업시간</label>
-        <textarea
-          v-model="formData.business_hours"
-          class="admin--form-textarea"
-          rows="3"
-          placeholder="평일: 09:00 - 18:00&#10;주말: 10:00 - 17:00"
-        ></textarea>
+  <div class="admin--page-content">
+    <form @submit.prevent="handleSubmit" class="admin--form">
+      <!-- 낚시분야 -->
+      <table class="admin--form--table">
+        <colgroup>
+          <col style="width: 120px;">
+          <col>
+        </colgroup>
+        <tbody>
+          <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
+                />
+                <p>낚시 종류 분류명. 1~30자 이내. 중복 불가</p>
+              </div>
+            </td>
+          </tr>
+          <tr>
+            <th>
+              <div>
+                가중치 <span class="admin--required">*</span>
+              </div>
+            </th>
+            <td>
+              <div class="input--wrap">
+                <input
+                  v-model="formData.weight"
+                  type="text"
+                  class="admin--form-input w--120"
+                  placeholder="0.0 ~ 1.0"
+                  required
+                />
+                <p>0.0 ~ 1.0 사이 소수점 1자리 (최대값 1.0 = 100% 확률). 물고기 잡을 때 아이템 지급 확률</p>
+              </div>
+            </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--info--box">
+        <h3>💡 가중치 안내</h3>
+        <ul>
+          <li>가중치 = 해당 분야에서 물고기 잡았을 때 아이템이 지급될 확률 (최대값 1.0 = 100%)</li>
+          <li>예: 0.8 = 80% 확률로 아이템 드롭. 분야별 난이도/희소성에 맞춰 신중히 설정</li>
+        </ul>
       </div>
 
       <!-- 버튼 영역 -->
       <div class="admin--form-actions">
-        <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
-          {{ isSaving ? "저장 중..." : "확인" }}
+        <button type="button" class="admin--btn" @click="goToList">
+          ← 목록으로
         </button>
-        <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
-          목록
+        <button type="submit" class="admin--btn admin--btn-red ml--auto" :disabled="isSaving">
+          {{ isSaving ? "저장 중..." : "저장" }}
         </button>
       </div>
 
@@ -128,42 +104,36 @@
   const router = useRouter();
   const { get, put } = useApi();
 
-  const isLoading = ref(true);
+  const fieldId = route.params.id;
+
   const isSaving = ref(false);
   const successMessage = ref("");
   const errorMessage = ref("");
 
   const formData = ref({
     name: "",
-    main_phone: "",
-    address: "",
-    detail_address: "",
-    latitude: null,
-    longitude: null,
-    business_hours: "",
+    weight: "",
+    status_YN: "Y",
   });
 
-  // 데이터 로드
-  const loadBranch = async () => {
-    isLoading.value = true;
-
-    const id = route.params.id;
-    const { data, error } = await get(`/branch/${id}`);
-
-    if (data?.success && data?.data) {
-      const branch = data.data;
-      formData.value = {
-        name: branch.name || "",
-        main_phone: branch.main_phone || "",
-        address: branch.address || "",
-        detail_address: branch.detail_address || "",
-        latitude: branch.latitude || null,
-        longitude: branch.longitude || null,
-        business_hours: branch.business_hours || "",
-      };
+  // 가중치 형식: 0.0 ~ 1.0, 소수점 1자리
+  const WEIGHT_PATTERN = /^(0(\.\d)?|1(\.0)?)$/;
+
+  // 기존 데이터 로드
+  const loadDetail = async () => {
+    const { data, error } = await get(`/field/${fieldId}`);
+
+    if (error || !data?.success) {
+      errorMessage.value = error?.message || data?.message || "조회에 실패했습니다.";
+      return;
     }
 
-    isLoading.value = false;
+    const row = data.data || {};
+    formData.value = {
+      name: row.name ?? "",
+      weight: row.weight ?? "",
+      status_YN: row.status_YN ?? "Y",
+    };
   };
 
   // 폼 제출
@@ -171,68 +141,40 @@
     successMessage.value = "";
     errorMessage.value = "";
 
-    // 유효성 검사
-    if (!formData.value.name) {
-      errorMessage.value = "지점명을 입력하세요.";
-      return;
-    }
-
-    if (!formData.value.main_phone) {
-      errorMessage.value = "대표번호를 입력하세요.";
-      return;
-    }
+    const name = formData.value.name.trim();
+    const weight = formData.value.weight.trim();
+    const status_YN = formData.value.status_YN;
 
-    if (!formData.value.address) {
-      errorMessage.value = "주소를 입력하세요.";
-      return;
-    }
+    if (!name) return (errorMessage.value = "분야명을 입력하세요.");
+    if (name.length > 30) return (errorMessage.value = "분야명은 30자 이내로 입력하세요.");
+    if (!weight) return (errorMessage.value = "가중치를 입력하세요.");
+    if (!WEIGHT_PATTERN.test(weight))
+      return (errorMessage.value = "가중치는 0.0 ~ 1.0 사이 소수점 1자리로 입력하세요.");
 
     isSaving.value = true;
-
     try {
-      const id = route.params.id;
-      const { data, error } = await put(`/branch/${id}`, formData.value);
+      const { data, error } = await put(`/field/${fieldId}`, { name, weight, status_YN });
 
       if (error || !data?.success) {
         errorMessage.value = error?.message || data?.message || "수정에 실패했습니다.";
       } else {
-        successMessage.value = data.message || "지점이 수정되었습니다.";
+        successMessage.value = data.message || "수정되었습니다.";
         setTimeout(() => {
-          router.push("/site-manager/branch/list");
-        }, 1000);
+          router.push(`/site-manager/field/detail/${fieldId}`);
+        }, 800);
       }
-    } catch (error) {
+    } catch (e) {
       errorMessage.value = "서버 오류가 발생했습니다.";
-      console.error("Save error:", error);
+      console.error("Update error:", e);
     } finally {
       isSaving.value = false;
     }
   };
 
-  // 목록으로 이동
-  const goToList = () => {
-    router.push("/site-manager/branch/list");
-  };
+  // 이동
+  const goToList = () => router.push(`/site-manager/field/list`);
 
   onMounted(() => {
-    loadBranch();
+    loadDetail();
   });
 </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>

+ 91 - 164
app/pages/site-manager/field/list.vue

@@ -1,12 +1,25 @@
 <template>
-  <div class="admin--branch-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-add" @click="goToCreate">
-          + 새 분야 추가
-        </button>
+        <button class="admin--btn-add" @click="goToCreate">+ 새 분야 추가</button>
       </div>
     </div>
 
@@ -15,49 +28,42 @@
       <table class="admin--table">
         <thead>
           <tr>
-            <th></th>
-            <th style="">번호</th>
-            <th style="width: 70%">분야</th>
+            <th style="width: 80px;">번호</th>
+            <th style="width: 40%;">분야</th>
             <th>가중치</th>
             <th>등록일</th>
-            <th>관리</th>
+            <th>상태</th>
+            <th style="width: 120px;">관리</th>
           </tr>
         </thead>
         <tbody>
           <tr v-if="isLoading">
             <td colspan="6" class="admin--table-loading">데이터를 불러오는 중...</td>
           </tr>
-          <tr v-else-if="!branches || branches.length === 0">
+          <tr v-else-if="!fields || fields.length === 0">
             <td colspan="6" class="admin--table-empty">등록된 낚시분야가 없습니다.</td>
           </tr>
-          <tr v-else v-for="(branch, index) in branches" :key="branch.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td class="admin--table-title">{{ branch.name }}</td>
-            <!-- <td>{{ branch.main_phone }}</td>
-            <td>{{ branch.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': branch.is_active == 1 }"
-                @click="toggleActive(branch.id, branch.is_active)"
-              >
-                {{ branch.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(branch.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="deleteBranch(branch.id)"
-                >
-                  삭제
-                </button>
               </div>
             </td>
           </tr>
@@ -91,24 +97,12 @@
         다음
       </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",
@@ -116,63 +110,17 @@
   });
 
   const router = useRouter();
-  const { get, del, post } = useApi();
+  const { get } = useApi();
 
   const isLoading = ref(false);
-  const branches = ref([]);
+  const fields = 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 searchQuery = ref("");
+  const filterStatus = ref("");
 
   // 보이는 페이지 번호 계산
   const visiblePages = computed(() => {
@@ -184,105 +132,84 @@
     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 loadBranches = async () => {
+  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 });
 
-    console.log("[BranchList] API 응답:", { data, error });
-
-    // API 응답: { success: true, data: { items, total }, message }
-    if (data?.success && data?.data) {
-      branches.value = data.data.items || [];
+    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 = Math.ceil(totalCount.value / perPage.value);
-      console.log("[BranchList] 로드 성공:", branches.value.length);
+      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;
-    loadBranches();
+    loadFields();
     window.scrollTo({ top: 0, behavior: "smooth" });
   };
 
-  // 낚시분야 등록 페이지로 이동
-  const goToCreate = () => {
-    router.push("/site-manager/field/create");
-  };
-
-  // 낚시분야 수정 페이지로 이동
-  const goToEdit = (id) => {
-    router.push(`/site-manager/field/edit/${id}`);
-  };
-
-  // 낚시분야 삭제
-  const deleteBranch = (id) => {
-    showConfirm(
-      "정말 삭제하시겠습니까?",
-      async () => {
-        const { data, error } = await del(`/field/${id}`);
-
-        if (error || !data?.success) {
-          showAlert(error?.message || data?.message || "삭제에 실패했습니다.", "오류");
-        } else {
-          showAlert(data.message || "낚시분야가 삭제되었습니다.", "성공");
-          loadBranches();
-        }
-      },
-      "낚시분야 삭제"
-    );
-  };
-
-  // 사용/비사용 토글
-  const toggleActive = (id, currentStatus) => {
-    console.log("[toggleActive] 호출:", { id, currentStatus });
-    const statusText = currentStatus == 1 ? "비사용" : "사용";
-
-    showConfirm(
-      `낚시분야을 ${statusText} 상태로 변경하시겠습니까?`,
-      async () => {
-        console.log("[toggleActive] API 호출:", `/field/${id}/toggle-active`);
-
-        // POST 방식으로 변경 (PATCH 대신)
-        const { data, error } = await post(`/field/${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 || "낚시분야 상태가 변경되었습니다.", "성공");
-          loadBranches();
-        }
-      },
-      "상태 변경"
-    );
+  // 이동
+  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(() => {
-    loadBranches();
+    loadFields();
   });
-</script>
+</script>

+ 0 - 400
app/pages/site-manager/field/manager/create.vue

@@ -1,400 +0,0 @@
-<template>
-  <div class="admin--manager-form">
-    <form @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 지점명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">지점명 <span class="admin--required">*</span></label>
-        <select
-          v-model="formData.branch_id"
-          class="admin--form-select"
-          required
-        >
-          <option value="">지점을 선택하세요</option>
-          <option
-            v-for="branch in branches"
-            :key="branch.id"
-            :value="branch.id"
-            :disabled="branch.is_active !== 1 && branch.is_active !== '1'"
-          >
-            {{ branch.name }}{{ (branch.is_active !== 1 && branch.is_active !== '1') ? ' (비활성화)' : '' }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 아이디 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">아이디 <span class="admin--required">*</span></label>
-        <div class="admin--input-with-button">
-          <input
-            v-model="formData.user_id"
-            type="text"
-            class="admin--form-input"
-            placeholder="아이디를 입력하세요"
-            required
-            @input="handleUserIdInput"
-          >
-          <button
-            type="button"
-            class="admin--btn admin--btn-check"
-            @click="checkDuplicateUserId"
-            :disabled="!formData.user_id || isCheckingUserId"
-          >
-            {{ isCheckingUserId ? '확인 중...' : '중복 체크' }}
-          </button>
-        </div>
-        <p v-if="userIdCheckResult === 'available'" class="admin--form-help admin--text-success">
-          사용 가능한 아이디입니다.
-        </p>
-        <p v-if="userIdCheckResult === 'duplicate'" class="admin--form-help admin--text-error">
-          이미 사용 중인 아이디입니다.
-        </p>
-      </div>
-
-      <!-- 비밀번호 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">비밀번호 <span class="admin--required">*</span></label>
-        <div class="admin--password-input-wrapper">
-          <input
-            v-model="formData.password"
-            :type="showPassword ? 'text' : 'password'"
-            class="admin--form-input"
-            placeholder="비밀번호를 입력하세요"
-            required
-          >
-          <button
-            type="button"
-            class="admin--password-toggle"
-            @click="showPassword = !showPassword"
-          >
-            {{ showPassword ? '👁️' : '👁️‍🗨️' }}
-          </button>
-        </div>
-      </div>
-
-      <!-- 비밀번호 확인 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">비밀번호 확인 <span class="admin--required">*</span></label>
-        <div class="admin--password-input-wrapper">
-          <input
-            v-model="formData.password_confirm"
-            :type="showPasswordConfirm ? 'text' : 'password'"
-            class="admin--form-input"
-            placeholder="비밀번호를 다시 입력하세요"
-            required
-          >
-          <button
-            type="button"
-            class="admin--password-toggle"
-            @click="showPasswordConfirm = !showPasswordConfirm"
-          >
-            {{ showPasswordConfirm ? '👁️' : '👁️‍🗨️' }}
-          </button>
-        </div>
-      </div>
-
-      <!-- 관리자명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">관리자명 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="email@example.com"
-          required
-        >
-      </div>
-
-      <!-- 인사말 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">인사말</label>
-        <textarea
-          v-model="formData.greeting"
-          class="admin--form-textarea"
-          placeholder="인사말을 입력하세요 (엔터로 줄바꿈 가능)"
-          rows="5"
-        ></textarea>
-        <p class="admin--form-help">영업사원 목록 페이지 상단에 표시됩니다.</p>
-      </div>
-
-      <!-- 사진 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사진</label>
-        <input
-          type="file"
-          accept="image/*"
-          class="admin--form-file"
-          @change="handlePhotoUpload"
-        >
-        <div v-if="photoPreview" class="admin--image-preview">
-          <img :src="photoPreview" alt="미리보기">
-          <button type="button" class="admin--btn-remove-image" @click="removePhoto">
-            삭제
-          </button>
-        </div>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { get, post, upload } = useApi()
-const { getImageUrl } = useImage()
-
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const showPassword = ref(false)
-const showPasswordConfirm = ref(false)
-const branches = ref([])
-const photoPreview = ref(null)
-const photoFile = ref(null)
-const isCheckingUserId = ref(false)
-const userIdCheckResult = ref('') // 'available', 'duplicate', ''
-
-const formData = ref({
-  branch_id: '',
-  user_id: '',
-  password: '',
-  password_confirm: '',
-  name: '',
-  email: '',
-  greeting: '',
-  photo_url: ''
-})
-
-// 지점 목록 로드
-const loadBranches = async () => {
-  const { data, error } = await get('/branch/list', { per_page: 1000 })
-  console.log('[BranchManagerCreate] API 응답:', { data, error })
-
-  if (data?.success && data?.data) {
-    branches.value = data.data.items || []
-    console.log('[BranchManagerCreate] 지점 목록 로드 성공')
-  }
-}
-
-// 아이디 입력 시 중복 체크 결과 초기화
-const handleUserIdInput = () => {
-  userIdCheckResult.value = ''
-}
-
-// 아이디 중복 체크
-const checkDuplicateUserId = async () => {
-  if (!formData.value.user_id) {
-    alert('아이디를 입력하세요.')
-    return
-  }
-
-  isCheckingUserId.value = true
-  userIdCheckResult.value = ''
-
-  try {
-    const { data, error } = await get('/branch/manager/check-userid', {
-      params: {
-        user_id: formData.value.user_id
-      }
-    })
-
-    console.log('[BranchManagerCreate] 중복 체크 응답:', { data, error })
-
-    if (error) {
-      alert('중복 체크에 실패했습니다.')
-      return
-    }
-
-    if (data?.success) {
-      if (data?.data?.available) {
-        userIdCheckResult.value = 'available'
-      } else {
-        userIdCheckResult.value = 'duplicate'
-      }
-    }
-  } catch (error) {
-    console.error('중복 체크 오류:', error)
-    alert('중복 체크 중 오류가 발생했습니다.')
-  } finally {
-    isCheckingUserId.value = false
-  }
-}
-
-// 사진 업로드
-const handlePhotoUpload = (event) => {
-  const file = event.target.files[0]
-  if (!file) return
-
-  if (!file.type.startsWith('image/')) {
-    alert('이미지 파일만 업로드 가능합니다.')
-    return
-  }
-
-  photoFile.value = file
-
-  const reader = new FileReader()
-  reader.onload = (e) => {
-    photoPreview.value = e.target.result
-  }
-  reader.readAsDataURL(file)
-}
-
-// 사진 삭제
-const removePhoto = () => {
-  photoPreview.value = null
-  photoFile.value = null
-  formData.value.photo_url = ''
-}
-
-// 폼 제출
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  // 유효성 검사
-  if (!formData.value.branch_id) {
-    errorMessage.value = '지점을 선택하세요.'
-    return
-  }
-
-  if (!formData.value.user_id) {
-    errorMessage.value = '아이디를 입력하세요.'
-    return
-  }
-
-  if (userIdCheckResult.value !== 'available') {
-    errorMessage.value = '아이디 중복 체크를 해주세요.'
-    return
-  }
-
-  if (!formData.value.password) {
-    errorMessage.value = '비밀번호를 입력하세요.'
-    return
-  }
-
-  if (formData.value.password !== formData.value.password_confirm) {
-    errorMessage.value = '비밀번호가 일치하지 않습니다.'
-    return
-  }
-
-  if (!formData.value.name) {
-    errorMessage.value = '관리자명을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.email) {
-    errorMessage.value = '이메일을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let photoUrl = formData.value.photo_url
-
-    // 새 사진 업로드
-    if (photoFile.value) {
-      const formDataImage = new FormData()
-      formDataImage.append('file', photoFile.value)
-
-      const { data: uploadData, error: uploadError } = await upload('/upload/bmanager-image', formDataImage)
-
-      console.log('[BranchManagerCreate] 이미지 업로드 응답:', { data: uploadData, error: uploadError })
-
-      if (uploadError) {
-        errorMessage.value = '사진 업로드에 실패했습니다: ' + (uploadError.message || uploadError)
-        isSaving.value = false
-        return
-      }
-
-      if (!uploadData?.success || !uploadData?.data?.url) {
-        errorMessage.value = '사진 업로드 응답이 올바르지 않습니다.'
-        isSaving.value = false
-        return
-      }
-
-      photoUrl = uploadData.data.url
-      console.log('[BranchManagerCreate] 업로드된 이미지 URL:', photoUrl)
-    }
-
-    const submitData = {
-      branch_id: formData.value.branch_id,
-      user_id: formData.value.user_id,
-      password: formData.value.password,
-      name: formData.value.name,
-      email: formData.value.email,
-      greeting: formData.value.greeting,
-      photo_url: photoUrl
-    }
-
-    const { data, error } = await post('/branch/manager', submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '등록에 실패했습니다.'
-    } else {
-      successMessage.value = '지점장이 등록되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/branch/manager')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-// 목록으로 이동
-const goToList = () => {
-  router.push('/site-manager/branch/manager')
-}
-
-onMounted(() => {
-  loadBranches()
-})
-</script>

+ 0 - 362
app/pages/site-manager/field/manager/edit/[id].vue

@@ -1,362 +0,0 @@
-<template>
-  <div class="admin--manager-form">
-    <div v-if="isLoading" class="admin--loading">
-      데이터를 불러오는 중...
-    </div>
-
-    <form v-else @submit.prevent="handleSubmit" class="admin--form">
-      <!-- 지점명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">지점명 <span class="admin--required">*</span></label>
-        <select
-          v-model="formData.branch_id"
-          class="admin--form-select"
-          required
-        >
-          <option value="">지점을 선택하세요</option>
-          <option
-            v-for="branch in branches"
-            :key="branch.id"
-            :value="branch.id"
-            :disabled="branch.is_active !== 1 && branch.is_active !== '1'"
-          >
-            {{ branch.name }}{{ (branch.is_active !== 1 && branch.is_active !== '1') ? ' (비활성화)' : '' }}
-          </option>
-        </select>
-      </div>
-
-      <!-- 아이디 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">아이디 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.user_id"
-          type="text"
-          class="admin--form-input"
-          placeholder="아이디를 입력하세요"
-          required
-          disabled
-        >
-        <p class="admin--form-help">아이디는 수정할 수 없습니다.</p>
-      </div>
-
-      <!-- 비밀번호 (선택사항) -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">비밀번호</label>
-        <div class="admin--password-input-wrapper">
-          <input
-            v-model="formData.password"
-            :type="showPassword ? 'text' : 'password'"
-            class="admin--form-input"
-            placeholder="변경할 비밀번호를 입력하세요"
-          >
-          <button
-            type="button"
-            class="admin--password-toggle"
-            @click="showPassword = !showPassword"
-          >
-            {{ showPassword ? '👁️' : '👁️‍🗨️' }}
-          </button>
-        </div>
-        <p class="admin--form-help">비밀번호를 변경하지 않으려면 비워두세요.</p>
-      </div>
-
-      <!-- 비밀번호 확인 -->
-      <div v-if="formData.password" class="admin--form-group">
-        <label class="admin--form-label">비밀번호 확인 <span class="admin--required">*</span></label>
-        <div class="admin--password-input-wrapper">
-          <input
-            v-model="formData.password_confirm"
-            :type="showPasswordConfirm ? 'text' : 'password'"
-            class="admin--form-input"
-            placeholder="비밀번호를 다시 입력하세요"
-            required
-          >
-          <button
-            type="button"
-            class="admin--password-toggle"
-            @click="showPasswordConfirm = !showPasswordConfirm"
-          >
-            {{ showPasswordConfirm ? '👁️' : '👁️‍🗨️' }}
-          </button>
-        </div>
-      </div>
-
-      <!-- 관리자명 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">관리자명 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.name"
-          type="text"
-          class="admin--form-input"
-          placeholder="이름을 입력하세요"
-          required
-        >
-      </div>
-
-      <!-- 이메일 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">이메일 <span class="admin--required">*</span></label>
-        <input
-          v-model="formData.email"
-          type="email"
-          class="admin--form-input"
-          placeholder="email@example.com"
-          required
-        >
-      </div>
-
-      <!-- 인사말 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">인사말</label>
-        <textarea
-          v-model="formData.greeting"
-          class="admin--form-textarea"
-          placeholder="인사말을 입력하세요 (엔터로 줄바꿈 가능)"
-          rows="5"
-        ></textarea>
-        <p class="admin--form-help">영업사원 목록 페이지 상단에 표시됩니다.</p>
-      </div>
-
-      <!-- 사진 -->
-      <div class="admin--form-group">
-        <label class="admin--form-label">사진</label>
-        <input
-          type="file"
-          accept="image/*"
-          class="admin--form-file"
-          @change="handlePhotoUpload"
-        >
-        <div v-if="photoPreview || formData.photo_url" class="admin--image-preview">
-          <img :src="photoPreview || getImageUrl(formData.photo_url)" alt="미리보기">
-          <button type="button" class="admin--btn-remove-image" @click="removePhoto">
-            삭제
-          </button>
-        </div>
-      </div>
-
-      <!-- 버튼 영역 -->
-      <div class="admin--form-actions">
-        <button
-          type="submit"
-          class="admin--btn admin--btn-primary"
-          :disabled="isSaving"
-        >
-          {{ isSaving ? '저장 중...' : '확인' }}
-        </button>
-        <button
-          type="button"
-          class="admin--btn admin--btn-secondary"
-          @click="goToList"
-        >
-          목록
-        </button>
-      </div>
-
-      <!-- 성공/에러 메시지 -->
-      <div v-if="successMessage" class="admin--alert admin--alert-success">
-        {{ successMessage }}
-      </div>
-      <div v-if="errorMessage" class="admin--alert admin--alert-error">
-        {{ errorMessage }}
-      </div>
-    </form>
-  </div>
-</template>
-
-<script setup>
-import { ref, onMounted } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const route = useRoute()
-const router = useRouter()
-const { get, put, upload } = useApi()
-const { getImageUrl } = useImage()
-
-const isLoading = ref(true)
-const isSaving = ref(false)
-const successMessage = ref('')
-const errorMessage = ref('')
-const showPassword = ref(false)
-const showPasswordConfirm = ref(false)
-const branches = ref([])
-const photoPreview = ref(null)
-const photoFile = ref(null)
-
-const formData = ref({
-  branch_id: '',
-  user_id: '',
-  password: '',
-  password_confirm: '',
-  name: '',
-  email: '',
-  greeting: '',
-  photo_url: ''
-})
-
-// 지점 목록 로드
-const loadBranches = async () => {
-  const { data, error } = await get('/branch/list', { per_page: 1000 })
-  console.log('[BranchManagerEdit] API 응답:', { data, error })
-
-  if (data?.success && data?.data) {
-    branches.value = data.data.items || []
-    console.log('[BranchManagerEdit] 지점 목록 로드 성공')
-  }
-}
-
-// 데이터 로드
-const loadManager = async () => {
-  isLoading.value = true
-
-  const id = route.params.id
-  const { data, error } = await get(`/branch/manager/${id}`)
-  console.log('[BranchManagerEdit] 데이터 로드:', { data, error })
-
-  if (data?.success && data?.data) {
-    const manager = data.data
-    formData.value = {
-      branch_id: manager.branch_id || '',
-      user_id: manager.user_id || '',
-      password: '',
-      password_confirm: '',
-      name: manager.name || '',
-      email: manager.email || '',
-      greeting: manager.greeting || '',
-      photo_url: manager.photo_url || ''
-    }
-    photoPreview.value = null
-    console.log('[BranchManagerEdit] 로드 성공')
-  }
-
-  isLoading.value = false
-}
-
-// 사진 업로드
-const handlePhotoUpload = (event) => {
-  const file = event.target.files[0]
-  if (!file) return
-
-  if (!file.type.startsWith('image/')) {
-    alert('이미지 파일만 업로드 가능합니다.')
-    return
-  }
-
-  photoFile.value = file
-
-  const reader = new FileReader()
-  reader.onload = (e) => {
-    photoPreview.value = e.target.result
-  }
-  reader.readAsDataURL(file)
-}
-
-// 사진 삭제
-const removePhoto = () => {
-  photoPreview.value = null
-  photoFile.value = null
-  formData.value.photo_url = ''
-}
-
-// 폼 제출
-const handleSubmit = async () => {
-  successMessage.value = ''
-  errorMessage.value = ''
-
-  // 유효성 검사
-  if (!formData.value.branch_id) {
-    errorMessage.value = '지점을 선택하세요.'
-    return
-  }
-
-  if (formData.value.password && formData.value.password !== formData.value.password_confirm) {
-    errorMessage.value = '비밀번호가 일치하지 않습니다.'
-    return
-  }
-
-  if (!formData.value.name) {
-    errorMessage.value = '관리자명을 입력하세요.'
-    return
-  }
-
-  if (!formData.value.email) {
-    errorMessage.value = '이메일을 입력하세요.'
-    return
-  }
-
-  isSaving.value = true
-
-  try {
-    let photoUrl = formData.value.photo_url
-
-    // 새 사진 업로드
-    if (photoFile.value) {
-      const formDataImage = new FormData()
-      formDataImage.append('file', photoFile.value)
-
-      const { data: uploadData, error: uploadError } = await upload('/upload/bmanager-image', formDataImage)
-
-      console.log('[BranchManagerEdit] 이미지 업로드 응답:', { data: uploadData, error: uploadError })
-
-      if (uploadError) {
-        errorMessage.value = '사진 업로드에 실패했습니다: ' + (uploadError.message || uploadError)
-        isSaving.value = false
-        return
-      }
-
-      if (!uploadData?.success || !uploadData?.data?.url) {
-        errorMessage.value = '사진 업로드 응답이 올바르지 않습니다.'
-        isSaving.value = false
-        return
-      }
-
-      photoUrl = uploadData.data.url
-      console.log('[BranchManagerEdit] 업로드된 이미지 URL:', photoUrl)
-    }
-
-    const submitData = {
-      branch_id: formData.value.branch_id,
-      name: formData.value.name,
-      email: formData.value.email,
-      greeting: formData.value.greeting,
-      photo_url: photoUrl
-    }
-
-    // 비밀번호가 입력된 경우에만 포함
-    if (formData.value.password) {
-      submitData.password = formData.value.password
-    }
-
-    const id = route.params.id
-    const { data, error } = await put(`/branch/manager/${id}`, submitData)
-
-    if (error) {
-      errorMessage.value = error.message || '수정에 실패했습니다.'
-    } else {
-      successMessage.value = '지점장 정보가 수정되었습니다.'
-      setTimeout(() => {
-        router.push('/site-manager/branch/manager')
-      }, 1000)
-    }
-  } catch (error) {
-    errorMessage.value = '서버 오류가 발생했습니다.'
-    console.error('Save error:', error)
-  } finally {
-    isSaving.value = false
-  }
-}
-
-// 목록으로 이동
-const goToList = () => {
-  router.push('/site-manager/branch/manager')
-}
-
-onMounted(async () => {
-  await loadBranches()
-  await loadManager()
-})
-</script>

+ 0 - 257
app/pages/site-manager/field/manager/index.vue

@@ -1,257 +0,0 @@
-<template>
-  <div class="admin--manager-list">
-    <!-- 검색 영역 -->
-    <div class="admin--search-box">
-      <div class="admin--search-form">
-        <select v-model="searchType" class="admin--form-select admin--search-select">
-          <option value="branch_name">지점명</option>
-          <option value="name">이름</option>
-          <option value="user_id">아이디</option>
-          <option value="email">이메일</option>
-        </select>
-        <input
-          v-model="searchKeyword"
-          type="text"
-          class="admin--form-input admin--search-input"
-          placeholder="검색어를 입력하세요"
-          @keyup.enter="handleSearch"
-        >
-        <button class="admin--btn-small admin--btn-small-primary" @click="handleSearch">
-          검색
-        </button>
-        <button class="admin--btn-small admin--btn-small-secondary" @click="handleReset">
-          초기화
-        </button>
-      </div>
-      <div class="admin--search-actions">
-        <button class="admin--btn-small admin--btn-small-primary" @click="goToCreate">
-          + 지점장 등록
-        </button>
-      </div>
-    </div>
-
-    <!-- 테이블 -->
-    <div class="admin--table-wrapper">
-      <table class="admin--table">
-        <thead>
-          <tr>
-            <th>NO</th>
-            <th>지점명</th>
-            <th>이름</th>
-            <th>아이디</th>
-            <th>이메일</th>
-            <th>관리</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-if="!managers || managers.length === 0">
-            <td colspan="6" class="admin--table-empty">
-              등록된 지점장이 없습니다.
-            </td>
-          </tr>
-          <tr v-else v-for="(manager, index) in managers" :key="manager.id">
-            <td>{{ totalCount - ((currentPage - 1) * perPage + index) }}</td>
-            <td>{{ manager.branch_name }}</td>
-            <td class="admin--table-title">{{ manager.name }}</td>
-            <td>{{ manager.username }}</td>
-            <td>{{ manager.email }}</td>
-            <td>
-              <div class="admin--table-actions">
-                <button
-                  class="admin--btn-small admin--btn-small-primary"
-                  @click="goToEdit(manager.id)"
-                >
-                  수정
-                </button>
-                <button
-                  class="admin--btn-small admin--btn-small-danger"
-                  @click="handleDelete(manager.id)"
-                >
-                  삭제
-                </button>
-              </div>
-            </td>
-          </tr>
-        </tbody>
-      </table>
-    </div>
-
-    <!-- 페이지네이션 -->
-    <div v-if="totalPages > 1" class="admin--pagination">
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(1)"
-        title="처음"
-      >
-        ⏮
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === 1"
-        @click="changePage(currentPage - 1)"
-        title="이전"
-      >
-        ◀
-      </button>
-      <button
-        v-for="page in visiblePages"
-        :key="page"
-        class="admin--pagination-btn"
-        :class="{ 'is-active': page === currentPage }"
-        @click="changePage(page)"
-      >
-        {{ page }}
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(currentPage + 1)"
-        title="다음"
-      >
-        ▶
-      </button>
-      <button
-        class="admin--pagination-btn"
-        :disabled="currentPage === totalPages"
-        @click="changePage(totalPages)"
-        title="끝"
-      >
-        ⏭
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted } from 'vue'
-import { useRouter } from 'vue-router'
-
-definePageMeta({
-  layout: 'admin',
-  middleware: ['auth']
-})
-
-const router = useRouter()
-const { get, del } = useApi()
-const managers = ref([])
-const searchType = ref('branch_name')
-const searchKeyword = ref('')
-const currentPage = ref(1)
-const perPage = ref(10)
-const totalCount = ref(0)
-const totalPages = ref(0)
-
-// 보이는 페이지 번호 계산
-const visiblePages = computed(() => {
-  const pages = []
-  const maxVisible = 5
-  let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2))
-  let end = Math.min(totalPages.value, start + maxVisible - 1)
-
-  if (end - start < maxVisible - 1) {
-    start = Math.max(1, end - maxVisible + 1)
-  }
-
-  for (let i = start; i <= end; i++) {
-    pages.push(i)
-  }
-
-  return pages
-})
-
-// 데이터 로드
-const loadManagers = async () => {
-  const params = {
-    page: currentPage.value,
-    per_page: perPage.value
-  }
-
-  if (searchKeyword.value) {
-    params.search_type = searchType.value
-    params.search_keyword = searchKeyword.value
-  }
-
-  const { data, error } = await get('/branch/manager', { params })
-
-  console.log('[BranchManager] API 응답:', { data, error })
-
-  // API 응답: { success: true, data: { items, total }, message }
-  if (data?.success && data?.data) {
-    managers.value = data.data.items || []
-    totalCount.value = data.data.total || 0
-    totalPages.value = Math.ceil(totalCount.value / perPage.value)
-    console.log('[BranchManager] 로드 성공:', managers.value.length)
-  }
-}
-
-// 검색
-const handleSearch = () => {
-  currentPage.value = 1
-  loadManagers()
-}
-
-// 초기화
-const handleReset = () => {
-  searchType.value = 'branch_name'
-  searchKeyword.value = ''
-  currentPage.value = 1
-  loadManagers()
-}
-
-// 페이지 변경
-const changePage = (page) => {
-  if (page < 1 || page > totalPages.value) return
-  currentPage.value = page
-  loadManagers()
-  window.scrollTo({ top: 0, behavior: 'smooth' })
-}
-
-// 등록 페이지로 이동
-const goToCreate = () => {
-  router.push('/site-manager/branch/manager/create')
-}
-
-// 수정 페이지로 이동
-const goToEdit = (id) => {
-  router.push(`/site-manager/branch/manager/edit/${id}`)
-}
-
-// 삭제
-const handleDelete = async (id) => {
-  if (!confirm('정말 삭제하시겠습니까?')) return
-
-  const { error } = await del(`/branch/manager/${id}`)
-
-  if (error) {
-    alert('삭제에 실패했습니다.')
-  } else {
-    alert('삭제되었습니다.')
-    loadManagers()
-  }
-}
-
-onMounted(() => {
-  loadManagers()
-})
-</script>
-
-<style scoped>
-.admin--search-actions .admin--btn-primary {
-  background: var(--admin-accent-primary);
-  color: white;
-  border-color: var(--admin-accent-primary);
-  font-weight: 500;
-  padding: 8px 18px;
-  font-size: 13px;
-  border-radius: 8px;
-  transition: all 0.3s ease;
-}
-
-.admin--search-actions .admin--btn-primary:hover {
-  background: var(--admin-accent-hover);
-  border-color: var(--admin-accent-hover);
-  transform: translateY(-1px);
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-</style>

+ 1 - 1
backend/app/Config/App.php

@@ -133,7 +133,7 @@ class App extends BaseConfig
      * @see https://www.php.net/manual/en/timezones.php for list of timezones
      *      supported by PHP.
      */
-    public string $appTimezone = 'UTC';
+    public string $appTimezone = 'Asia/Seoul';
 
     /**
      * --------------------------------------------------------------------------

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

@@ -29,6 +29,13 @@ $routes->delete('api/admin/(:num)', 'Api\AdminController::delete/$1');
 $routes->post('api/admin/(:num)/password', 'Api\AdminController::changePassword/$1');
 $routes->post('api/admin/(:num)/unlock', 'Api\AdminController::unlockAccount/$1');
 
+// Fishing Field (낚시분야)
+$routes->get('api/field/list', 'Api\FishingFieldController::index');
+$routes->get('api/field/(:num)', 'Api\FishingFieldController::show/$1');
+$routes->post('api/field', 'Api\FishingFieldController::create');
+$routes->put('api/field/(:num)', 'Api\FishingFieldController::update/$1');
+$routes->delete('api/field/(:num)', 'Api\FishingFieldController::delete/$1');
+
 // File Upload
 $routes->post('api/upload/file', 'Api\UploadController::uploadFile');
 $routes->post('api/upload/image', 'Api\UploadController::uploadImage');

+ 290 - 0
backend/app/Controllers/Api/FishingFieldController.php

@@ -0,0 +1,290 @@
+<?php
+
+namespace App\Controllers\Api;
+
+use CodeIgniter\HTTP\ResponseInterface;
+
+class FishingFieldController extends BaseApiController
+{
+    protected $format = 'json';
+    protected $table = 'fishing_field';
+
+    /**
+     * 낚시분야 목록
+     * GET /api/field/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')); // 'Y' or 'N' 필터 (선택)
+
+            $db = $this->getDB();
+            $builder = $db->table($this->table);
+
+            // soft delete 제외
+            $builder->where('deleted_YN', 'N');
+
+            if ($search !== '') {
+                $builder->like('name', $search);
+            }
+            if ($status === 'Y' || $status === 'N') {
+                $builder->where('status_YN', $status);
+            }
+
+            $total = $builder->countAllResults(false);
+
+            $items = $builder
+                ->select('id, name, weight, status_YN, created_at, updated_at')
+                ->orderBy('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', 'FishingFieldController index error: ' . $e->getMessage());
+            return $this->respondError('목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 낚시분야 등록
+     * POST /api/field
+     */
+    public function create()
+    {
+        $auth = $this->requireAuth();
+        if ($auth instanceof ResponseInterface) {
+            return $auth;
+        }
+
+        try {
+            // JSON 요청 우선, fallback 으로 form-encoded 도 허용
+            $payload = $this->request->getJSON(true);
+            if (!is_array($payload) || empty($payload)) {
+                $payload = $this->request->getPost() ?? [];
+            }
+
+            $name = trim((string) ($payload['name'] ?? ''));
+            $weightRaw = trim((string) ($payload['weight'] ?? ''));
+
+            // 분야명 검증: 1~30자, 필수
+            if ($name === '') {
+                return $this->respondError('분야명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+            if (mb_strlen($name) > 30) {
+                return $this->respondError('분야명은 30자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            // 가중치 검증: 0.0 ~ 1.0, 소수점 1자리
+            if ($weightRaw === '' || !preg_match('/^(0(\.\d)?|1(\.0)?)$/', $weightRaw)) {
+                return $this->respondError('가중치는 0.0 ~ 1.0 사이 소수점 1자리로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            // 소수점 1자리로 정규화 (예: "1" → "1.0", ".5" → "0.5")
+            $weight = number_format((float) $weightRaw, 1, '.', '');
+
+            $db = $this->getDB();
+            $builder = $db->table($this->table);
+
+            // 중복 검사
+            $exists = $builder->where('name', $name)->countAllResults();
+            if ($exists > 0) {
+                return $this->respondError('이미 등록된 분야명입니다.', ResponseInterface::HTTP_CONFLICT);
+            }
+
+            $now = date('Y-m-d H:i:s');
+            $insertData = [
+                'name'       => $name,
+                'weight'     => $weight,
+                'created_at' => $now,
+            ];
+
+            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();
+
+            return $this->respondSuccess($row, '낚시분야가 등록되었습니다.', ResponseInterface::HTTP_CREATED);
+        } catch (\Exception $e) {
+            log_message('error', 'FishingFieldController create error: ' . $e->getMessage());
+            return $this->respondError('등록 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 낚시분야 상세 조회
+     * GET /api/field/: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)
+                ->select('id, name, weight, status_YN, created_at, updated_at')
+                ->where('id', (int) $id)
+                ->where('deleted_YN', 'N')
+                ->get()
+                ->getRow();
+
+            if (!$row) {
+                return $this->respondError('해당 낚시분야를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
+            }
+
+            return $this->respondSuccess($row);
+        } catch (\Exception $e) {
+            log_message('error', 'FishingFieldController show error: ' . $e->getMessage());
+            return $this->respondError('조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 낚시분야 수정
+     * PUT /api/field/:id
+     */
+    public function update($id = null)
+    {
+        $auth = $this->requireAuth();
+        if ($auth instanceof ResponseInterface) {
+            return $auth;
+        }
+
+        if (empty($id)) {
+            return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
+        }
+
+        try {
+            $payload = $this->request->getJSON(true);
+            if (!is_array($payload) || empty($payload)) {
+                $payload = $this->request->getRawInput() ?? [];
+            }
+
+            $name = trim((string) ($payload['name'] ?? ''));
+            $weightRaw = trim((string) ($payload['weight'] ?? ''));
+            $status = trim((string) ($payload['status_YN'] ?? 'Y'));
+
+            // 분야명 검증
+            if ($name === '') {
+                return $this->respondError('분야명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+            if (mb_strlen($name) > 30) {
+                return $this->respondError('분야명은 30자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            // 가중치 검증
+            if ($weightRaw === '' || !preg_match('/^(0(\.\d)?|1(\.0)?)$/', $weightRaw)) {
+                return $this->respondError('가중치는 0.0 ~ 1.0 사이 소수점 1자리로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+            $weight = number_format((float) $weightRaw, 1, '.', '');
+
+            // 상태 검증
+            if ($status !== 'Y' && $status !== 'N') {
+                return $this->respondError('상태값이 올바르지 않습니다.', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $db = $this->getDB();
+            $builder = $db->table($this->table);
+
+            // 대상 행 존재 확인
+            $exists = $builder->where('id', (int) $id)->where('deleted_YN', 'N')->countAllResults(false);
+            if ($exists === 0) {
+                return $this->respondError('해당 낚시분야를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
+            }
+            $builder->resetQuery();
+
+            // 중복 검사 (자기 자신 제외)
+            $dupe = $db->table($this->table)
+                ->where('name', $name)
+                ->where('id !=', (int) $id)
+                ->where('deleted_YN', 'N')
+                ->countAllResults();
+            if ($dupe > 0) {
+                return $this->respondError('이미 등록된 분야명입니다.', ResponseInterface::HTTP_CONFLICT);
+            }
+
+            $updateData = [
+                'name'       => $name,
+                'weight'     => $weight,
+                'status_YN'  => $status,
+                'updated_at' => date('Y-m-d H:i:s'),
+            ];
+
+            $db->table($this->table)->where('id', (int) $id)->update($updateData);
+
+            $row = $db->table($this->table)->where('id', (int) $id)->get()->getRow();
+            return $this->respondSuccess($row, '낚시분야가 수정되었습니다.');
+        } catch (\Exception $e) {
+            log_message('error', 'FishingFieldController update error: ' . $e->getMessage());
+            return $this->respondError('수정 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * 낚시분야 삭제 (soft delete)
+     * DELETE /api/field/: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', 'FishingFieldController delete error: ' . $e->getMessage());
+            return $this->respondError('삭제 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+}

+ 54 - 10
db.vuerd.json

@@ -5,7 +5,7 @@
     "width": 2000,
     "height": 2000,
     "scrollTop": 0,
-    "scrollLeft": -24,
+    "scrollLeft": -23,
     "zoomLevel": 1,
     "show": 431,
     "database": 4,
@@ -49,14 +49,18 @@
           "ZohCM1jv0ehj-396ANamu",
           "cKDPY2OPmJguD0EaEO6AK",
           "LTAAwKOsboOXo0QloDDKe",
-          "ynAe4Kk4ZBjsyu2n7LhYc"
+          "ynAe4Kk4ZBjsyu2n7LhYc",
+          "xpp-hXcIZidnwzPwrdRBc",
+          "4hL25cYYWJqKXtSE-Ko-J"
         ],
         "seqColumnIds": [
           "tVtTK-A1GhU8WRdiZT89G",
           "ZohCM1jv0ehj-396ANamu",
           "cKDPY2OPmJguD0EaEO6AK",
           "LTAAwKOsboOXo0QloDDKe",
-          "ynAe4Kk4ZBjsyu2n7LhYc"
+          "ynAe4Kk4ZBjsyu2n7LhYc",
+          "xpp-hXcIZidnwzPwrdRBc",
+          "4hL25cYYWJqKXtSE-Ko-J"
         ],
         "ui": {
           "x": 117,
@@ -67,7 +71,7 @@
           "color": ""
         },
         "meta": {
-          "updateAt": 1779855737252,
+          "updateAt": 1779862680279,
           "createAt": 1779771222669
         }
       },
@@ -157,18 +161,18 @@
         "tableId": "FISm81kEr0UVuk9sKYz4j",
         "name": "created_at",
         "comment": "",
-        "dataType": "DATETIME",
+        "dataType": "TIMESTAMP",
         "default": "",
         "options": 8,
         "ui": {
           "keys": 0,
           "widthName": 60,
           "widthComment": 60,
-          "widthDataType": 60,
+          "widthDataType": 65,
           "widthDefault": 60
         },
         "meta": {
-          "updateAt": 1779850324785,
+          "updateAt": 1779861453680,
           "createAt": 1779850233419
         }
       },
@@ -177,20 +181,60 @@
         "tableId": "FISm81kEr0UVuk9sKYz4j",
         "name": "updated_at",
         "comment": "",
-        "dataType": "DATETIME",
+        "dataType": "TIMESTAMP",
         "default": "",
         "options": 0,
         "ui": {
           "keys": 0,
           "widthName": 62,
           "widthComment": 60,
-          "widthDataType": 60,
+          "widthDataType": 65,
           "widthDefault": 60
         },
         "meta": {
-          "updateAt": 1779850322708,
+          "updateAt": 1779861459518,
           "createAt": 1779850242123
         }
+      },
+      "xpp-hXcIZidnwzPwrdRBc": {
+        "id": "xpp-hXcIZidnwzPwrdRBc",
+        "tableId": "FISm81kEr0UVuk9sKYz4j",
+        "name": "status_YN",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "'Y'",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 60,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779862679108,
+          "createAt": 1779862661804
+        }
+      },
+      "4hL25cYYWJqKXtSE-Ko-J": {
+        "id": "4hL25cYYWJqKXtSE-Ko-J",
+        "tableId": "FISm81kEr0UVuk9sKYz4j",
+        "name": "deleted_YN",
+        "comment": "",
+        "dataType": "VARCHAR",
+        "default": "'N'",
+        "options": 8,
+        "ui": {
+          "keys": 0,
+          "widthName": 63,
+          "widthComment": 60,
+          "widthDataType": 60,
+          "widthDefault": 60
+        },
+        "meta": {
+          "updateAt": 1779862694090,
+          "createAt": 1779862680279
+        }
       }
     },
     "relationshipEntities": {},