|
|
@@ -422,7 +422,15 @@
|
|
|
<button type="button" class="admin--btn" @click="goToList">
|
|
|
← 목록으로
|
|
|
</button>
|
|
|
- <button type="submit" class="admin--btn admin--btn-red ml--auto" :disabled="isSaving">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="admin--btn admin--btn-primary ml--auto"
|
|
|
+ :disabled="isSavingDraft"
|
|
|
+ @click="handleSaveDraft"
|
|
|
+ >
|
|
|
+ {{ isSavingDraft ? "저장 중..." : "임시저장" }}
|
|
|
+ </button>
|
|
|
+ <button type="submit" class="admin--btn admin--btn-red" :disabled="isSaving">
|
|
|
{{ isSaving ? "저장 중..." : "저장" }}
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -505,6 +513,17 @@
|
|
|
</div>
|
|
|
</Teleport>
|
|
|
</ClientOnly>
|
|
|
+
|
|
|
+ <!-- 임시저장 불러오기 모달 -->
|
|
|
+ <AdminAlertModal
|
|
|
+ v-if="showDraftModal"
|
|
|
+ title="임시저장 불러오기"
|
|
|
+ :message="`임시저장된 챌린지가 있습니다.\n(저장: ${draftSavedAt})\n불러올까요?\n\n[확인] 불러오기 [취소] 새로 작성 (임시저장 삭제)`"
|
|
|
+ type="confirm"
|
|
|
+ @confirm="loadDraft"
|
|
|
+ @cancel="discardDraft"
|
|
|
+ @close="showDraftModal = false"
|
|
|
+ />
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -513,6 +532,7 @@
|
|
|
import { useRouter } from "vue-router";
|
|
|
import DatePicker from "~/components/admin/DatePicker.vue";
|
|
|
import SunEditor from "~/components/admin/SunEditor.vue";
|
|
|
+ import AdminAlertModal from "~/components/admin/AdminAlertModal.vue";
|
|
|
|
|
|
definePageMeta({
|
|
|
layout: "admin",
|
|
|
@@ -520,13 +540,19 @@
|
|
|
});
|
|
|
|
|
|
const router = useRouter();
|
|
|
- const { get, post, upload } = useApi();
|
|
|
+ const { get, post, del, upload } = useApi();
|
|
|
const { getImageUrl } = useImage();
|
|
|
|
|
|
const isSaving = ref(false);
|
|
|
+ const isSavingDraft = ref(false);
|
|
|
const successMessage = ref("");
|
|
|
const errorMessage = ref("");
|
|
|
|
|
|
+ // 임시저장 관련 상태
|
|
|
+ const showDraftModal = ref(false);
|
|
|
+ const draftSavedAt = ref("");
|
|
|
+ const draftData = ref(null);
|
|
|
+
|
|
|
// ============================
|
|
|
// 옵션 데이터
|
|
|
// ============================
|
|
|
@@ -835,6 +861,129 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ============================
|
|
|
+ // 임시저장 (draft)
|
|
|
+ // ============================
|
|
|
+
|
|
|
+ // 페이지 진입 시 임시저장 있는지 확인
|
|
|
+ async function checkDraft() {
|
|
|
+ try {
|
|
|
+ const { data } = await get("/challenge/draft");
|
|
|
+ if (data?.success && data.data) {
|
|
|
+ draftData.value = data.data.data; // 응답의 data 컬럼 (이미 객체로 파싱됨)
|
|
|
+ const ts = data.data.updated_at || data.data.created_at;
|
|
|
+ draftSavedAt.value = ts
|
|
|
+ ? new Date(String(ts).replace(" ", "T")).toLocaleString("ko-KR")
|
|
|
+ : "";
|
|
|
+ showDraftModal.value = true;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error("[Draft] 조회 실패:", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 임시저장 불러오기 (모달 확인 후)
|
|
|
+ function loadDraft() {
|
|
|
+ showDraftModal.value = false;
|
|
|
+ const d = draftData.value;
|
|
|
+ if (!d) return;
|
|
|
+
|
|
|
+ formData.value = {
|
|
|
+ name: d.name || "",
|
|
|
+ fee: d.fee || "",
|
|
|
+ max_participants: d.max_participants || "",
|
|
|
+ status_YN: d.status_YN || "Y",
|
|
|
+ description: d.description || "",
|
|
|
+ };
|
|
|
+ startDate.value = d.start_date || "";
|
|
|
+ endDate.value = d.end_date || "";
|
|
|
+ isFree.value = !!d.is_free;
|
|
|
+
|
|
|
+ if (Array.isArray(d.rounds) && d.rounds.length >= 2) {
|
|
|
+ rounds.value = d.rounds.map((r) => {
|
|
|
+ const round = createRound(r.round_no);
|
|
|
+ round.place_mode = r.place_mode || "all";
|
|
|
+ round.qualified = String(r.qualified || "");
|
|
|
+ round.items = (r.items || []).map((it) => ({ ...it }));
|
|
|
+ round.places = (r.places || []).map((p) => {
|
|
|
+ const place = createPlace();
|
|
|
+ place.field_id = p.field_id || "";
|
|
|
+ place.area_id = p.area_id || "";
|
|
|
+ place.partnership_YN = p.partnership_YN || "";
|
|
|
+ place.onboards = [...(p.onboards || [])]; // 키 배열 그대로
|
|
|
+ place.items = (p.items || []).map((it) => ({ ...it }));
|
|
|
+ return place;
|
|
|
+ });
|
|
|
+ return round;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ successMessage.value = "임시저장을 불러왔습니다.";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 임시저장 삭제하고 새로 작성
|
|
|
+ async function discardDraft() {
|
|
|
+ showDraftModal.value = false;
|
|
|
+ try {
|
|
|
+ await del("/challenge/draft");
|
|
|
+ } catch (e) {
|
|
|
+ console.error("[Draft] 삭제 실패:", e);
|
|
|
+ }
|
|
|
+ draftData.value = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 임시저장 버튼 핸들러
|
|
|
+ async function handleSaveDraft() {
|
|
|
+ errorMessage.value = "";
|
|
|
+ successMessage.value = "";
|
|
|
+
|
|
|
+ if (!formData.value.name.trim()) {
|
|
|
+ return (errorMessage.value = "임시저장하려면 최소한 챌린지명은 입력하세요.");
|
|
|
+ }
|
|
|
+
|
|
|
+ isSavingDraft.value = true;
|
|
|
+ try {
|
|
|
+ const payload = {
|
|
|
+ name: formData.value.name,
|
|
|
+ fee: formData.value.fee,
|
|
|
+ max_participants: formData.value.max_participants,
|
|
|
+ status_YN: formData.value.status_YN,
|
|
|
+ description: formData.value.description,
|
|
|
+ start_date: startDate.value,
|
|
|
+ end_date: endDate.value,
|
|
|
+ is_free: isFree.value,
|
|
|
+ rounds: rounds.value.map((r) => ({
|
|
|
+ round_no: r.round_no,
|
|
|
+ place_mode: r.place_mode,
|
|
|
+ qualified: r.qualified,
|
|
|
+ items: r.items.map((it) => ({
|
|
|
+ item_id: it.item_id, name: it.name, type: it.type, point: it.point,
|
|
|
+ })),
|
|
|
+ places: r.places.map((p) => ({
|
|
|
+ field_id: p.field_id,
|
|
|
+ area_id: p.area_id,
|
|
|
+ partnership_YN: p.partnership_YN,
|
|
|
+ onboards: [...p.onboards],
|
|
|
+ items: p.items.map((it) => ({
|
|
|
+ item_id: it.item_id, name: it.name, type: it.type, point: it.point,
|
|
|
+ })),
|
|
|
+ })),
|
|
|
+ })),
|
|
|
+ };
|
|
|
+
|
|
|
+ const { data, error } = await post("/challenge/draft", payload);
|
|
|
+ if (error || !data?.success) {
|
|
|
+ errorMessage.value = error?.message || data?.message || "임시저장 실패";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ successMessage.value = "임시저장되었습니다.";
|
|
|
+ } catch (e) {
|
|
|
+ console.error("[Draft] 저장 실패:", e);
|
|
|
+ errorMessage.value = "서버 오류가 발생했습니다.";
|
|
|
+ } finally {
|
|
|
+ isSavingDraft.value = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// ============================
|
|
|
// 폼 제출
|
|
|
// ============================
|
|
|
@@ -918,6 +1067,9 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 정식 등록 성공 → 임시저장 삭제
|
|
|
+ try { await del("/challenge/draft"); } catch (_) { /* noop */ }
|
|
|
+
|
|
|
successMessage.value = data.message || "챌린지가 등록되었습니다.";
|
|
|
setTimeout(() => {
|
|
|
router.push("/site-manager/challenge/list");
|
|
|
@@ -932,9 +1084,11 @@
|
|
|
|
|
|
const goToList = () => router.push("/site-manager/challenge/list");
|
|
|
|
|
|
- onMounted(() => {
|
|
|
- loadOptions();
|
|
|
+ onMounted(async () => {
|
|
|
document.addEventListener("click", handleDocumentClick);
|
|
|
+ await loadOptions();
|
|
|
+ // 옵션 로드 후 임시저장 확인 (불러올 때 onboards 매핑 안전)
|
|
|
+ await checkDraft();
|
|
|
});
|
|
|
|
|
|
onBeforeUnmount(() => {
|