| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- <template>
- <div class="admin--page-content">
- <div class="admin--form">
- <form @submit.prevent="handleSubmit">
- <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="예: 등급 포인트 +100"
- required
- />
- </div>
- </td>
- </tr>
- <tr>
- <th><div>구분 <span class="admin--required">*</span></div></th>
- <td>
- <div class="input--wrap">
- <label class="admin--radio-label">
- <input type="radio" v-model="formData.type" value="T" /> 진출권
- </label>
- <label class="admin--radio-label ml--16">
- <input type="radio" v-model="formData.type" value="P" /> 포인트
- </label>
- <label class="admin--radio-label ml--16">
- <input type="radio" v-model="formData.type" value="B" /> 뱃지
- </label>
- </div>
- </td>
- </tr>
- <tr v-if="formData.type !== 'T'">
- <th>
- <div>
- 포인트 <span v-if="formData.type === 'P'" class="admin--required">*</span>
- </div>
- </th>
- <td>
- <div class="input--wrap">
- <input
- v-model.number="formData.point"
- type="number"
- min="0"
- class="admin--form-input w--200"
- placeholder="예: 100"
- />
- </div>
- <p class="mt--10" v-if="formData.type === 'P'">아이템 구분이 "포인트"인 경우 필수 입력</p>
- <p class="mt--10" v-else>아이템 구분이 "뱃지"인 경우 선택 입력</p>
- </td>
- </tr>
- <tr>
- <th><div>이미지</div></th>
- <td>
- <div class="input--wrap">
- <input
- ref="imageInput"
- type="file"
- accept="image/*"
- class="admin--form-file-hidden"
- @change="onImageChange"
- />
- <button type="button" class="admin--btn-small admin--btn-blue" @click="triggerImageInput">
- 이미지 선택
- </button>
- <span v-if="image" class="ml--16">{{ image.file.name }}</span>
- </div>
- <p class="mt--10">JPG/PNG/GIF/WebP, 10MB 이하 (선택 사항)</p>
- <div v-if="image" class="onboard--photo-grid mt--10">
- <div class="onboard--photo-item">
- <img :src="image.preview" alt="미리보기" />
- <button type="button" class="onboard--photo-remove" @click="removeImage"></button>
- </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.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 bg--white">
- <h3>💡 아이템 구분별 지급 조건</h3>
- <div class="item--info--box">
- <div class="item--info">
- <span class="item--badge">진출권</span>
- <p>챌린지 진행 시 사용자가 조건의 물고기를 획득하면 다음 라운드에 진출할 수 있는 아이템</p>
- <span>ㆍ포인트 필드 미사용</span>
- </div>
- <div class="item--info">
- <span class="item--badge">포인트</span>
- <p>챌린지에서 조건의 물고기를 획득하면 지급. 사용자 등급 포인트를 지급해주는 아이템</p>
- <span>ㆍ포인트 필드 필수</span>
- </div>
- <div class="item--info">
- <span class="item--badge">뱃지</span>
- <p>퀘스트 진행 시 조건 달성한 사용자에게 지급되는 아이템</p>
- <span>ㆍ포인트 필드 사용 가능</span>
- </div>
- </div>
- </div>
- <!-- 버튼 영역 -->
- <div class="admin--form-actions">
- <button type="button" class="admin--btn" @click="goToList">
- ← 목록으로
- </button>
- <button type="submit" class="admin--btn admin--btn-red ml--auto" :disabled="isSaving">
- {{ isSaving ? "저장 중..." : "저장" }}
- </button>
- </div>
- <!-- 성공/에러 메시지 -->
- <div v-if="successMessage" class="admin--alert admin--alert-success">
- {{ successMessage }}
- </div>
- <div v-if="errorMessage" class="admin--alert admin--alert-error">
- {{ errorMessage }}
- </div>
- </form>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, watch } from "vue";
- import { useRouter } from "vue-router";
- definePageMeta({
- layout: "admin",
- middleware: ["auth"],
- });
- const router = useRouter();
- const { upload } = useApi();
- const isSaving = ref(false);
- const successMessage = ref("");
- const errorMessage = ref("");
- const formData = ref({
- name: "",
- type: "T", // T(진출권) / P(포인트) / B(뱃지)
- point: null,
- status_YN: "Y",
- });
- // 이미지 1장 — { file, preview }
- const imageInput = ref(null);
- const image = ref(null);
- const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
- const triggerImageInput = () => imageInput.value?.click();
- const onImageChange = (e) => {
- const file = (e.target.files || [])[0];
- e.target.value = "";
- if (!file) return;
- if (!file.type.startsWith("image/")) {
- errorMessage.value = "이미지 파일만 업로드할 수 있습니다.";
- return;
- }
- if (file.size > MAX_IMAGE_SIZE) {
- errorMessage.value = "이미지가 10MB를 초과합니다.";
- return;
- }
- if (image.value) URL.revokeObjectURL(image.value.preview);
- image.value = { file, preview: URL.createObjectURL(file) };
- };
- const removeImage = () => {
- if (image.value) {
- URL.revokeObjectURL(image.value.preview);
- image.value = null;
- }
- };
- // 진출권으로 바뀌면 포인트 값 초기화 (UI는 v-if로 숨김)
- watch(
- () => formData.value.type,
- (val) => {
- if (val === "T") formData.value.point = null;
- }
- );
- // 폼 제출
- const handleSubmit = async () => {
- successMessage.value = "";
- errorMessage.value = "";
- const name = formData.value.name.trim();
- if (!name) return (errorMessage.value = "아이템명을 입력하세요.");
- if (name.length > 50) return (errorMessage.value = "아이템명은 50자 이내로 입력하세요.");
- if (!["T", "P", "B"].includes(formData.value.type)) {
- return (errorMessage.value = "구분을 선택하세요.");
- }
- // 포인트 검증
- if (formData.value.type === "P") {
- if (formData.value.point === null || formData.value.point === "" || Number(formData.value.point) < 0) {
- return (errorMessage.value = "포인트를 입력하세요.");
- }
- } else if (formData.value.type === "B") {
- if (formData.value.point !== null && formData.value.point !== "" && Number(formData.value.point) < 0) {
- return (errorMessage.value = "포인트는 0 이상의 숫자여야 합니다.");
- }
- }
- isSaving.value = true;
- try {
- const fd = new FormData();
- fd.append("name", name);
- fd.append("type", formData.value.type);
- if (formData.value.type !== "T" && formData.value.point !== null && formData.value.point !== "") {
- fd.append("point", String(formData.value.point));
- }
- fd.append("status_YN", formData.value.status_YN);
- if (image.value) fd.append("image", image.value.file);
- const { data, error } = await upload("/item", fd);
- if (error || !data?.success) {
- errorMessage.value = error?.message || data?.message || "등록에 실패했습니다.";
- } else {
- successMessage.value = data.message || "아이템이 등록되었습니다.";
- setTimeout(() => {
- router.push("/site-manager/item/list");
- }, 1000);
- }
- } catch (e) {
- errorMessage.value = "서버 오류가 발생했습니다.";
- console.error("Save error:", e);
- } finally {
- isSaving.value = false;
- }
- };
- const goToList = () => router.push("/site-manager/item/list");
- </script>
|