259 lines
9.1 KiB
Vue
259 lines
9.1 KiB
Vue
<template>
|
|
<Dialog
|
|
:visible="visible"
|
|
@update:visible="$emit('update:visible', $event)"
|
|
:header="eventModel.title"
|
|
:style="{ width: '90vw', maxWidth: '1200px' }"
|
|
:modal="true"
|
|
class="event-details-dialog"
|
|
>
|
|
<Tabs value="기본정보">
|
|
<TabList>
|
|
<Tab value="기본정보"><i class="pi pi-info-circle mr-2" />기본정보</Tab>
|
|
<Tab value="참가자 관리"><i class="pi pi-users mr-2" />참가자 관리</Tab>
|
|
<Tab value="점수 관리"><i class="pi pi-chart-bar mr-2" />점수 관리</Tab>
|
|
<Tab v-if="hasTeamFeature" value="팀 편성"><i class="pi pi-sitemap mr-2" />팀 편성</Tab>
|
|
</TabList>
|
|
<TabPanels>
|
|
<!-- 기본 정보 탭 -->
|
|
<TabPanel value="기본정보">
|
|
<div class="event-details p-3">
|
|
<div class="flex align-items-center gap-2 mb-3">
|
|
<Badge :value="getEventTypeName(eventModel.eventType)" :severity="getStatusSeverity(eventModel.status)" />
|
|
<Badge :value="getStatusName(eventModel.status)" :severity="getStatusSeverity(eventModel.status)" />
|
|
</div>
|
|
<div class="card surface-100 p-3 mb-3">
|
|
<div class="grid">
|
|
<div class="col-12 md:col-6 mb-2">
|
|
<span class="font-bold text-color-secondary">시작일:</span>
|
|
<span class="ml-2">{{ formattedStartDate }}</span>
|
|
</div>
|
|
<div class="col-12 md:col-6 mb-2">
|
|
<span class="font-bold text-color-secondary">종료일:</span>
|
|
<span class="ml-2">{{ formattedEndDate }}</span>
|
|
</div>
|
|
<div class="col-12 md:col-6 mb-2">
|
|
<span class="font-bold text-color-secondary">장소:</span>
|
|
<span class="ml-2">{{ eventModel.location }}</span>
|
|
</div>
|
|
<div class="col-12 md:col-6 mb-2">
|
|
<span class="font-bold text-color-secondary">참가자 수:</span>
|
|
<span class="ml-2">{{ eventModel.currentParticipants }} / {{ eventModel.maxParticipants }}</span>
|
|
</div>
|
|
<div class="col-12 md:col-6 mb-2 flex align-items-center">
|
|
<span class="font-bold text-color-secondary">공개 URL:</span>
|
|
<InputText :value="publicEventUrl" class="ml-2 w-full" readonly />
|
|
<Button icon="pi pi-copy" class="ml-2" @click="copyPublicUrl" />
|
|
</div>
|
|
<div class="col-12 md:col-6 mb-2">
|
|
<span class="font-bold text-color-secondary">공개 비밀번호:</span>
|
|
<span class="ml-2">{{ eventModel.publicPassword }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card surface-100 p-3">
|
|
<span class="font-bold text-color-secondary">설명</span>
|
|
<div class="mt-2">{{ eventModel.description }}</div>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<!-- 참가자 관리 탭 -->
|
|
<TabPanel value="참가자 관리">
|
|
<ParticipantManagement
|
|
:eventId="eventModel.id"
|
|
:availableMembers="availableMembers"
|
|
@participant-updated="handleParticipantUpdated"
|
|
/>
|
|
</TabPanel>
|
|
|
|
<!-- 점수 관리 탭 -->
|
|
<TabPanel value="점수 관리">
|
|
<ScoreManagement
|
|
:eventId="eventModel.id"
|
|
:participants="eventModel.participants || []"
|
|
:maxGames="eventModel.gameCount"
|
|
@score-updated="$emit('score-updated')"
|
|
/>
|
|
</TabPanel>
|
|
<!-- 팀 편성 탭 -->
|
|
<TabPanel v-if="hasTeamFeature" value="팀 편성">
|
|
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @save-teams="handleSaveTeams" />
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<Button label="수정" icon="pi pi-pencil" class="p-button-text" @click="$emit('edit')" />
|
|
</div>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue';
|
|
import TeamGeneration from './TeamGeneration.vue';
|
|
import Dialog from 'primevue/dialog';
|
|
import ParticipantManagement from './ParticipantManagement.vue';
|
|
import ScoreManagement from './ScoreManagement.vue';
|
|
|
|
const props = defineProps({
|
|
visible: { type: Boolean, required: true },
|
|
event: { type: Object, default: () => ({}) },
|
|
getEventTypeName: { type: Function, required: true },
|
|
getStatusName: { type: Function, required: true },
|
|
getStatusSeverity: { type: Function, required: true },
|
|
formatDate: { type: Function, required: true },
|
|
// 팀 기능 사용 권한 여부
|
|
hasTeamFeature: { type: Boolean, default: false },
|
|
|
|
// (권장) 상태명이 매칭 안 될 경우 실제 상태 문자열 반환
|
|
// getStatusName 함수 예시:
|
|
// (status) => STATUS_LABELS[status] || status
|
|
|
|
availableMembers: { type: Array, default: () => [] }
|
|
});
|
|
|
|
const emit = defineEmits(['update:visible', 'hide', 'edit', 'participant-updated', 'score-updated']);
|
|
|
|
const eventModel = computed(() => props.event || {});
|
|
const formattedStartDate = computed(() => props.formatDate(eventModel.value.startDate));
|
|
const formattedEndDate = computed(() => props.formatDate(eventModel.value.endDate));
|
|
|
|
// 공개 URL 전체 경로
|
|
const publicEventUrl = computed(() => {
|
|
if (!eventModel.value.publicHash) return '';
|
|
// 실제 서비스 도메인에 맞게 수정 가능
|
|
return `${window.location.origin}/public/${eventModel.value.publicHash}`;
|
|
});
|
|
|
|
function copyPublicUrl() {
|
|
if (!publicEventUrl.value) return;
|
|
navigator.clipboard.writeText(publicEventUrl.value).then(() => {
|
|
toast.add({ severity: 'info', summary: 'URL 복사', detail: '공개 URL이 복사되었습니다.', life: 1500 });
|
|
});
|
|
}
|
|
|
|
const hideDialog = () => {
|
|
emit('hide');
|
|
};
|
|
// 팀 생성 결과 저장
|
|
const teams = ref([]);
|
|
|
|
// 이벤트 상세 다이얼로그가 열릴 때마다 팀 목록 불러오기 (팀 기능 사용 권한이 있는 경우에만)
|
|
watch(() => eventModel.value.id, async (eventId) => {
|
|
if (eventId && props.hasTeamFeature) {
|
|
try {
|
|
const latestTeams = await teamService.getTeams(eventId);
|
|
teams.value = latestTeams;
|
|
} catch (error) {
|
|
console.error('팀 정보를 불러오는 중 오류가 발생했습니다:', error);
|
|
teams.value = [];
|
|
}
|
|
}
|
|
}, { immediate: true });
|
|
|
|
// 참가확정 참가자 목록을 계산하는 computed 속성
|
|
const confirmedParticipants = ref([]);
|
|
function updateConfirmedParticipants() {
|
|
const arr = eventModel.value.EventParticipants?.filter(p => p.status === '참가확정' || p.status === '참가예정') || [];
|
|
confirmedParticipants.value = arr;
|
|
}
|
|
// 최초 및 eventModel 변경 시 동기화
|
|
watch(() => eventModel.value.EventParticipants, updateConfirmedParticipants, { immediate: true, deep: true });
|
|
|
|
// 참가확정 인원 수
|
|
const confirmedCount = computed(() => confirmedParticipants.value.length);
|
|
|
|
|
|
// 팀 배정 저장 핸들러
|
|
import teamService from '@/services/teamService';
|
|
import { useToast } from 'primevue/usetoast';
|
|
const toast = useToast();
|
|
|
|
async function handleSaveTeams(savedTeams) {
|
|
// 팀 기능 사용 권한이 없는 경우 저장하지 않음
|
|
if (!props.hasTeamFeature) {
|
|
toast.add({ severity: 'error', summary: '권한 없음', detail: '팀 기능을 사용할 수 있는 권한이 없습니다.', life: 3000 });
|
|
return;
|
|
}
|
|
|
|
const eventId = eventModel.value.id;
|
|
try {
|
|
await teamService.saveTeams(eventId, savedTeams);
|
|
toast.add({ severity: 'success', summary: '저장 완료', detail: '팀 배정이 저장되었습니다.', life: 2000 });
|
|
// 저장 후 최신 팀 목록 불러오기
|
|
const latestTeams = await teamService.getTeams(eventId);
|
|
teams.value = latestTeams;
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: '저장 실패', detail: '팀 저장 중 오류가 발생했습니다.', life: 3000 });
|
|
}
|
|
}
|
|
|
|
// 참가자 상태 변경 처리 함수
|
|
function handleParticipantUpdated(data) {
|
|
// 부모 컴포넌트에 이벤트 전달
|
|
emit('participant-updated', data);
|
|
// 전체 참가자 목록이 있는 경우 직접 업데이트
|
|
if (data.allParticipants) {
|
|
// EventParticipants 속성을 완전히 대체
|
|
eventModel.value.EventParticipants = [...data.allParticipants];
|
|
// 참가확정 참가자도 즉시 반영
|
|
updateConfirmedParticipants();
|
|
}
|
|
};
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
:deep(.mini-inputnumber .p-inputnumber-input) {
|
|
width: 60px !important;
|
|
min-width: 0 !important;
|
|
max-width: 100% !important;
|
|
}
|
|
</style>
|
|
|
|
<style scoped>
|
|
.team-generation-section {
|
|
margin-top: 1rem;
|
|
}
|
|
.team-box {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
|
}
|
|
.event-details {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.event-details-header {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.event-details-section {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.event-details-section h4 {
|
|
margin-bottom: 1rem;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.dialog-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
:deep(.p-dialog-content) {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
:deep(.p-tabview-panels) {
|
|
padding: 1.5rem !important;
|
|
}
|
|
</style>
|