258 lines
8.4 KiB
Vue
258 lines
8.4 KiB
Vue
<template>
|
|
<Dialog
|
|
:visible="visible"
|
|
@update:visible="$emit('update:visible', $event)"
|
|
:header="isNew ? '이벤트 추가' : '이벤트 수정'"
|
|
:style="{ width: '650px' }"
|
|
:modal="true"
|
|
class="p-fluid"
|
|
>
|
|
<div class="grid">
|
|
<div class="col-12">
|
|
<label for="title">이벤트 제목 *</label>
|
|
<InputText id="title" v-model="eventModel.title" :class="{'p-invalid': submitted && !eventModel.title}" variant="filled" required autofocus />
|
|
<small class="p-error" v-if="submitted && !eventModel.title">제목은 필수입니다.</small>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label for="description">이벤트 설명</label>
|
|
<Textarea id="description" v-model="eventModel.description" variant="filled" rows="3" autoResize class="w-full" />
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="eventType">이벤트 유형 *</label>
|
|
<Select id="eventType" v-model="eventModel.eventType" variant="filled" :options="eventTypeOptions" optionLabel="name" optionValue="value" placeholder="유형 선택" :class="{'p-invalid': submitted && !eventModel.eventType, 'w-full': true}"/>
|
|
<small class="p-error" v-if="submitted && !eventModel.eventType">이벤트 유형은 필수입니다.</small>
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="status">상태 *</label>
|
|
<Select id="status" v-model="eventModel.status" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" :class="{'p-invalid': submitted && !eventModel.status, 'w-full': true}" />
|
|
<small class="p-error" v-if="submitted && !eventModel.status">상태는 필수입니다.</small>
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="startDate">시작일 *</label>
|
|
<Datepicker id="startDate" v-model="eventModel.startDate" :showTime="true" dateFormat="yy-mm-dd" timeFormat="24" hourFormat="24" :showIcon="true" :stepMinute="5" :class="{'p-invalid': submitted && !eventModel.startDate, 'w-full': true}" />
|
|
<small class="p-error" v-if="submitted && !eventModel.startDate">시작일은 필수입니다.</small>
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="endDate">종료일</label>
|
|
<Datepicker id="endDate" v-model="eventModel.endDate" :showTime="true" dateFormat="yy-mm-dd" timeFormat="24" hourFormat="24" :showIcon="true" :stepMinute="5" :class="{'p-invalid': submitted && !eventModel.endDate, 'w-full': true}" />
|
|
<small class="p-error" v-if="submitted && !eventModel.endDate">종료일은 필수입니다.</small>
|
|
</div>
|
|
|
|
<div class="col-12">
|
|
<label for="location">장소</label>
|
|
<InputText id="location" v-model="eventModel.location" :class="{'p-invalid': submitted && !eventModel.location, 'w-full': true}" />
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="maxParticipants">최대 참가자 수</label>
|
|
<InputNumber id="maxParticipants" v-model="eventModel.maxParticipants" :min="0" class="w-full" />
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="gameCount">게임 수</label>
|
|
<InputNumber id="gameCount" v-model="eventModel.gameCount" :min="1" class="w-full" />
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="participantFee">참가비</label>
|
|
<InputNumber id="participantFee" v-model="eventModel.participantFee" :min="0" mode="currency" currency="KRW" locale="ko-KR" class="w-full" />
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="registrationDeadline">신청마감</label>
|
|
<Datepicker id="registrationDeadline" v-model="eventModel.registrationDeadline" :showTime="true" dateFormat="yy-mm-dd" timeFormat="24" hourFormat="24" :showIcon="true" :stepMinute="5" :class="{'p-invalid': submitted && !eventModel.registrationDeadline, 'w-full': true}" />
|
|
</div>
|
|
|
|
<div class="col-12 md:col-6">
|
|
<label for="publicHash">공개 URL</label>
|
|
<InputGroup>
|
|
<InputText id="publicHash" v-model="eventModel.publicHash" readonly placeholder="생성 후 표시" />
|
|
<Button
|
|
type="button"
|
|
icon="pi pi-refresh"
|
|
@click="regeneratePublicHash"
|
|
:disabled="isNew || loadingHash"
|
|
class="p-button-sm"
|
|
:aria-label="eventModel.publicHash ? '공개 URL 재생성' : '공개 URL 생성'"
|
|
/>
|
|
</InputGroup>
|
|
</div>
|
|
<div class="col-12 md:col-6">
|
|
<label for="accessPassword">공개 비밀번호(선택)</label>
|
|
<InputText id="accessPassword" v-model="eventModel.accessPassword" type="text" maxlength="32" placeholder="공개 페이지 접근용 비밀번호(선택)" style="width:100%;" />
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
|
|
<Button label="저장" icon="pi pi-check" class="p-button-text" @click="saveEvent" />
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, toRefs, computed } from 'vue';
|
|
import InputGroup from 'primevue/inputgroup';
|
|
import dayjs from 'dayjs';
|
|
|
|
const statusOptions = [
|
|
{ name: '준비', value: '준비' },
|
|
{ name: '활성', value: '활성' },
|
|
{ name: '완료', value: '완료' },
|
|
{ name: '취소', value: '취소' },
|
|
{ name: '삭제', value: '삭제' }
|
|
];
|
|
|
|
const props = defineProps({
|
|
visible: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
event: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
isNew: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
submitted: {
|
|
type: Boolean,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['update:visible', 'save', 'hide']);
|
|
|
|
// 이벤트 타입 옵션
|
|
const eventTypeOptions = [
|
|
{ name: '정기전', value: '정기전' },
|
|
{ name: '번개', value: '번개' },
|
|
{ name: '연습', value: '연습' },
|
|
{ name: '교류전', value: '교류전' },
|
|
{ name: '이벤트', value: '이벤트' },
|
|
{ name: '기타', value: '기타' }
|
|
];
|
|
|
|
// 이벤트 모델
|
|
const eventModel = ref({ ...props.event });
|
|
import { useToast } from 'primevue/usetoast';
|
|
const toast = useToast();
|
|
const loadingHash = ref(false);
|
|
|
|
|
|
// 날짜 형식 변환
|
|
const formatDate = (date) => {
|
|
if (!date) return null;
|
|
return dayjs(date).toDate();
|
|
};
|
|
|
|
// props 변경 시 이벤트 모델 업데이트
|
|
watch(() => props.event, (newValue) => {
|
|
const formattedEvent = {
|
|
...newValue,
|
|
startDate: formatDate(newValue.startDate),
|
|
endDate: formatDate(newValue.endDate),
|
|
registrationDeadline: formatDate(newValue.registrationDeadline)
|
|
};
|
|
eventModel.value = formattedEvent;
|
|
}, { deep: true });
|
|
|
|
// 공개 URL(공개 해시) 재생성
|
|
const regeneratePublicHash = async () => {
|
|
if (props.isNew) return;
|
|
loadingHash.value = true;
|
|
try {
|
|
const res = await fetch(`/api/club/events/${eventModel.value.id}/publicHash`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
if (!res.ok) throw new Error('공개 URL 생성/재생성에 실패했습니다.');
|
|
const data = await res.json();
|
|
eventModel.value.publicHash = data.publicHash;
|
|
toast.add({ severity: 'info', summary: '공개 URL 재생성', detail: '저장 버튼을 눌러야 최종 반영됩니다.', life: 2000 });
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: '오류', detail: e.message });
|
|
} finally {
|
|
loadingHash.value = false;
|
|
}
|
|
};
|
|
|
|
// 이벤트 저장
|
|
const saveEvent = () => {
|
|
const eventData = { ...eventModel.value };
|
|
if (eventData.startDate) {
|
|
eventData.startDate = dayjs(eventData.startDate).toDate();
|
|
}
|
|
if (eventData.endDate) {
|
|
eventData.endDate = dayjs(eventData.endDate).toDate();
|
|
}
|
|
if (eventData.registrationDeadline) {
|
|
eventData.registrationDeadline = dayjs(eventData.registrationDeadline).toDate();
|
|
}
|
|
|
|
emit('save', eventData);
|
|
};
|
|
|
|
// 다이얼로그 닫기
|
|
const hideDialog = () => {
|
|
emit('hide');
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.field {
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
margin: -0.25rem;
|
|
}
|
|
|
|
.col-12 {
|
|
width: 100%;
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
.md\:col-6 {
|
|
width: 50%;
|
|
padding: 0.25rem;
|
|
}
|
|
|
|
.dialog-footer {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.p-dialog-content {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.p-dialog-footer {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
:deep(.p-dropdown),
|
|
:deep(.p-calendar),
|
|
:deep(.p-inputnumber) {
|
|
width: 100%;
|
|
}
|
|
|
|
:deep(.p-inputtext) {
|
|
width: 100%;
|
|
}
|
|
|
|
.public-hash-btn.p-button-icon-only {
|
|
min-width:28px;
|
|
height:28px;
|
|
padding:0;
|
|
font-size:1.1em;
|
|
}
|
|
</style>
|