Files
bowlingManager/frontend/src/components/event/EventDialog.vue
T
2025-05-07 21:02:50 +09:00

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>