|
|
@@ -1,337 +1,507 @@
|
|
|
<template>
|
|
|
- <div class="admin--showroom-form">
|
|
|
+ <div class="admin--page-content">
|
|
|
<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: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">
|
|
|
- + 링크 추가
|
|
|
+ <div v-else 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">
|
|
|
+ <input
|
|
|
+ ref="photoInput"
|
|
|
+ type="file"
|
|
|
+ accept="image/*"
|
|
|
+ multiple
|
|
|
+ class="admin--form-file-hidden"
|
|
|
+ @change="onPhotoChange"
|
|
|
+ />
|
|
|
+ <button type="button" class="admin--btn-small admin--btn-blue" @click="triggerPhotoInput">
|
|
|
+ 사진 추가
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <p class="mt--10">JPG/PNG/WebP, 한 장당 10MB 이하. 첫 번째 사진이 대표 이미지로 사용됩니다.</p>
|
|
|
+ <div v-if="existingPhotos.length || photos.length" class="onboard--photo-grid mt--10">
|
|
|
+ <!-- 기존 사진 -->
|
|
|
+ <div v-for="p in existingPhotos" :key="'e' + p.id" class="onboard--photo-item">
|
|
|
+ <img :src="getImageUrl(p.file_path)" :alt="p.original_name" />
|
|
|
+ <button type="button" class="onboard--photo-remove" @click="removeExistingPhoto(p.id)"></button>
|
|
|
+ </div>
|
|
|
+ <!-- 새로 추가한 사진 -->
|
|
|
+ <div v-for="(p, i) in photos" :key="'n' + p.id" class="onboard--photo-item">
|
|
|
+ <img :src="p.preview" alt="미리보기" />
|
|
|
+ <button type="button" class="onboard--photo-remove" @click="removePhoto(i)"></button>
|
|
|
+ <span class="onboard--photo-new">NEW</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </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="goToDetail">
|
|
|
+ ← 취소
|
|
|
+ </button>
|
|
|
+ <button type="submit" class="admin--btn admin--btn-red ml--auto" :disabled="isSaving">
|
|
|
+ {{ isSaving ? "저장 중..." : "저장" }}
|
|
|
</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 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>
|
|
|
+
|
|
|
+ <!-- 알림 모달 -->
|
|
|
+ <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, onMounted } from "vue";
|
|
|
- import { useRouter, useRoute } from "vue-router";
|
|
|
+ import { ref, computed, watch, onMounted } from "vue";
|
|
|
+ import { useRoute, useRouter } from "vue-router";
|
|
|
+ import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
|
|
|
|
|
|
definePageMeta({
|
|
|
layout: "admin",
|
|
|
middleware: ["auth"],
|
|
|
});
|
|
|
|
|
|
- const router = useRouter();
|
|
|
const route = useRoute();
|
|
|
- const { get, put } = useApi();
|
|
|
+ const router = useRouter();
|
|
|
+ const config = useRuntimeConfig();
|
|
|
+ const { get, put, upload, del } = useApi();
|
|
|
+ const { getImageUrl } = useImage();
|
|
|
+
|
|
|
+ const onboardId = route.params.id;
|
|
|
|
|
|
const isLoading = ref(true);
|
|
|
const isSaving = ref(false);
|
|
|
const successMessage = ref("");
|
|
|
const errorMessage = ref("");
|
|
|
- const branches = ref([]);
|
|
|
+ const coordError = ref("");
|
|
|
+
|
|
|
+ // 분야 / 지역 select 옵션
|
|
|
+ const fieldOptions = ref([]);
|
|
|
+ const areaOptions = ref([]);
|
|
|
+
|
|
|
+ // 사진
|
|
|
+ const photoInput = ref(null);
|
|
|
+ const photos = ref([]); // 새로 추가: { id, file, preview }
|
|
|
+ const existingPhotos = ref([]); // 기존: { id, file_path, original_name, ... }
|
|
|
+ const MAX_PHOTO_SIZE = 10 * 1024 * 1024;
|
|
|
+
|
|
|
+ // 주요 은행 목록
|
|
|
+ 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: "",
|
|
|
- branch_id: "",
|
|
|
- phone: "",
|
|
|
+ area_detail: "",
|
|
|
+ tonnage: "",
|
|
|
+ capacity: "",
|
|
|
+ zip_code: "",
|
|
|
address: "",
|
|
|
- detail_address: "",
|
|
|
- latitude: null,
|
|
|
- longitude: null,
|
|
|
- business_hours: "",
|
|
|
- quote_link: "",
|
|
|
- test_drive_link: "",
|
|
|
- links: [],
|
|
|
+ address_detail: "",
|
|
|
+ address_refer: "",
|
|
|
+ lat: "",
|
|
|
+ lng: "",
|
|
|
+ bank_code: "",
|
|
|
+ account_number: "",
|
|
|
+ account_holder: "",
|
|
|
+ partnership_YN: "N",
|
|
|
+ status_YN: "Y",
|
|
|
});
|
|
|
|
|
|
- // 지점 목록 로드
|
|
|
- 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}`);
|
|
|
+ const isPartner = computed(() => formData.value.partnership_YN === "Y");
|
|
|
|
|
|
- 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 : [""],
|
|
|
- };
|
|
|
+ // 비제휴 전환 시 계좌 정보 초기화
|
|
|
+ watch(
|
|
|
+ () => formData.value.partnership_YN,
|
|
|
+ (val) => {
|
|
|
+ if (val === "N") {
|
|
|
+ formData.value.bank_code = "";
|
|
|
+ formData.value.account_number = "";
|
|
|
+ formData.value.account_holder = "";
|
|
|
+ }
|
|
|
}
|
|
|
+ );
|
|
|
|
|
|
- isLoading.value = false;
|
|
|
+ // 알림 모달
|
|
|
+ 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 addLink = () => {
|
|
|
- formData.value.links.push("");
|
|
|
+ const showConfirm = (message, onConfirm, title = "확인") => {
|
|
|
+ alertModal.value = { show: true, title, message, type: "confirm", onConfirm };
|
|
|
};
|
|
|
-
|
|
|
- // 링크 삭제
|
|
|
- const removeLink = (index) => {
|
|
|
- formData.value.links.splice(index, 1);
|
|
|
+ const closeAlertModal = () => { alertModal.value.show = false; };
|
|
|
+ const handleAlertConfirm = () => {
|
|
|
+ if (alertModal.value.onConfirm) alertModal.value.onConfirm();
|
|
|
+ closeAlertModal();
|
|
|
+ };
|
|
|
+ const handleAlertCancel = () => closeAlertModal();
|
|
|
+
|
|
|
+ // 분야 / 지역 옵션 로드
|
|
|
+ const loadOptions = async () => {
|
|
|
+ const [fieldRes, areaRes] = await Promise.all([
|
|
|
+ get("/field/list", { params: { per_page: 1000 } }),
|
|
|
+ get("/area/list", { params: { per_page: 1000 } }),
|
|
|
+ ]);
|
|
|
+ if (fieldRes.data?.success) fieldOptions.value = (fieldRes.data.data.items || []).reverse();
|
|
|
+ if (areaRes.data?.success) areaOptions.value = (areaRes.data.data.items || []).reverse();
|
|
|
};
|
|
|
|
|
|
- // 폼 제출
|
|
|
- const handleSubmit = async () => {
|
|
|
- successMessage.value = "";
|
|
|
- errorMessage.value = "";
|
|
|
-
|
|
|
- // 유효성 검사
|
|
|
- if (!formData.value.name) {
|
|
|
- errorMessage.value = "전시장명을 입력하세요.";
|
|
|
+ // 기존 데이터 로드
|
|
|
+ const loadDetail = async () => {
|
|
|
+ const { data, error } = await get(`/onboard/${onboardId}`);
|
|
|
+ if (error || !data?.success) {
|
|
|
+ errorMessage.value = error?.message || data?.message || "조회에 실패했습니다.";
|
|
|
+ isLoading.value = false;
|
|
|
return;
|
|
|
}
|
|
|
+ const row = data.data || {};
|
|
|
+ formData.value = {
|
|
|
+ field_id: row.field_id ?? "",
|
|
|
+ area_id: row.area_id ?? "",
|
|
|
+ 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",
|
|
|
+ };
|
|
|
+ existingPhotos.value = row.photos ?? [];
|
|
|
+ isLoading.value = false;
|
|
|
+ };
|
|
|
|
|
|
- if (!formData.value.branch_id) {
|
|
|
- errorMessage.value = "소속 지점을 선택하세요.";
|
|
|
+ // 외부 스크립트 로드
|
|
|
+ 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)
|
|
|
+ const searchCoords = async (address) => {
|
|
|
+ coordError.value = "";
|
|
|
+ const key = config.public.googleMapKey;
|
|
|
+ if (!key) {
|
|
|
+ coordError.value = "좌표를 자동으로 가져올 수 없습니다. 위도/경도를 직접 입력하세요.";
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- if (!formData.value.phone) {
|
|
|
- errorMessage.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);
|
|
|
+ coordError.value = "좌표 조회 중 오류가 발생했습니다. 위도/경도를 직접 입력해주세요.";
|
|
|
}
|
|
|
+ };
|
|
|
|
|
|
- if (!formData.value.address) {
|
|
|
- errorMessage.value = "주소를 입력하세요.";
|
|
|
+ // 우편번호 검색
|
|
|
+ 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();
|
|
|
+ };
|
|
|
|
|
|
- isSaving.value = true;
|
|
|
+ // 사진 추가/삭제
|
|
|
+ const triggerPhotoInput = () => photoInput.value?.click();
|
|
|
+ const onPhotoChange = (e) => {
|
|
|
+ const files = Array.from(e.target.files || []);
|
|
|
+ for (const file of files) {
|
|
|
+ if (!file.type.startsWith("image/")) continue;
|
|
|
+ if (file.size > MAX_PHOTO_SIZE) {
|
|
|
+ errorMessage.value = `'${file.name}' 파일이 10MB를 초과합니다.`;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ photos.value.push({
|
|
|
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
|
+ file,
|
|
|
+ preview: URL.createObjectURL(file),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ e.target.value = "";
|
|
|
+ };
|
|
|
+ const removePhoto = (index) => {
|
|
|
+ const [removed] = photos.value.splice(index, 1);
|
|
|
+ if (removed) URL.revokeObjectURL(removed.preview);
|
|
|
+ };
|
|
|
|
|
|
- try {
|
|
|
- const id = route.params.id;
|
|
|
+ // 기존 사진 삭제 (즉시 서버 반영)
|
|
|
+ const removeExistingPhoto = (photoId) => {
|
|
|
+ showConfirm(
|
|
|
+ "이 사진을 삭제하시겠습니까?",
|
|
|
+ async () => {
|
|
|
+ const { data, error } = await del(`/onboard/photo/${photoId}`);
|
|
|
+ if (error || !data?.success) {
|
|
|
+ showAlert(error?.message || data?.message || "사진 삭제에 실패했습니다.", "오류");
|
|
|
+ } else {
|
|
|
+ existingPhotos.value = existingPhotos.value.filter((p) => p.id !== photoId);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ "사진 삭제"
|
|
|
+ );
|
|
|
+ };
|
|
|
|
|
|
- // 빈 링크 제거
|
|
|
- const submitData = {
|
|
|
- ...formData.value,
|
|
|
- links: formData.value.links.filter((link) => link.trim() !== ""),
|
|
|
- };
|
|
|
+ // 폼 제출
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ successMessage.value = "";
|
|
|
+ errorMessage.value = "";
|
|
|
|
|
|
- const { data, error } = await put(`/showroom/${id}`, submitData);
|
|
|
+ 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 {
|
|
|
+ // 1) 선상 수정
|
|
|
+ const { data, error } = await put(`/onboard/${onboardId}`, { ...formData.value });
|
|
|
if (error || !data?.success) {
|
|
|
errorMessage.value = error?.message || data?.message || "수정에 실패했습니다.";
|
|
|
- } else {
|
|
|
- successMessage.value = data.message || "전시장이 수정되었습니다.";
|
|
|
- setTimeout(() => {
|
|
|
- router.push("/site-manager/showroom/list");
|
|
|
- }, 1000);
|
|
|
+ return;
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
+
|
|
|
+ // 2) 새 사진이 있으면 업로드
|
|
|
+ if (photos.value.length) {
|
|
|
+ const fd = new FormData();
|
|
|
+ photos.value.forEach((p) => fd.append("photos[]", p.file));
|
|
|
+ const { data: photoRes, error: photoErr } = await upload(`/onboard/${onboardId}/photos`, fd);
|
|
|
+ if (photoErr || !photoRes?.success) {
|
|
|
+ errorMessage.value = "선상은 수정됐지만 사진 업로드에 실패했습니다.";
|
|
|
+ setTimeout(() => router.push(`/site-manager/onboard/detail/${onboardId}`), 1500);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ successMessage.value = data.message || "수정되었습니다.";
|
|
|
+ setTimeout(() => {
|
|
|
+ router.push(`/site-manager/onboard/detail/${onboardId}`);
|
|
|
+ }, 1000);
|
|
|
+ } catch (e) {
|
|
|
errorMessage.value = "서버 오류가 발생했습니다.";
|
|
|
- console.error("Save error:", error);
|
|
|
+ console.error("Update error:", e);
|
|
|
} finally {
|
|
|
isSaving.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- // 목록으로 이동
|
|
|
- const goToList = () => {
|
|
|
- router.push("/site-manager/showroom/list");
|
|
|
- };
|
|
|
+ // 이동
|
|
|
+ const goToDetail = () => router.push(`/site-manager/onboard/detail/${onboardId}`);
|
|
|
|
|
|
onMounted(async () => {
|
|
|
- await loadBranches();
|
|
|
- await loadShowroom();
|
|
|
+ await loadOptions();
|
|
|
+ await 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>
|
|
|
+</script>
|