정리
This commit is contained in:
@@ -1260,14 +1260,30 @@ exports.getEventParticipants = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 이벤트 참가자 등록
|
// 참가자 정보 핵심 update 함수 (중복 제거)
|
||||||
|
async function updateEventParticipantCore({ id, eventId, memberId, clubId, status, paymentStatus, comment }) {
|
||||||
|
const [updatedCount] = await EventParticipant.update(
|
||||||
|
{ status, paymentStatus, comment },
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
eventId,
|
||||||
|
memberId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return updatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이벤트 참가자 등록 (이미 있으면 update, 없으면 create)
|
||||||
exports.addEventParticipant = async (req, res) => {
|
exports.addEventParticipant = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
clubId,
|
clubId,
|
||||||
memberId,
|
memberId,
|
||||||
status,
|
status,
|
||||||
paymentStatus
|
paymentStatus,
|
||||||
|
comment
|
||||||
} = req.body;
|
} = req.body;
|
||||||
const eventId = req.params.id;
|
const eventId = req.params.id;
|
||||||
if (!eventId || !clubId || !memberId) {
|
if (!eventId || !clubId || !memberId) {
|
||||||
@@ -1311,51 +1327,58 @@ exports.addEventParticipant = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let participant;
|
||||||
|
let isUpdate = false;
|
||||||
if (existingParticipant) {
|
if (existingParticipant) {
|
||||||
return res.status(400).json({ message: '이미 참가 신청한 사용자입니다.' });
|
// 이미 있으면 update (핵심 update 함수 활용)
|
||||||
}
|
await updateEventParticipantCore({
|
||||||
|
id: existingParticipant.id,
|
||||||
// 참가자 수 제한 확인
|
eventId,
|
||||||
if (event.maxParticipants > 0) {
|
memberId,
|
||||||
const currentParticipants = await EventParticipant.count({
|
clubId,
|
||||||
where: {
|
status: status || existingParticipant.status,
|
||||||
eventId,
|
paymentStatus: paymentStatus || existingParticipant.paymentStatus,
|
||||||
status: { [Op.ne]: '취소' }
|
comment: comment || existingParticipant.comment
|
||||||
}
|
});
|
||||||
|
participant = await EventParticipant.findOne({
|
||||||
|
where: { id: existingParticipant.id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Member,
|
||||||
|
attributes: ['id', 'name']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
isUpdate = true;
|
||||||
|
} else {
|
||||||
|
// 없으면 create
|
||||||
|
participant = await EventParticipant.create({
|
||||||
|
eventId,
|
||||||
|
memberId,
|
||||||
|
status: status || '참가예정',
|
||||||
|
paymentStatus: paymentStatus || '미납',
|
||||||
|
comment: comment || null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
});
|
||||||
|
participant = await EventParticipant.findOne({
|
||||||
|
where: { id: participant.id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: Member,
|
||||||
|
attributes: ['id', 'name']
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentParticipants >= event.maxParticipants) {
|
|
||||||
return res.status(400).json({ message: '참가자 수가 초과되었습니다.' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const participant = await EventParticipant.create({
|
|
||||||
eventId,
|
|
||||||
memberId,
|
|
||||||
status: status || '참가예정',
|
|
||||||
paymentStatus: paymentStatus || '미납',
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
});
|
|
||||||
|
|
||||||
// 참가자 정보 조회
|
|
||||||
const participantWithDetails = await EventParticipant.findOne({
|
|
||||||
where: { id: participant.id },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: Member,
|
|
||||||
attributes: ['id', 'name']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
message: '참가자가 등록되었습니다.',
|
message: isUpdate ? '기존 참가자 정보를 수정했습니다.' : '참가자가 등록되었습니다.',
|
||||||
participant: participantWithDetails
|
participant
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('참가자 등록 오류:', error);
|
console.error('참가자 등록/수정 오류:', error);
|
||||||
res.status(500).json({ message: '참가자 등록 중 오류가 발생했습니다.' });
|
res.status(500).json({ message: '참가자 등록/수정 중 오류가 발생했습니다.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1363,7 +1386,7 @@ exports.addEventParticipant = async (req, res) => {
|
|||||||
exports.updateEventParticipant = async (req, res) => {
|
exports.updateEventParticipant = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const eventId = req.params.id;
|
const eventId = req.params.id;
|
||||||
const { id, clubId, memberId, status, paymentStatus } = req.body;
|
const { id, clubId, memberId, status, paymentStatus, comment } = req.body;
|
||||||
|
|
||||||
if (!eventId || !clubId || !memberId || !id) {
|
if (!eventId || !clubId || !memberId || !id) {
|
||||||
return res.status(400).json({ message: '이벤트 ID, 클럽 ID, 사용자 ID가 필요합니다.' });
|
return res.status(400).json({ message: '이벤트 ID, 클럽 ID, 사용자 ID가 필요합니다.' });
|
||||||
@@ -1384,7 +1407,7 @@ exports.updateEventParticipant = async (req, res) => {
|
|||||||
|
|
||||||
// 참가자 정보 수정
|
// 참가자 정보 수정
|
||||||
const [updatedCount] = await EventParticipant.update(
|
const [updatedCount] = await EventParticipant.update(
|
||||||
{ status, paymentStatus },
|
{ status, paymentStatus, comment },
|
||||||
{
|
{
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ const EventParticipant = sequelize.define('EventParticipant', {
|
|||||||
}, {
|
}, {
|
||||||
tableName: 'EventParticipants',
|
tableName: 'EventParticipants',
|
||||||
comment: '이벤트별 참가자 정보를 저장하는 테이블',
|
comment: '이벤트별 참가자 정보를 저장하는 테이블',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
unique: true,
|
||||||
|
fields: ['eventId', 'memberId'],
|
||||||
|
name: 'unique_event_member'
|
||||||
|
}
|
||||||
|
],
|
||||||
timestamps: true
|
timestamps: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const syncModels = async () => {
|
|||||||
// await sequelize.query('SET FOREIGN_KEY_CHECKS = 1');
|
// await sequelize.query('SET FOREIGN_KEY_CHECKS = 1');
|
||||||
|
|
||||||
// 운영 alter: true - 구조변경만 alter처리
|
// 운영 alter: true - 구조변경만 alter처리
|
||||||
await sequelize.sync({ alter: true });
|
// await sequelize.sync({ alter: true });
|
||||||
// await sequelize.sync();
|
await sequelize.sync();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('모델 동기화 중 오류 발생:', error);
|
console.error('모델 동기화 중 오류 발생:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ module.exports = {
|
|||||||
const clubSubscriptions = [];
|
const clubSubscriptions = [];
|
||||||
for (let clubId = 1; clubId <= 5; clubId++) {
|
for (let clubId = 1; clubId <= 5; clubId++) {
|
||||||
const planId = Math.floor(Math.random() * 2) + 1; // 랜덤으로 플랜 1, 2 중 하나 선택
|
const planId = Math.floor(Math.random() * 2) + 1; // 랜덤으로 플랜 1, 2 중 하나 선택
|
||||||
const startDate = new Date(2025, 3, 1 + clubId);
|
const startDate = new Date(2025, 4, 1 + clubId);
|
||||||
const endDate = new Date(startDate.getFullYear(), startDate.getMonth()+1, startDate.getDate());
|
const endDate = new Date(startDate.getFullYear(), startDate.getMonth()+1, startDate.getDate());
|
||||||
clubSubscriptions.push({
|
clubSubscriptions.push({
|
||||||
clubId,
|
clubId,
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ module.exports = {
|
|||||||
visible: true,
|
visible: true,
|
||||||
parentId: clubMenuId,
|
parentId: clubMenuId,
|
||||||
featureCode: 'member_management',
|
featureCode: 'member_management',
|
||||||
requiredSubscriptionTier: 'basic',
|
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,70 +7,81 @@
|
|||||||
:modal="true"
|
:modal="true"
|
||||||
class="event-details-dialog"
|
class="event-details-dialog"
|
||||||
>
|
>
|
||||||
<TabView>
|
<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 value="팀 편성"><i class="pi pi-sitemap mr-2" />팀 편성</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
<!-- 기본 정보 탭 -->
|
<!-- 기본 정보 탭 -->
|
||||||
<TabPanel header="기본 정보">
|
<TabPanel value="기본정보">
|
||||||
<div class="event-details">
|
<div class="event-details p-3">
|
||||||
<div class="event-details-header">
|
<div class="flex align-items-center gap-2 mb-3">
|
||||||
<Badge :value="getEventTypeName(eventModel.eventType)" :severity="getStatusSeverity(eventModel.status)" />
|
<Badge :value="getEventTypeName(eventModel.eventType)" :severity="getStatusSeverity(eventModel.status)" />
|
||||||
<Badge :value="getStatusName(eventModel.status)" :severity="getStatusSeverity(eventModel.status)" />
|
<Badge :value="getStatusName(eventModel.status)" :severity="getStatusSeverity(eventModel.status)" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card surface-100 p-3 mb-3">
|
||||||
<div class="event-details-content">
|
|
||||||
<div class="event-details-section">
|
|
||||||
<h4>이벤트 정보</h4>
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6 mb-2">
|
||||||
<p><strong>시작일:</strong> {{ formattedStartDate }}</p>
|
<span class="font-bold text-color-secondary">시작일:</span>
|
||||||
<p><strong>종료일:</strong> {{ formattedEndDate }}</p>
|
<span class="ml-2">{{ formattedStartDate }}</span>
|
||||||
<p><strong>장소:</strong> {{ eventModel.location }}</p>
|
|
||||||
<p v-if="eventModel.publicHash">
|
|
||||||
<strong>공개 URL:</strong>
|
|
||||||
<InputText :value="publicEventUrl" readonly style="width:70%" class="mr-2" />
|
|
||||||
<Button icon="pi pi-copy" @click="copyPublicUrl" class="p-button-sm" v-tooltip.bottom="'복사'" />
|
|
||||||
</p>
|
|
||||||
<p v-if="eventModel.accessPassword">
|
|
||||||
<strong>공개 비밀번호:</strong> {{ eventModel.accessPassword }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6 mb-2">
|
||||||
<p><strong>최대 참가자 수:</strong> {{ eventModel.maxParticipants || '제한 없음' }}</p>
|
<span class="font-bold text-color-secondary">종료일:</span>
|
||||||
<p><strong>현재 참가자 수:</strong> {{ eventModel.participants?.length || 0 }}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card surface-100 p-3">
|
||||||
<div class="event-details-section">
|
<span class="font-bold text-color-secondary">설명</span>
|
||||||
<h4>설명</h4>
|
<div class="mt-2">{{ eventModel.description }}</div>
|
||||||
<p>{{ eventModel.description || '설명이 없습니다.' }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabPanel>
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<!-- 참가자 관리 탭 -->
|
<!-- 참가자 관리 탭 -->
|
||||||
<TabPanel header="참가자 관리">
|
<TabPanel value="참가자 관리">
|
||||||
<ParticipantManagement
|
<ParticipantManagement
|
||||||
:eventId="eventModel.id"
|
:eventId="eventModel.id"
|
||||||
:availableMembers="availableMembers"
|
:availableMembers="availableMembers"
|
||||||
@participant-updated="handleParticipantUpdated"
|
@participant-updated="handleParticipantUpdated"
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<!-- 점수 관리 탭 -->
|
<!-- 점수 관리 탭 -->
|
||||||
<TabPanel header="점수 관리">
|
<TabPanel value="점수 관리">
|
||||||
<ScoreManagement
|
<ScoreManagement
|
||||||
:eventId="eventModel.id"
|
:eventId="eventModel.id"
|
||||||
:participants="eventModel.participants || []"
|
:participants="eventModel.participants || []"
|
||||||
:maxGames="eventModel.gameCount"
|
:maxGames="eventModel.gameCount"
|
||||||
@score-updated="$emit('score-updated')"
|
@score-updated="$emit('score-updated')"
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<!-- 팀 편성 탭 -->
|
<!-- 팀 편성 탭 -->
|
||||||
<TabPanel header="팀 편성">
|
<TabPanel value="팀 편성">
|
||||||
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @update:teams="teams = $event" @teams-generated="teams = $event" @save-teams="handleSaveTeams" />
|
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @save-teams="handleSaveTeams" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabView>
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
@@ -84,15 +95,8 @@
|
|||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import TeamGeneration from './TeamGeneration.vue';
|
import TeamGeneration from './TeamGeneration.vue';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
import TabView from 'primevue/tabview';
|
|
||||||
import TabPanel from 'primevue/tabpanel';
|
|
||||||
import Badge from 'primevue/badge';
|
|
||||||
import Button from 'primevue/button';
|
|
||||||
import InputNumber from 'primevue/inputnumber';
|
|
||||||
import InputText from 'primevue/inputtext';
|
|
||||||
import ParticipantManagement from './ParticipantManagement.vue';
|
import ParticipantManagement from './ParticipantManagement.vue';
|
||||||
import ScoreManagement from './ScoreManagement.vue';
|
import ScoreManagement from './ScoreManagement.vue';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: { type: Boolean, required: true },
|
visible: { type: Boolean, required: true },
|
||||||
@@ -144,11 +148,13 @@ watch(() => eventModel.value.id, async (eventId) => {
|
|||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
// 참가확정 참가자 목록을 계산하는 computed 속성
|
// 참가확정 참가자 목록을 계산하는 computed 속성
|
||||||
const confirmedParticipants = computed(() => {
|
const confirmedParticipants = ref([]);
|
||||||
|
function updateConfirmedParticipants() {
|
||||||
const arr = eventModel.value.EventParticipants?.filter(p => p.status === '참가확정' || p.status === '참가예정') || [];
|
const arr = eventModel.value.EventParticipants?.filter(p => p.status === '참가확정' || p.status === '참가예정') || [];
|
||||||
console.log('[상위] 참가확정:', arr);
|
confirmedParticipants.value = arr;
|
||||||
return arr;
|
}
|
||||||
});
|
// 최초 및 eventModel 변경 시 동기화
|
||||||
|
watch(() => eventModel.value.EventParticipants, updateConfirmedParticipants, { immediate: true, deep: true });
|
||||||
|
|
||||||
// 참가확정 인원 수
|
// 참가확정 인원 수
|
||||||
const confirmedCount = computed(() => confirmedParticipants.value.length);
|
const confirmedCount = computed(() => confirmedParticipants.value.length);
|
||||||
@@ -176,11 +182,12 @@ async function handleSaveTeams(savedTeams) {
|
|||||||
function handleParticipantUpdated(data) {
|
function handleParticipantUpdated(data) {
|
||||||
// 부모 컴포넌트에 이벤트 전달
|
// 부모 컴포넌트에 이벤트 전달
|
||||||
emit('participant-updated', data);
|
emit('participant-updated', data);
|
||||||
|
|
||||||
// 전체 참가자 목록이 있는 경우 직접 업데이트
|
// 전체 참가자 목록이 있는 경우 직접 업데이트
|
||||||
if (data.allParticipants) {
|
if (data.allParticipants) {
|
||||||
// EventParticipants 속성을 완전히 대체
|
// EventParticipants 속성을 완전히 대체
|
||||||
eventModel.value.EventParticipants = [...data.allParticipants];
|
eventModel.value.EventParticipants = [...data.allParticipants];
|
||||||
|
// 참가확정 참가자도 즉시 반영
|
||||||
|
updateConfirmedParticipants();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<Dialog v-model:visible="participantDialog" :header="dialogTitle" modal class="p-fluid" :style="{width: '450px'}">
|
<Dialog v-model:visible="participantDialog" :header="dialogTitle" modal class="p-fluid" :style="{width: '450px'}">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="member">회원</label>
|
<label for="member">회원</label>
|
||||||
<Dropdown
|
<Select
|
||||||
id="member"
|
id="member"
|
||||||
v-model="editingParticipant.memberId"
|
v-model="editingParticipant.memberId"
|
||||||
:options="props.availableMembers"
|
:options="props.availableMembers"
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="status">참가 상태</label>
|
<label for="status">참가 상태</label>
|
||||||
<Dropdown
|
<Select
|
||||||
id="status"
|
id="status"
|
||||||
v-model="editingParticipant.status"
|
v-model="editingParticipant.status"
|
||||||
:options="participantStatusOptions"
|
:options="participantStatusOptions"
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="paymentStatus">결제 상태</label>
|
<label for="paymentStatus">결제 상태</label>
|
||||||
<Dropdown
|
<Select
|
||||||
id="paymentStatus"
|
id="paymentStatus"
|
||||||
v-model="editingParticipant.paymentStatus"
|
v-model="editingParticipant.paymentStatus"
|
||||||
:options="paymentStatusOptions"
|
:options="paymentStatusOptions"
|
||||||
@@ -118,13 +118,6 @@ import { ref, computed, onMounted } from 'vue';
|
|||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import eventService from '@/services/eventService';
|
import eventService from '@/services/eventService';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import DataTable from 'primevue/datatable';
|
|
||||||
import Column from 'primevue/column';
|
|
||||||
import Dialog from 'primevue/dialog';
|
|
||||||
import Button from 'primevue/button';
|
|
||||||
import Dropdown from 'primevue/dropdown';
|
|
||||||
import Badge from 'primevue/badge';
|
|
||||||
import Toast from 'primevue/toast';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
eventId: {
|
eventId: {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
<Dialog v-model:visible="scoreDialog" :header="scoreDialogTitle" modal class="p-fluid" :style="{width: '600px'}">
|
<Dialog v-model:visible="scoreDialog" :header="scoreDialogTitle" modal class="p-fluid" :style="{width: '600px'}">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="participant">참가자</label>
|
<label for="participant">참가자</label>
|
||||||
<Dropdown
|
<Select
|
||||||
id="participant"
|
id="participant"
|
||||||
v-model="editingScore.participantId"
|
v-model="editingScore.participantId"
|
||||||
:options="participants"
|
:options="participants"
|
||||||
@@ -122,7 +122,6 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import eventService from '@/services/eventService';
|
import eventService from '@/services/eventService';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
eventId: {
|
eventId: {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</draggable>
|
</draggable>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="flex align-items-center gap-2 mt-2">
|
<div class="flex align-items-center gap-2 mt-2">
|
||||||
<Dropdown
|
<Select
|
||||||
v-model="team._selectedMember"
|
v-model="team._selectedMember"
|
||||||
:options="availableMembersForTeam(idx)"
|
:options="availableMembersForTeam(idx)"
|
||||||
optionLabel="name"
|
optionLabel="name"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Select>
|
||||||
<Button icon="pi pi-plus" class="p-button-text p-0" :disabled="!team._selectedMember" @click="addMemberToTeam(idx)" />
|
<Button icon="pi pi-plus" class="p-button-text p-0" :disabled="!team._selectedMember" @click="addMemberToTeam(idx)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,11 +89,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import draggable from 'vuedraggable'; // npm install vuedraggable@next 필요 (vue3)
|
import draggable from 'vuedraggable'; // npm install vuedraggable@next 필요 (vue3)
|
||||||
|
|
||||||
import { ref, computed, watch, unref } from 'vue';
|
import { ref, computed, watch, unref } from 'vue';
|
||||||
import InputNumber from 'primevue/inputnumber';
|
|
||||||
import Button from 'primevue/button';
|
|
||||||
import Dropdown from 'primevue/dropdown';
|
|
||||||
|
|
||||||
// 참가자 에버리지 구하기
|
// 참가자 에버리지 구하기
|
||||||
function getMemberAverage(member) {
|
function getMemberAverage(member) {
|
||||||
|
|||||||
+11
-1
@@ -26,10 +26,10 @@ import DataTable from 'primevue/datatable'
|
|||||||
import Column from 'primevue/column'
|
import Column from 'primevue/column'
|
||||||
import Dialog from 'primevue/dialog'
|
import Dialog from 'primevue/dialog'
|
||||||
import Select from 'primevue/select'
|
import Select from 'primevue/select'
|
||||||
|
import MultiSelect from 'primevue/multiselect'
|
||||||
import Toast from 'primevue/toast'
|
import Toast from 'primevue/toast'
|
||||||
import ToastService from 'primevue/toastservice'
|
import ToastService from 'primevue/toastservice'
|
||||||
import Card from 'primevue/card'
|
import Card from 'primevue/card'
|
||||||
import TabPanel from 'primevue/tabpanel'
|
|
||||||
import Menu from 'primevue/menu'
|
import Menu from 'primevue/menu'
|
||||||
import Menubar from 'primevue/menubar'
|
import Menubar from 'primevue/menubar'
|
||||||
import PanelMenu from 'primevue/panelmenu'
|
import PanelMenu from 'primevue/panelmenu'
|
||||||
@@ -46,6 +46,11 @@ import Datepicker from 'primevue/datepicker'
|
|||||||
import Tag from 'primevue/tag'
|
import Tag from 'primevue/tag'
|
||||||
import FloatLabel from 'primevue/floatlabel';
|
import FloatLabel from 'primevue/floatlabel';
|
||||||
import Tooltip from 'primevue/tooltip';
|
import Tooltip from 'primevue/tooltip';
|
||||||
|
import Tabs from 'primevue/tabs';
|
||||||
|
import TabPanels from 'primevue/tabpanels';
|
||||||
|
import TabPanel from 'primevue/tabpanel';
|
||||||
|
import TabList from 'primevue/tablist';
|
||||||
|
import Tab from 'primevue/tab';
|
||||||
import {ko} from 'primelocale/js/ko.js';
|
import {ko} from 'primelocale/js/ko.js';
|
||||||
|
|
||||||
import AppHeader from './components/AppHeader.vue';
|
import AppHeader from './components/AppHeader.vue';
|
||||||
@@ -76,9 +81,14 @@ app.component('DataTable', DataTable)
|
|||||||
app.component('Column', Column)
|
app.component('Column', Column)
|
||||||
app.component('Dialog', Dialog)
|
app.component('Dialog', Dialog)
|
||||||
app.component('Select', Select)
|
app.component('Select', Select)
|
||||||
|
app.component('MultiSelect', MultiSelect)
|
||||||
app.component('Toast', Toast)
|
app.component('Toast', Toast)
|
||||||
app.component('Card', Card)
|
app.component('Card', Card)
|
||||||
|
app.component('Tabs', Tabs)
|
||||||
|
app.component('TabPanels', TabPanels)
|
||||||
app.component('TabPanel', TabPanel)
|
app.component('TabPanel', TabPanel)
|
||||||
|
app.component('TabList', TabList)
|
||||||
|
app.component('Tab', Tab)
|
||||||
app.component('Menu', Menu)
|
app.component('Menu', Menu)
|
||||||
app.component('Menubar', Menubar)
|
app.component('Menubar', Menubar)
|
||||||
app.component('PanelMenu', PanelMenu)
|
app.component('PanelMenu', PanelMenu)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function fetchEvent(publicHash, password) {
|
|||||||
headers,
|
headers,
|
||||||
body
|
body
|
||||||
});
|
});
|
||||||
if (res.status === 403 || res.status === 401) {
|
if (res.status === 403 || res.status === 401 || res.status === 404) {
|
||||||
removeToken(publicHash);
|
removeToken(publicHash);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -61,42 +61,35 @@
|
|||||||
<Dialog v-model:visible="memberDialog" :style="{width: '450px'}"
|
<Dialog v-model:visible="memberDialog" :style="{width: '450px'}"
|
||||||
:header="dialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
:header="dialogMode === 'new' ? '새 회원 추가' : '회원 정보 수정'"
|
||||||
:modal="true" class="p-fluid">
|
:modal="true" class="p-fluid">
|
||||||
<div class="field">
|
<!-- 회원 정보 다이얼로그 내부 폼 예시 -->
|
||||||
<label for="name">이름</label>
|
<div class="field mb-3">
|
||||||
<InputText id="name" v-model="member.name" required autofocus />
|
<label for="gender" class="mb-1">성별</label>
|
||||||
|
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" placeholder="성별 선택" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="gender">성별</label>
|
<label for="memberType" class="mb-1">회원 유형</label>
|
||||||
<Dropdown id="gender" v-model="member.gender" :options="genderOptions"
|
<Select id="memberType" v-model="member.memberTypeObj" :options="memberTypes" optionLabel="name" placeholder="회원 유형 선택" class="w-full" />
|
||||||
optionLabel="name" placeholder="성별 선택" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="memberType">회원 유형</label>
|
<label for="phone" class="mb-1">전화번호</label>
|
||||||
<Dropdown id="memberType" v-model="member.memberTypeObj" :options="memberTypes"
|
<InputText id="phone" v-model="member.phone" class="w-full" />
|
||||||
optionLabel="name" placeholder="회원 유형 선택" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="phone">전화번호</label>
|
<label for="email" class="mb-1">이메일</label>
|
||||||
<InputText id="phone" v-model="member.phone" />
|
<InputText id="email" v-model="member.email" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="email">이메일</label>
|
<label for="handicap" class="mb-1">핸디캡</label>
|
||||||
<InputText id="email" v-model="member.email" />
|
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field mb-3">
|
||||||
<label for="handicap">핸디캡</label>
|
<label for="status" class="mb-1">상태</label>
|
||||||
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" />
|
<Select id="status" v-model="member.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="flex justify-content-end gap-2 mt-4">
|
||||||
<label for="status">상태</label>
|
|
||||||
<Dropdown id="status" v-model="member.status" :options="statusOptions"
|
|
||||||
optionLabel="label" optionValue="value" placeholder="상태 선택" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
|
<Button label="취소" icon="pi pi-times" class="p-button-text" @click="hideDialog" />
|
||||||
<Button label="저장" icon="pi pi-check" class="p-button-text" @click="saveMember" />
|
<Button label="저장" icon="pi pi-check" class="p-button-text" @click="saveMember" />
|
||||||
</template>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<!-- 삭제 확인 다이얼로그 -->
|
<!-- 삭제 확인 다이얼로그 -->
|
||||||
@@ -177,13 +170,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
import { ref, reactive, computed, onMounted } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import Button from 'primevue/button';
|
|
||||||
import InputText from 'primevue/inputtext';
|
|
||||||
import MultiSelect from 'primevue/multiselect';
|
|
||||||
import InputNumber from 'primevue/inputnumber';
|
|
||||||
import Dialog from 'primevue/dialog';
|
|
||||||
import DataTable from 'primevue/datatable';
|
|
||||||
import Column from 'primevue/column';
|
|
||||||
import apiClient from '@/services/api';
|
import apiClient from '@/services/api';
|
||||||
import clubService from '@/services/clubService';
|
import clubService from '@/services/clubService';
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<Dropdown v-model="filters.team" :options="teamOptions" optionLabel="name"
|
<Select v-model="filters.team" :options="teamOptions" optionLabel="name"
|
||||||
placeholder="팀 선택" class="w-full" />
|
placeholder="팀 선택" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
@@ -118,11 +118,11 @@
|
|||||||
<div class="ranking-filters mb-3">
|
<div class="ranking-filters mb-3">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<Dropdown v-model="rankingType" :options="rankingOptions" optionLabel="name"
|
<Select v-model="rankingType" :options="rankingOptions" optionLabel="name"
|
||||||
placeholder="순위 유형" class="w-full" />
|
placeholder="순위 유형" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-4">
|
<div class="col-12 md:col-4">
|
||||||
<Dropdown v-model="rankingGame" :options="gameRankingOptions" optionLabel="name"
|
<Select v-model="rankingGame" :options="gameRankingOptions" optionLabel="name"
|
||||||
placeholder="게임 선택" class="w-full" />
|
placeholder="게임 선택" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
<div class="export-options">
|
<div class="export-options">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="exportFormat">파일 형식</label>
|
<label for="exportFormat">파일 형식</label>
|
||||||
<Dropdown id="exportFormat" v-model="exportFormat" :options="exportFormatOptions"
|
<Select id="exportFormat" v-model="exportFormat" :options="exportFormatOptions"
|
||||||
optionLabel="name" placeholder="형식 선택" class="w-full" />
|
optionLabel="name" placeholder="형식 선택" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--p-gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-public-wrapper>.p-card>.p-card-body {
|
.event-public-wrapper>.p-card>.p-card-body {
|
||||||
@@ -251,7 +253,11 @@
|
|||||||
.team-score-table-wrapper {
|
.team-score-table-wrapper {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 12px 8px;
|
padding: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score-table-wrapper .p-card-body {
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-public-card {
|
.event-public-card {
|
||||||
@@ -401,7 +407,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.team-name-strong {
|
.team-name-strong {
|
||||||
font-size: 2.3rem;
|
font-size: 2rem;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="event-public-wrapper">
|
<div class="event-public-wrapper">
|
||||||
<h1 v-if="clubName" class="p-text-primary p-text-bold p-text-lg p-mb-2">{{ clubName }}</h1>
|
<h1 v-if="clubName" class="p-text-primary p-text-bold p-text-lg p-mb-2">{{ clubName }}</h1>
|
||||||
<Divider class="p-mb-4" />
|
<div class="event-public-card">
|
||||||
<div class="p-shadow-8 p-p-5 p-rounded-xl event-public-card">
|
<div v-if="loading" class="p-text-center p-mb-4">로딩 중...</div>
|
||||||
<div>
|
<div v-else-if="error" class="p-text-center p-mb-4">{{ error }}</div>
|
||||||
<div v-if="loading" class="p-text-center p-mb-4">로딩 중...</div>
|
<div v-else-if="!isAuthenticated">
|
||||||
<div v-else-if="error" class="p-text-center p-mb-4">{{ error }}</div>
|
<Card>
|
||||||
<div v-else-if="!isAuthenticated">
|
<template #content>
|
||||||
<div class="event-password-form flex flex-column p-ai-center p-mt-6 p-mb-6">
|
<div class="event-password-form flex flex-column p-ai-center p-mt-6 p-mb-6">
|
||||||
<i class="pi pi-lock p-mb-3 event-password-lock"></i>
|
<i class="pi pi-lock p-mb-3 event-password-lock"></i>
|
||||||
<div class="p-text-center p-mb-3 p-text-lg event-password-desc">이 이벤트는 비밀번호가 필요합니다.<br>비밀번호를 입력해 주세요.</div>
|
<div class="p-text-center p-mb-3 p-text-lg event-password-desc">이 이벤트는 비밀번호가 필요합니다.<br>비밀번호를 입력해 주세요.</div>
|
||||||
<InputText v-model="inputPassword" type="password" placeholder="비밀번호 입력" class="p-mb-2 event-password-input"
|
<InputText v-model="inputPassword" type="password" placeholder="비밀번호 입력" class="p-mb-2 event-password-input"
|
||||||
@keyup.enter="verifyPassword" autofocus />
|
@keyup.enter="verifyPassword" autofocus />
|
||||||
<Button label="확인" @click="verifyPassword" class="p-mb-2 event-password-btn" :loading="loading" />
|
<Button label="확인" @click="verifyPassword" class="p-mb-2 event-password-btn" :loading="loading" />
|
||||||
<div v-if="passwordError" class="p-error p-mt-1 event-password-error">{{ passwordError }}</div>
|
<div v-if="passwordError" class="p-error p-mt-1 event-password-error">{{ passwordError }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div v-else>
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<template v-if="event">
|
||||||
|
<Divider class="p-mb-4" />
|
||||||
<div class="event-tag">
|
<div class="event-tag">
|
||||||
<Tag :value="event.eventType" class="event-type-tag" />
|
<Tag :value="event.eventType" class="event-type-tag" />
|
||||||
<Tag :value="event.status"
|
<Tag :value="event.status"
|
||||||
:severity="event.status === '완료' ? 'success' : event.status === '활성' ? 'info' : 'warning'"
|
:severity="event.status === '완료' ? 'success' : event.status === '활성' ? 'info' : 'warning'"
|
||||||
class="p-ml-4" />
|
class="ml-1" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex justify-content-end gap-2">
|
<div class="flex justify-content-end gap-2">
|
||||||
@@ -57,8 +61,10 @@
|
|||||||
<div class="p-text-secondary p-mb-4">{{ event.description || '' }}</div>
|
<div class="p-text-secondary p-mb-4">{{ event.description || '' }}</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<!-- 팀별 게임별 점수/등수 표 -->
|
<!-- 팀별 게임별 점수/등수 표 -->
|
||||||
<template v-if="teams.length > 0">
|
</div>
|
||||||
<div class="team-score-table-wrapper p-mt-4">
|
<template v-if="teams.length > 0">
|
||||||
|
<Card class="team-score-table-wrapper mb-4">
|
||||||
|
<template #content>
|
||||||
<DataTable :value="getAllTeamsWithUnassigned()" class="team-score-table" :responsiveLayout="'scroll'">
|
<DataTable :value="getAllTeamsWithUnassigned()" class="team-score-table" :responsiveLayout="'scroll'">
|
||||||
<Column header="팀">
|
<Column header="팀">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
@@ -88,13 +94,65 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</div>
|
</template>
|
||||||
<div v-for="team in getAllTeamsWithUnassigned()" :key="team.id">
|
</Card>
|
||||||
|
<div v-for="team in getAllTeamsWithUnassigned()" :key="team.id" class="mb-4">
|
||||||
|
<Card>
|
||||||
|
<template #content>
|
||||||
|
<MemberListCard
|
||||||
|
:title="parseInt(team.teamNumber) ? '팀 ' + team.teamNumber : '미배정 인원'"
|
||||||
|
:members="team.members"
|
||||||
|
:teamHandicap="team.handicap"
|
||||||
|
:teamNumber="team.teamNumber"
|
||||||
|
:gameCount="event.gameCount"
|
||||||
|
:canInputScore="canInputScore"
|
||||||
|
:scoresProxy="scoresProxy"
|
||||||
|
:scoreStates="scoreStates"
|
||||||
|
:onScoreInput="onScoreInput"
|
||||||
|
:onScoreBlur="onScoreBlur"
|
||||||
|
:onScoreEnter="onScoreEnter"
|
||||||
|
:getMemberTotalScore="getMemberTotalScore"
|
||||||
|
:getMemberAverageScore="getMemberAverageScore"
|
||||||
|
:scoreColorClass="scoreColorClass"
|
||||||
|
/>
|
||||||
|
<!-- 팀 카드 요약(총점/등수/게임별 점수)은 '팀'만 노출 -->
|
||||||
|
<Divider />
|
||||||
|
<div v-if="team.teamNumber" class="team-game-scores">
|
||||||
|
<div class="team-total-row team-total-row-responsive team-total-row-block">
|
||||||
|
<div class="team-games-grid"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${event.gameCount > 3 ? 3 : event.gameCount}, 1fr)` }">
|
||||||
|
<template v-for="n in event.gameCount" :key="n">
|
||||||
|
<div class="team-game-cell">
|
||||||
|
<b>{{ n }}G:</b> {{ getTeamTotalScore(team, n) }}
|
||||||
|
<span v-if="getRanksByGame(n)[(team.id || team.teamNumber) + ''] !== undefined"
|
||||||
|
class="rank-badge"
|
||||||
|
:class="'rank-' + getRanksByGame(n)[(team.id || team.teamNumber) + '']">
|
||||||
|
({{ getRanksByGame(n)[(team.id || team.teamNumber) + ''] }}위)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="team-total-main">
|
||||||
|
<span class="team-total-label">총점</span>
|
||||||
|
<span class="team-total-scores">{{ Array.from({ length: event.gameCount }, (_, i) => getTeamTotalScore(team, i + 1)).reduce((a, b) => a + b, 0) }}</span>
|
||||||
|
<span v-if="getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })"
|
||||||
|
class="rank-badge"
|
||||||
|
:class="'rank-' + getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })">
|
||||||
|
{{ getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' }) }}위
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Card>
|
||||||
|
<template #content>
|
||||||
<MemberListCard
|
<MemberListCard
|
||||||
:title="parseInt(team.teamNumber) ? '팀 ' + team.teamNumber : '미배정 인원'"
|
title="참가자"
|
||||||
:members="team.members"
|
:members="sortedParticipants"
|
||||||
:teamHandicap="team.handicap"
|
|
||||||
:teamNumber="team.teamNumber"
|
|
||||||
:gameCount="event.gameCount"
|
:gameCount="event.gameCount"
|
||||||
:canInputScore="canInputScore"
|
:canInputScore="canInputScore"
|
||||||
:scoresProxy="scoresProxy"
|
:scoresProxy="scoresProxy"
|
||||||
@@ -106,53 +164,10 @@
|
|||||||
:getMemberAverageScore="getMemberAverageScore"
|
:getMemberAverageScore="getMemberAverageScore"
|
||||||
:scoreColorClass="scoreColorClass"
|
:scoreColorClass="scoreColorClass"
|
||||||
/>
|
/>
|
||||||
<!-- 팀 카드 요약(총점/등수/게임별 점수)은 '팀'만 노출 -->
|
</template>
|
||||||
<div v-if="team.teamNumber" class="team-game-scores">
|
</Card>
|
||||||
<div class="team-total-row team-total-row-responsive team-total-row-block">
|
</template>
|
||||||
<div class="team-games-grid"
|
</template>
|
||||||
:style="{ gridTemplateColumns: `repeat(${event.gameCount > 3 ? 3 : event.gameCount}, 1fr)` }">
|
|
||||||
<template v-for="n in event.gameCount" :key="n">
|
|
||||||
<div class="team-game-cell">
|
|
||||||
<b>{{ n }}G:</b> {{ getTeamTotalScore(team, n) }}
|
|
||||||
<span v-if="getRanksByGame(n)[(team.id || team.teamNumber) + ''] !== undefined"
|
|
||||||
class="rank-badge"
|
|
||||||
:class="'rank-' + getRanksByGame(n)[(team.id || team.teamNumber) + '']">
|
|
||||||
({{ getRanksByGame(n)[(team.id || team.teamNumber) + ''] }}위)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="team-total-main">
|
|
||||||
<span class="team-total-label">총점</span>
|
|
||||||
<span class="team-total-scores">{{ Array.from({ length: event.gameCount }, (_, i) => getTeamTotalScore(team, i + 1)).reduce((a, b) => a + b, 0) }}</span>
|
|
||||||
<span v-if="getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })"
|
|
||||||
class="rank-badge"
|
|
||||||
:class="'rank-' + getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' })">
|
|
||||||
{{ getTeamOverallRank({ ...team, id: (team.id || team.teamNumber) + '' }) }}위
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<MemberListCard
|
|
||||||
title="참가자"
|
|
||||||
:members="sortedParticipants"
|
|
||||||
:gameCount="event.gameCount"
|
|
||||||
:canInputScore="canInputScore"
|
|
||||||
:scoresProxy="scoresProxy"
|
|
||||||
:scoreStates="scoreStates"
|
|
||||||
:onScoreInput="onScoreInput"
|
|
||||||
:onScoreBlur="onScoreBlur"
|
|
||||||
:onScoreEnter="onScoreEnter"
|
|
||||||
:getMemberTotalScore="getMemberTotalScore"
|
|
||||||
:getMemberAverageScore="getMemberAverageScore"
|
|
||||||
:scoreColorClass="scoreColorClass"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<JoinDialog v-if="showJoinDialog && canJoin" v-model:visible="showJoinDialog" :publicHash="publicHash"
|
<JoinDialog v-if="showJoinDialog && canJoin" v-model:visible="showJoinDialog" :publicHash="publicHash"
|
||||||
@@ -170,6 +185,7 @@ import MemberListCard from '@/components/public/MemberListCard.vue';
|
|||||||
import JoinDialog from '@/components/public/JoinDialog.vue';
|
import JoinDialog from '@/components/public/JoinDialog.vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import PublicEventService from '@/services/PublicEventService';
|
import PublicEventService from '@/services/PublicEventService';
|
||||||
|
import Panel from 'primevue/panel';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const publicHash = route.params.publicHash;
|
const publicHash = route.params.publicHash;
|
||||||
@@ -346,18 +362,22 @@ async function fetchEventData(password) {
|
|||||||
try {
|
try {
|
||||||
const { status, data } = await PublicEventService.fetchEvent(publicHash, password);
|
const { status, data } = await PublicEventService.fetchEvent(publicHash, password);
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
console.log('[fetchEventData] 403', { publicHash, password });
|
|
||||||
error.value = '이벤트에 접근할 수 없습니다.';
|
error.value = '이벤트에 접근할 수 없습니다.';
|
||||||
PublicEventService.removeToken(publicHash);
|
PublicEventService.removeToken(publicHash);
|
||||||
|
event.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
console.log('[fetchEventData] 401', { publicHash, password });
|
|
||||||
isAuthenticated.value = false;
|
isAuthenticated.value = false;
|
||||||
PublicEventService.removeToken(publicHash);
|
PublicEventService.removeToken(publicHash);
|
||||||
|
event.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status === 404 || (data && data.message === '이벤트를 찾을 수 없습니다.')) {
|
||||||
|
error.value = data && data.message ? data.message : '존재하지 않는 이벤트입니다.';
|
||||||
|
event.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[fetchEventData] 200', data);
|
|
||||||
clubName.value = data.clubName || '';
|
clubName.value = data.clubName || '';
|
||||||
event.value = data.event;
|
event.value = data.event;
|
||||||
teams.value = data.teams || [];
|
teams.value = data.teams || [];
|
||||||
@@ -393,6 +413,7 @@ async function fetchEventData(password) {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.message;
|
error.value = e.message;
|
||||||
|
event.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user