This commit is contained in:
2025-06-13 03:19:51 +09:00
parent e91d481216
commit f6a2f1223f
28 changed files with 970 additions and 346 deletions

View File

@@ -19,7 +19,7 @@
</NuxtLink>
</div>
<nav
<!-- <nav
class="-mb-6 flex flex-wrap justify-center gap-x-6 sm:gap-x-12 gap-y-3 text-sm"
aria-label="Footer Navigation"
>
@@ -31,7 +31,7 @@
>
{{ item.name }}
</NuxtLink>
</nav>
</nav> -->
<hr class="my-8 border-gray-300 dark:border-gray-700 w-1/2 mx-auto" />

View File

@@ -6,14 +6,14 @@
>
<!-- MobileSidebar Open Button -->
<div class="flex justify-start">
<button
<!-- <button
type="button"
class="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-700 dark:text-white"
@click="mobileMenuOpen = true"
aria-label="Open main menu"
>
<Bars3Icon class="size-6" aria-hidden="true" />
</button>
</button> -->
</div>
<!-- Desktop Navigation -->
@@ -46,7 +46,7 @@
<!-- Desktop HeaderActions -->
<div class="flex justify-end">
<HeaderActions />
<!-- <HeaderActions /> -->
</div>
</nav>
<!-- MobileSidebar -->

View File

@@ -1,282 +0,0 @@
<template>
>
<form @submit.prevent="onSubmit" class="mx-auto mt-10 max-w-lg space-y-8">
<!-- 1) 성함 -->
<div>
<label
for="name"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
성함 (Your Name)
</label>
<input
v-model="form.name"
type="text"
id="name"
required
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
</div>
<!-- 2) 와디즈 결제 번호 -->
<div>
<label
for="paymentId"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
와디즈 결제 번호 (Wadiz Payment ID)
</label>
<input
v-model="form.paymentId"
type="text"
id="paymentId"
required
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
</div>
<!-- 3) 메일 -->
<div>
<label
for="email"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
메일 (Email)
</label>
<input
v-model="form.email"
type="email"
id="email"
required
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
</div>
<!-- 4) 본인 연락처 -->
<div>
<label
for="phone"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
본인 연락처 (Your Phone)
</label>
<input
v-model="form.phone"
type="tel"
id="phone"
required
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
placeholder="010-1234-5678"
/>
</div>
<!-- 5) 본인 참석 여부 -->
<div>
<span class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>본인 참석 여부 (Are you attending?)</span
>
<div class="mt-2 flex items-center gap-6">
<label class="inline-flex items-center">
<input
type="radio"
name="attending"
value="yes"
v-model="form.attending"
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
/>
<span class="ml-2 text-gray-700 dark:text-gray-300"> (Yes)</span>
</label>
<label class="inline-flex items-center">
<input
type="radio"
name="attending"
value="no"
v-model="form.attending"
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
/>
<span class="ml-2 text-gray-700 dark:text-gray-300">아니오 (No)</span>
</label>
</div>
</div>
<!-- 6) If 아니오, show alternate attendee fields -->
<transition name="fade" mode="out-in">
<div v-if="form.attending === 'no'" class="space-y-4">
<!-- 대리 참석자 성함 -->
<div>
<label
for="altName"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
대리 참석자 성함 (Alternate Attendee Name)
</label>
<input
v-model="form.altName"
type="text"
id="altName"
required
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
</div>
<!-- 대리 참석자 연락처 -->
<div>
<label
for="altPhone"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
대리 참석자 연락처 (Alternate Attendee Phone)
</label>
<input
v-model="form.altPhone"
type="tel"
id="altPhone"
required
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
placeholder="010-1234-5678"
/>
</div>
</div>
</transition>
<!-- 7) 기타 문의 사항 -->
<div>
<label
for="remarks"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
기타 문의 사항 (Other Remarks)
</label>
<textarea
v-model="form.remarks"
id="remarks"
rows="4"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
placeholder="예: 채팅으로 연락해주세요."
/>
</div>
<!-- Submit -->
<div class="mt-6">
<button
type="submit"
:disabled="submitting"
class="inline-flex w-full justify-center rounded-md bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-base font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-600 disabled:opacity-50"
>
{{ submitting ? '전송 중…' : '제출하기' }}
</button>
</div>
<!-- Success / Error Message -->
<p
v-if="message"
:class="
messageError
? 'mt-4 text-sm text-red-600'
: 'mt-4 text-sm text-green-600'
"
>
{{ message }}
</p>
</form>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useNuxtApp } from '#app';
/**
* This form will write a document to Firestore collection "wadizSubmissions".
* Make sure you've set up a Nuxt plugin that provides `$firebase.firestore()` if you haven't already.
*/
const submitting = ref(false);
const message = ref('');
const messageError = ref(false);
// Form state
const form = ref({
name: '',
paymentId: '',
email: '',
phone: '',
attending: 'yes', // default “yes”
altName: '',
altPhone: '',
remarks: '',
});
async function onSubmit() {
submitting.value = true;
message.value = '';
messageError.value = false;
// Basic validation
if (
!form.value.name ||
!form.value.paymentId ||
!form.value.email ||
!form.value.phone
) {
message.value = '필수 항목을 모두 채워주세요.';
messageError.value = true;
submitting.value = false;
return;
}
if (
form.value.attending === 'no' &&
(!form.value.altName || !form.value.altPhone)
) {
message.value = '대리 참석자 정보를 모두 입력해주세요.';
messageError.value = true;
submitting.value = false;
return;
}
// Prepare payload
const payload = {
name: form.value.name,
paymentId: form.value.paymentId,
email: form.value.email,
phone: form.value.phone,
attending: form.value.attending === 'yes',
altName: form.value.attending === 'no' ? form.value.altName : null,
altPhone: form.value.attending === 'no' ? form.value.altPhone : null,
remarks: form.value.remarks,
submittedAt: new Date(),
};
try {
// Save to Firestore (collection “wadizSubmissions”)
message.value = '성공적으로 제출되었습니다!';
messageError.value = false;
// Clear form
form.value.name = '';
form.value.paymentId = '';
form.value.email = '';
form.value.phone = '';
form.value.attending = 'yes';
form.value.altName = '';
form.value.altPhone = '';
form.value.remarks = '';
} catch (err) {
console.error(err);
message.value = '제출 중 오류가 발생했습니다. 다시 시도해주세요.';
messageError.value = true;
} finally {
submitting.value = false;
}
}
</script>
<style scoped>
/* Fade transition for the conditional fields */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div class="isolate bg-white dark:bg-gray-900 px-6 py-12 sm:py-12 lg:px-8">
<!-- Section -->
<section
class="isolate bg-white dark:bg-gray-900 px-6 py-16 sm:py-24 lg:px-8"
>
<div class="mx-auto max-w-2xl lg:max-w-4xl">
<p
class="text-center text-2xl font-extrabold text-indigo-500 dark:text-indigo-400"
>
와디즈 펀딩 참여자 발송 페이지
</p>
<figure class="mt-10">
<blockquote
class="text-center text-lg font-normal text-gray-900 dark:text-gray-100"
>
<p>안녕하세요, 메이커 보부입니다!</p>
<p>보부의 여정에 마음을 더해주셔서 감사합니다.</p>
<p>여러분의 따뜻한 응원 덕분에,</p>
<p>정선의 자연속 1 2일의 쉼을 준비할 있었습니다.</p>
<p>자연에 기대어, 잠시 천천히 머물러보는 시간을 선물해드릴게요.</p>
<p class="mt-4 font-semibold">
보부와 함께할 하루, 이제 예약으로 이어집니다."
</p>
</blockquote>
</figure>
</div>
<NuxtLink to="/wadiz/upload">
<button
class="block w-full bg-indigo-600 text-white py-3 px-3 rounded transition hover:bg-indigo-700 mt-12 font-semibold dark:bg-indigo-500 dark:hover:bg-indigo-600 disabled:opacity-50 dark:disabled:opacity-60"
>
참여자 정보 입력하기
</button>
</NuxtLink>
</section>
<!-- Form -->
<ClientOnly> </ClientOnly>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,671 @@
<template>
<div class="mx-auto max-w-5xl w-full py-4 px-6 items-center">
<div class="border-b border-gray-200 dark:border-gray-700 pb-8">
<VeeForm @submit="handleUpload" v-slot="{ isSubmitting }">
<div class="mx-auto mt-10 max-w-lg space-y-8">
<!-- 1) 와디즈 결제 번호 -->
<div>
<label
for="paymentId"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
와디즈 결제 번호
</label>
<VeeField
name="paymentId"
id="paymentId"
type="text"
rules="required"
v-model="boardsData.paymentId"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
<VeeErrorMessage
name="paymentId"
class="text-red-500 text-sm mt-1"
/>
</div>
<!-- 2) 성함 -->
<div>
<label
for="name"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
성함
</label>
<VeeField
name="name"
id="name"
type="text"
rules="required"
v-model="boardsData.name"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
<VeeErrorMessage name="name" class="text-red-500 text-sm mt-1" />
</div>
<!-- 3) 주소 -->
<div>
<label
for="address"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
주소
</label>
<VeeField
name="address"
id="address"
type="text"
rules="required"
v-model="boardsData.address"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600"
/>
<VeeErrorMessage name="address" class="text-red-500 text-sm mt-1" />
</div>
<!-- 4) 메일 -->
<div>
<label
for="email"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
메일
</label>
<VeeField
name="email"
id="email"
type="email"
rules="required|email"
v-model="boardsData.email"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
<VeeErrorMessage name="email" class="text-red-500 text-sm mt-1" />
</div>
<!-- 5) 본인 연락처 -->
<div>
<label
for="phone"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
본인 연락처 (Your Phone)
</label>
<VeeField
name="phone"
id="phone"
type="tel"
rules="required"
v-model="boardsData.phone"
placeholder="010-1234-5678"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
<VeeErrorMessage name="phone" class="text-red-500 text-sm mt-1" />
</div>
<!-- 6) 긴급 연락처 -->
<div>
<label
for="emergencyPhone"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
긴급 연락처 | 본인 제3자
</label>
<VeeField
name="emergencyPhone"
id="emergencyPhone"
type="tel"
rules="required"
v-model="boardsData.emergencyPhone"
placeholder="010-0000-0000"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600"
/>
<VeeErrorMessage
name="emergencyPhone"
class="text-red-500 text-sm mt-1"
/>
</div>
<!-- 7) 예약 일정 선택 -->
<div>
<label
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
예약 일정 선택
</label>
<div class="mt-2 flex gap-4">
<!-- 시작일 -->
<div class="w-1/2">
<label
for="scheduleStart"
class="mb-1 block text-sm text-gray-700 dark:text-gray-300"
>
시작일
</label>
<Datepicker
v-model="scheduleStartDate"
:highlight="highlightedDates"
:allowed-dates="allowedDates"
:enable-time-picker="false"
auto-apply
id="scheduleStart"
/>
</div>
<!-- 종료일 -->
<div class="w-1/2">
<label
for="scheduleEnd"
class="mb-1 block text-sm text-gray-700 dark:text-gray-300"
>
종료일
</label>
<input
v-model="boardsData.scheduleEnd"
id="scheduleEnd"
type="text"
disabled
placeholder="자동 계산 (1박 2일)"
class="w-full rounded-md bg-gray-100 dark:bg-gray-700 px-3 py-2 text-gray-400 dark:text-gray-400 border dark:border-gray-600"
/>
</div>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
예약불가 날짜: 2025 10 3()~5(), 연박 불가 / ··
1박만 가능
</p>
</div>
<!-- 8) 본인 참석 여부 -->
<div>
<span
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
본인 참석 여부 (Are you attending?)
</span>
<div class="mt-2 flex items-center gap-6">
<label class="inline-flex items-center">
<VeeField
name="attending"
type="radio"
:value="'yes'"
v-model="boardsData.attending"
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
/>
<span class="ml-2 text-gray-700 dark:text-gray-300"
> (Yes)</span
>
</label>
<label class="inline-flex items-center">
<VeeField
name="attending"
type="radio"
:value="'no'"
v-model="boardsData.attending"
class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500"
/>
<span class="ml-2 text-gray-700 dark:text-gray-300"
>아니오 (No)</span
>
</label>
</div>
<VeeErrorMessage
name="attending"
class="text-red-500 text-sm mt-1"
/>
</div>
<!-- 8-2) If "아니오", show alternate attendee fields -->
<div v-if="boardsData.attending === 'no'" class="space-y-4">
<!-- 대리 참석자 성함 -->
<div>
<label
for="altName"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
대리 참석자 성함 (Alternate Attendee Name)
</label>
<VeeField
name="altName"
id="altName"
type="text"
v-model="boardsData.altName"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
<VeeErrorMessage
name="altName"
class="text-red-500 text-sm mt-1"
/>
</div>
<!-- 대리 참석자 연락처 -->
<div>
<label
for="altPhone"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
대리 참석자 연락처 (Alternate Attendee Phone)
</label>
<VeeField
name="altPhone"
id="altPhone"
type="tel"
v-model="boardsData.altPhone"
placeholder="010-1234-5678"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border outline-1 outline-offset-1 outline-gray-300 dark:outline-gray-600 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-2 focus:outline-indigo-600"
/>
<VeeErrorMessage
name="altPhone"
class="text-red-500 text-sm mt-1"
/>
</div>
</div>
<!-- 9) 참여 동반자 정보 -->
<div>
<label
for="companions"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
참여 동반자 정보 (선택)
</label>
<VeeField
name="companions"
id="companions"
type="text"
v-model="boardsData.companions"
placeholder="동반자 성함 등"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border dark:border-gray-600"
/>
<VeeErrorMessage
name="companions"
class="text-red-500 text-sm mt-1"
/>
</div>
<!-- 10) 알레르기나 건강 특이사항 (선택) -->
<div>
<label
for="healthNotes"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
알레르기나 건강 특이사항 (선택)
</label>
<VeeField
name="healthNotes"
id="healthNotes"
v-model="boardsData.healthNotes"
as="textarea"
rows="2"
placeholder="예: 견과류 알레르기"
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border dark:border-gray-600"
/>
<VeeErrorMessage
name="healthNotes"
class="text-red-500 text-sm mt-1"
/>
</div>
<!-- 11) 전달하고 싶은 (선택) -->
<div>
<label
for="remarks"
class="block text-sm font-medium text-gray-900 dark:text-gray-100"
>
전달하고 싶은 (선택)
</label>
<VeeField
name="remarks"
id="remarks"
v-model="boardsData.remarks"
as="textarea"
rows="4"
placeholder="예: 채팅으로 연락해주세요."
class="mt-1 block w-full rounded-md bg-white dark:bg-gray-800 px-3.5 py-2 text-base text-gray-900 dark:text-gray-100 border dark:border-gray-600"
/>
<VeeErrorMessage name="remarks" class="text-red-500 text-sm mt-1" />
</div>
<!-- Submit -->
<button
type="submit"
class="block w-full bg-indigo-600 text-white py-3 px-3 rounded transition hover:bg-indigo-700 mt-12 font-semibold dark:bg-indigo-500 dark:hover:bg-indigo-600 disabled:opacity-50 dark:disabled:opacity-60"
:disabled="in_submission"
>
<div
v-if="show_alert"
:class="[
alert_variant, // Ensure this variable contains dark: variants
'text-white text-center font-bold p-4 rounded mb-4',
]"
>
{{ alert_msg }}
</div>
<div v-show="!in_submission">
{{ isEdit ? '수정하기' : '제출하기' }}
</div>
<div v-if="in_submission">
<font-awesome-icon :icon="['fas', 'spinner']" spin />
</div>
</button>
</div>
</VeeForm>
</div>
</div>
</template>
<script setup lang="ts">
import {
ref,
onBeforeUnmount,
onMounted,
watch,
reactive,
computed,
} from 'vue';
import { useRouter } from 'vue-router';
import { fetchLatestDocumentNumber } from '@/utils/firebaseUtils';
import {
resetFileSlots,
getFilesFromUploads,
createBoardsData,
uploadFiles,
handleUpdateBoard,
handleCreateBoard,
getDeleteFileIndexesFromUploads,
createEmptyUploadFileData,
getBoardDocRef,
} from '@/utils/boardUtils';
import {
Form as VeeForm,
Field as VeeField,
ErrorMessage as VeeErrorMessage,
} from 'vee-validate';
import { syncBoardAndUploadsData } from '@/utils/boardUtils';
import { useUserStore } from '@/stores/user';
//types
import { UploadSettings } from '@/data/config';
import type {
WadizBoard,
BoardItem,
ThumbnailData,
FileItem,
UploadFileData,
UploadsData,
} from '@/types';
import type { Ref } from 'vue';
const props = defineProps({
isEdit: { type: Boolean, default: false },
board: { type: Object as PropType<BoardItem>, default: () => ({}) },
});
const emit = defineEmits(['update-success', 'success']);
const userStore = useUserStore();
const userId = computed(() => userStore.docId);
//customize
const { $firebase } = useNuxtApp();
const wadizesCollection = $firebase.wadizesCollection;
const currentCollection = wadizesCollection;
const currentBoard = 'wadiz';
const compData = {
title: '공지사항 | NOTICE',
};
//loading Message
const loadingMessage = 'Uploading! 잠시만 기다려주세요...';
const isUploading = ref(false);
//alert
const { show_alert, alert_variant, alert_msg, showAlert } = useAlert();
// Validation
const validateInput = (): boolean => {
if (!boardsData.value.paymentId.trim()) {
showAlert('와디즈 결제 번호는 필수입니다!', 'bg-red-500', true);
in_submission.value = false;
return false;
}
return true;
};
//state
const in_submission = ref(false);
const isEdit = ref(props.isEdit ?? false);
const newBoard = ref(props.board);
const boardsData: Ref<WadizBoard> = ref({
docId: '',
userId: '',
title: '',
description: '',
boardState: { state: 'processing' },
announcement: false,
created: '',
files: [],
//depreciated
boards_number: 0,
thumbnail: { name: '', url: '' } as FileItem,
ishidden: false,
//wadizes
name: '',
paymentId: '',
email: '',
phone: '',
address: '',
emergencyPhone: '',
scheduleStart: '',
scheduleEnd: '',
attending: 'yes',
altName: '',
altPhone: '',
companions: '',
healthNotes: '',
remarks: '',
});
const thumbnailInput = ref<HTMLInputElement | null>(null);
const thumbnail: ThumbnailData = {
input: ref(null),
preview: ref(''),
deleteTrigger: ref(false),
oldPreviewState: ref(false),
uploadState: ref(false),
previewState: ref(false),
displayedName: ref(''),
};
const files = reactive<UploadFileData[]>([]);
const uploadsData: UploadsData = {
thumbnail,
files,
};
//FOR EDIT, Write
// firebase Upload
const handleUpload = async () => {
in_submission.value = true;
if (!validateInput()) {
in_submission.value = false;
return;
}
// ✅ Validate plain files (UploadFileData[])
const rejectedFiles = uploadsData.files
.map((fileData) => fileData?.input)
.filter(
(file) => file instanceof File && !UploadSettings.isValidFile(file)
);
if (rejectedFiles.length > 0) {
const reason =
rejectedFiles[0] instanceof File
? UploadSettings.getInvalidFileReasonKey(rejectedFiles[0])
: null;
const msg = UploadSettings.UploadErrorMessages[reason!];
showAlert(msg ?? 'Invalid file', 'bg-red-500');
in_submission.value = false;
return;
}
try {
const newBoardsNumber = await fetchLatestDocumentNumber(
currentCollection,
'boards_number'
);
let boardPayload: WadizBoard = createBoardsData(
boardsData.value,
newBoardsNumber
) as WadizBoard;
// ✅ Collect valid files and thumbnail
const { thumbnail, files } = getFilesFromUploads(uploadsData);
if (isEdit.value) {
if (!newBoard.value) {
showAlert('기존 게시글 데이터를 찾을 수 없습니다.', 'bg-red-500');
in_submission.value = false;
return;
}
const oldFiles = newBoard.value.files ?? [];
const oldThumbUrl = newBoard.value.thumbnail?.url ?? '';
const deleteThumbnail = uploadsData.thumbnail.deleteTrigger.value;
const deleteFileIndexes = getDeleteFileIndexesFromUploads(uploadsData);
boardPayload = (await uploadFiles({
newBoardData: boardPayload,
newThumbnail: thumbnail instanceof File ? thumbnail : null,
newFiles: files,
oldThumbnailUrl: oldThumbUrl,
oldFiles,
deleteThumbnail,
deleteFileIndexes,
currentBoard,
})) as WadizBoard;
await handleUpdateBoard(boardPayload, currentCollection);
showAlert('성공적으로 수정되었습니다', 'bg-green-800');
emit('success');
} else {
const { docId: freshDocId } = getBoardDocRef(currentCollection);
boardPayload = {
...boardPayload,
docId: freshDocId,
userId: userId.value,
boardState: { state: 'processing' },
};
boardPayload = (await uploadFiles({
newBoardData: boardPayload,
newThumbnail: thumbnail,
newFiles: files,
currentBoard,
})) as WadizBoard;
if (boardPayload.scheduleStart instanceof Date) {
boardPayload.scheduleStart = format(
boardPayload.scheduleStart,
'yyyy-MM-dd'
);
}
if (boardPayload.scheduleEnd instanceof Date) {
boardPayload.scheduleEnd = format(
boardPayload.scheduleEnd,
'yyyy-MM-dd'
);
}
await handleCreateBoard(boardPayload, currentCollection);
showAlert('성공적으로 생성되었습니다', 'bg-green-800', true);
emit('success');
}
} catch (error) {
console.error('업로드 중 에러 발생:', error);
showAlert('업로드 실패: 파일 또는 데이터 에러', 'bg-red-500');
} finally {
in_submission.value = false;
}
};
onMounted(() => {
// if (uploadsData.files.length === 0) {
// addFileSlot(uploadsData.files);
// }
console.log('mounted', uploadsData);
});
watch(
() => props.board,
(newBoard) => {
if (newBoard) {
syncBoardAndUploadsData(
newBoard,
boardsData,
uploadsData,
createEmptyUploadFileData,
resetFileSlots
);
console.log('newBoard', newBoard);
}
},
{ immediate: true }
);
//calender
import Datepicker from '@vuepic/vue-datepicker';
import '@vuepic/vue-datepicker/dist/main.css';
import { format, addDays } from 'date-fns';
// Blocked weekend range (연박 불가)
const blockedDates = ['2025-10-03', '2025-10-04', '2025-10-05'];
const allowedDates = computed(() => {
const result: Date[] = [];
const start = new Date('2025-06-13');
const end = new Date('2025-12-01');
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const date = new Date(d);
const day = date.getDay(); // 0: Sun, 5: Fri, 6: Sat
const dateStr = format(date, 'yyyy-MM-dd');
const isUnavailable = dateStr >= '2025-10-03' && dateStr <= '2025-10-05';
const isWeekend = [5, 6, 0].includes(day);
if (isWeekend && !isUnavailable) {
result.push(date);
}
}
return result;
});
// Optional: Highlight allowed dates with a class
const highlightedDates = computed(() => {
const map: Partial<Record<number, string>> = {};
allowedDates.value.forEach((date) => {
map[date.getTime()] = 'dp-highlight-green';
});
return map;
});
// Auto-calculate end date
watch(
() => boardsData.value.scheduleStart,
(val) => {
if (!val) return;
const endDate = addDays(new Date(val as string), 1);
boardsData.value.scheduleEnd = format(endDate, 'yyyy-MM-dd'); // ✅ Use ISO format
}
);
// Two-way binding date model
const scheduleStartDate = computed({
get: () =>
boardsData.value.scheduleStart instanceof Date
? boardsData.value.scheduleStart
: new Date(),
set: (val: Date) => {
boardsData.value.scheduleStart = val;
},
});
</script>
<style>
.ck-editor__editable {
min-height: 500px;
}
</style>

View File

@@ -0,0 +1,22 @@
<template>
<section class="bg-white dark:bg-gray-900 py-16 px-6">
<div class="max-w-2xl mx-auto text-center">
<h1
class="text-2xl sm:text-3xl font-bold text-gray-800 dark:text-white mb-6"
>
예약이 완료되었습니다. 감사합니다.
</h1>
<p class="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
정선의 자연, 그리고 보부의 하루에 함께해주셔서 진심으로 감사합니다.
여러분의 걸음이 저희에겐 의미였습니다.<br /><br />
자연 속에서의 , 차분히 준비해두겠습니다.<br />
<span class="font-semibold text-indigo-600 dark:text-indigo-400"
>, 정선에서 뵙겠습니다.</span
>
</p>
</div>
</section>
</template>
<script setup lang="ts">
console.log('Success page mounted!');
</script>

View File

@@ -6,10 +6,10 @@ export const COLORSELECTOR_BG = {
};
export const LOGOS = {
White: '/assets/img/logo/BOBU_LOGO_WHITE.webp',
RedGaro: '/assets/img/logo/Garo_Red.webp',
Red: '/assets/img/logo/BOBU_LOGO_RED.webp',
WhiteGaro: '/assets/img/logo/Garo_White.webp',
White: '/assets/img/logo/Bobu_White.webp',
RedGaro: '/assets/img/logo/Bobu_Red.webp',
Red: '/assets/img/logo/Bobu_Red.webp',
WhiteGaro: '/assets/img/logo/Bobu_White.webp',
};
export const ABOUT_IMAGES = {
manos: '/assets/img/shoot.jpg',

View File

@@ -29,12 +29,12 @@ export const SOCIAL_LINKS = {
export const companyInfo = {
name: '노마드보부', // Or the official name from registration
registrationNumber: '693-82-00244',
president: '배현일',
address: '강원 정선군 정선읍 정선로 1324 2층',
registrationNumber: '830-86-02932',
president: '서지민',
address: '강원 정선군 정선읍 정선로 1324, 2층',
phone: '0507-1353-1868',
fax: '0507-1353-1868',
email: 'manoscoop@naver.com',
email: 'bobu1104@naver.com',
copyYear: new Date().getFullYear(), // Automatically get current year
};
// Upload Settings

View File

@@ -61,6 +61,26 @@ export interface NavigationItem {
}
// Board : Elements
export type ReservationSubmission = {
docId: string;
userId: string;
paymentId: string;
name: string;
address: string;
email: string;
phone: string;
emergencyPhone: string;
scheduleStart: string;
scheduleEnd: string | null;
attending: boolean;
altName?: string | null;
altPhone?: string | null;
companions?: string;
healthNotes?: string;
remarks?: string;
submittedAt: Date;
boardState?: { state: 'processing' | 'completed' };
};
export type FileItem = {
name: string;

View File

@@ -0,0 +1,7 @@
<template>
<div>
<h1>Wadiz Success</h1>
</div>
</template>
<script setup></script>

View File

@@ -1,12 +1,17 @@
<template>
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<!-- Featured Projects Section -->
<FeaturesCarousel />
<!-- <FeaturesCarousel />
<AboutSection1 />
<AboutSection3 />
<AboutSection3 /> -->
<!-- <AppWadiz /> -->
</div>
</template>
<script setup lang="ts">
// import AppWadiz from '~/pages/wadiz/index.vue';
import { MAIN } from '~/data/assets';
definePageMeta({
redirect: '/wadiz',
});
</script>

View File

@@ -1,43 +0,0 @@
<template>
<div class="isolate bg-white dark:bg-gray-900 px-6 py-12 sm:py-12 lg:px-8">
<!-- Section -->
<section
class="isolate bg-white dark:bg-gray-900 px-6 py-16 sm:py-24 lg:px-8"
>
<div class="mx-auto max-w-2xl lg:max-w-4xl">
<p
class="text-center text-2xl font-extrabold text-indigo-500 dark:text-indigo-400"
>
와디즈 펀딩 참여자 발송 페이지
</p>
<figure class="mt-10">
<blockquote
class="text-center text-lg font-normal text-gray-900 dark:text-gray-100"
>
<p>안녕하세요, 주식회사 보부입니다.</p>
<p>
지난 와디즈에서 진행한 정선 백패킹 포레스트 관련한 예약 페이지
입니다.
</p>
<p class="mt-4">
참여하신 분들은
<strong class="text-gray-900 dark:text-gray-100"
>6 13일까지</strong
>
작성 부탁드립니다.
</p>
</blockquote>
</figure>
</div>
</section>
<!-- Form -->
<ClientOnly>
<app-wadiz-form />
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import AppWadizForm from '@/components/WadizForm.vue';
</script>

View File

@@ -0,0 +1,86 @@
<template>
<div>
<app-wadiz-welcome />
<!-- <app-board-list
v-model="selectedSort"
:title="compdata.title"
:sortOptions="sortOptions"
:userRole="userRole"
:current-page="currentPage"
:total-pages="totalPages"
:page-numbers="pageNumbers"
:show-select-boxes="showSelectBoxes"
:uploadRoute="currentUploadRoute"
@toggle-select-boxes="showSelectBoxes = !showSelectBoxes"
@delete-selected="onDeleteSelected"
@go-to-page="onGoToPage"
@prev-page="onPrevPage"
@next-page="onNextPage"
:isLoading="isLoading"
:loadingMessage="loadingMessage"
>
<template #list>
<app-board-list-single
v-for="item in currentItems"
:key="item.docId"
:userId="item.userId"
:item="item"
:showSelectBox="showSelectBoxes"
@select="onToggleSelect(item)"
:iconName="['fas', 'bell']"
:routeName="currentBoardRouteName"
/>
</template>
</app-board-list> -->
</div>
</template>
<script setup lang="ts">
import AppWadizWelcome from '~/components/WadizWelcome.vue';
import AppBoardsHeader from '@/components/boards/BoardHeader.vue';
import AppBoardList from '@/components/boards/BoardList.vue';
import AppBoardListSingle from '@/components/boards/BoardListSingle.vue';
import type { OrderByDirection, BoardAccessMode } from '@/types';
//customize
const access: BoardAccessMode = 'public';
const currentCollection = 'wadizes';
const currentUploadRoute = '/wadiz/upload';
const currentBoardRouteName = '/wadiz';
const compData = {
title: '공지사항',
itemsPerPage: 20,
defaultSort: 'desc' as OrderByDirection,
};
const loading = '게시물을 불러오는 중입니다...';
import { useBoardList } from '@/composables/useBoardList';
// destructure only the things you actually need
const {
isLoading,
loadingMessage,
compdata,
currentItems,
currentPage,
pageNumbers,
selectedSort,
sortOptions,
userRole,
totalPages,
selectedItems,
showSelectBoxes,
onToggleSelect,
onDeleteSelected,
onGoToPage,
onPrevPage,
onNextPage,
} = useBoardList(currentCollection, {
title: compData.title,
itemsPerPage: compData.itemsPerPage,
defaultSort: compData.defaultSort,
access: access,
loadingMessage: loading,
});
// no more fetchBoardsAndUpdateItems or onBeforeMount
</script>

View File

@@ -0,0 +1,22 @@
<template>
<section class="bg-white dark:bg-gray-900 py-16 px-6">
<div class="max-w-2xl mx-auto text-center">
<h1
class="text-2xl sm:text-3xl font-bold text-gray-800 dark:text-white mb-6"
>
예약이 완료되었습니다. 감사합니다.
</h1>
<p class="text-lg text-gray-700 dark:text-gray-300 leading-relaxed">
정선의 자연, 그리고 보부의 하루에 함께해주셔서 진심으로 감사합니다.
여러분의 걸음이 저희에겐 의미였습니다.<br /><br />
자연 속에서의 , 차분히 준비해두겠습니다.<br />
<span class="font-semibold text-indigo-600 dark:text-indigo-400"
>, 정선에서 뵙겠습니다.</span
>
</p>
</div>
</section>
</template>
<script setup lang="ts">
console.log('Success page mounted!');
</script>

View File

@@ -0,0 +1,16 @@
<template>
<ClientOnly>
<app-upload-wadiz-form
v-if="!isSuccess"
:isEdit="false"
@success="isSuccess = true"
/>
<app-wadiz-success v-if="isSuccess" />
</ClientOnly>
</template>
<script setup lang="ts">
const isSuccess = ref(false);
import AppUploadWadizForm from '@/components/boards/wadiz/UploadWadizForm.vue';
import AppWadizSuccess from '@/components/boards/wadiz/WadizSuccess.vue';
</script>

View File

@@ -0,0 +1,9 @@
<template>
<div>
<!-- <app-wadiz /> -->
</div>
</template>
<script setup>
import AppWadiz from '~/components/WadizWelcome.vue';
</script>

View File

@@ -36,6 +36,7 @@ export default defineNuxtPlugin((nuxtApp) => {
// Collections
const usersCollection = collection(db, 'users');
const wadizesCollection = collection(db, 'wadizes');
const faqboardsCollection = collection(db, 'faqboards');
const countersCollection = collection(db, 'counters');
const attendsCollection = collection(db, 'attends');
@@ -76,6 +77,7 @@ export default defineNuxtPlugin((nuxtApp) => {
functions,
analytics,
usersCollection,
wadizesCollection,
faqboardsCollection,
countersCollection,
attendsCollection,

View File

@@ -6,6 +6,7 @@ import type {
ProgramCategory,
VideoProvider,
} from '../data/config';
import { format } from 'date-fns';
// Management Page - Link to firebase/functions/src/types/boardItem.ts
// If you change , change up there too *******
@@ -124,6 +125,22 @@ export type BoardItem = {
ishidden?: boolean; // depriciated, we can use this for later admin control
thumbnail?: ImageItem; //Leave it for previous data structure
};
export interface WadizBoard extends BoardItem {
name: string;
paymentId: string;
email: string;
phone: string;
address: string;
emergencyPhone: string;
scheduleStart?: string | Date;
scheduleEnd?: string | Date;
attending: 'yes' | 'no';
altName?: string;
altPhone?: string;
companions?: string;
healthNotes?: string;
remarks?: string;
}
// Extended Board Types
export interface ProjectBoard extends BoardItem {
subtitle: string;

View File

@@ -14,6 +14,7 @@ export interface FirebasePlugin {
functions: Functions;
analytics: Analytics;
usersCollection: CollectionReference;
wadizesCollection: CollectionReference;
faqboardsCollection: CollectionReference;
countersCollection: CollectionReference;
attendsCollection: CollectionReference;

View File

@@ -1,5 +1,5 @@
const SESSION_URL = 'https://createsession-d4sni42fjq-du.a.run.app';
const LOGOUT_URL = 'https://logout-d4sni42fjq-du.a.run.app';
const SESSION_URL = 'https://createsession-edvvp3hbnq-du.a.run.app';
const LOGOUT_URL = 'https://logout-edvvp3hbnq-du.a.run.app';
/**
* Call Firebase Function to create session cookie.

View File

@@ -1,12 +1,12 @@
import type { BoardAccessMode } from '~/types';
const FUNCTION_BASE = 'https://countboards-d4sni42fjq-du.a.run.app';
const FUNCTION_BASE = 'https://countboards-edvvp3hbnq-du.a.run.app';
export async function fetchCountsFromFunction(
collection: string,
access: BoardAccessMode = 'public'
): Promise<number> {
return await $fetch<{ count: number }>(`${FUNCTION_BASE}/countBoards`, {
return await $fetch<{ count: number }>(`${FUNCTION_BASE}`, {
method: 'POST', // ✅ explicitly typed string
body: { collection, access }, // ✅ valid JSON object
credentials: access !== 'public' ? 'include' : undefined, // ✅ conditional credentials

View File

@@ -15,7 +15,7 @@ interface FetchBoardsParams {
pageToken?: string;
}
const FUNCTION_BASE = 'https://fetchboards-d4sni42fjq-du.a.run.app';
const FUNCTION_BASE = 'https://fetchboards-edvvp3hbnq-du.a.run.app';
/* --------------------------------------------------------------- */
export async function fetchBoardsFromFunction<T extends BoardItem>(

View File

@@ -1,4 +1,4 @@
const VERIFY_URL = 'https://verifysession-d4sni42fjq-du.a.run.app';
const VERIFY_URL = 'https://verifysession-edvvp3hbnq-du.a.run.app';
export type VerifiedSession = {
uid: string;

27
bobu/package-lock.json generated
View File

@@ -22,8 +22,10 @@
"@vee-validate/nuxt": "^4.15.0",
"@vee-validate/rules": "^4.15.0",
"@vesp/nuxt-fontawesome": "^1.2.1",
"@vuepic/vue-datepicker": "^11.0.2",
"ckeditor5": "^45.0.0",
"ckeditor5-premium-features": "^45.0.0",
"date-fns": "^4.1.0",
"firebase": "^11.8.1",
"firebase-functions": "^6.3.2",
"install": "^0.13.0",
@@ -12190,6 +12192,21 @@
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT"
},
"node_modules/@vuepic/vue-datepicker": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-11.0.2.tgz",
"integrity": "sha512-uHh78mVBXCEjam1uVfTzZ/HkyDwut/H6b2djSN9YTF+l/EA+XONfdCnOVSi1g+qVGSy65DcQAwyBNidAssnudQ==",
"license": "MIT",
"dependencies": {
"date-fns": "^4.1.0"
},
"engines": {
"node": ">=18.12.0"
},
"peerDependencies": {
"vue": ">=3.3.0"
}
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -13939,6 +13956,16 @@
"node": ">=10"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/db0": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/db0/-/db0-0.3.1.tgz",

View File

@@ -26,8 +26,10 @@
"@vee-validate/nuxt": "^4.15.0",
"@vee-validate/rules": "^4.15.0",
"@vesp/nuxt-fontawesome": "^1.2.1",
"@vuepic/vue-datepicker": "^11.0.2",
"ckeditor5": "^45.0.0",
"ckeditor5-premium-features": "^45.0.0",
"date-fns": "^4.1.0",
"firebase": "^11.8.1",
"firebase-functions": "^6.3.2",
"install": "^0.13.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB