랭킹표시 수정
This commit is contained in:
@@ -84,11 +84,11 @@ exports.getEventByPublicHash = async (event, token = null) => {
|
|||||||
try {
|
try {
|
||||||
const allMembers = await Member.findAll({
|
const allMembers = await Member.findAll({
|
||||||
where: { clubId: event.clubId },
|
where: { clubId: event.clubId },
|
||||||
attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'average']
|
attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'handicap', 'average']
|
||||||
});
|
});
|
||||||
const participants = await EventParticipant.findAll({
|
const participants = await EventParticipant.findAll({
|
||||||
where: { eventId: event.id, status: { [Op.ne]: '취소' } },
|
where: { eventId: event.id, status: { [Op.ne]: '취소' } },
|
||||||
include: [ { model: Member, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'average'] } ]
|
include: [ { model: Member, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'handicap', 'average'] } ]
|
||||||
});
|
});
|
||||||
const allMemberObjs = allMembers.map(m => m.dataValues);
|
const allMemberObjs = allMembers.map(m => m.dataValues);
|
||||||
const participantMemberIds = [
|
const participantMemberIds = [
|
||||||
@@ -293,7 +293,7 @@ exports.addPublicGuest = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 여성 핸디캡 적용
|
// 여성 핸디캡 적용
|
||||||
let handicap = null;
|
let handicap = 0;
|
||||||
if (gender === '여성' && club.femaleHandicap) {
|
if (gender === '여성' && club.femaleHandicap) {
|
||||||
handicap = club.femaleHandicap;
|
handicap = club.femaleHandicap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<Toast />
|
||||||
<template v-if="isRouterReady">
|
<template v-if="isRouterReady">
|
||||||
<AppPublicLayout v-if="isPublicLayout">
|
<AppPublicLayout v-if="isPublicLayout">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6 mb-2">
|
<div class="col-12 md:col-6 mb-2">
|
||||||
<span class="font-bold text-color-secondary">참가자 수:</span>
|
<span class="font-bold text-color-secondary">참가자 수:</span>
|
||||||
<span class="ml-2">{{ eventModel.currentParticipants }} / {{ eventModel.maxParticipants }}</span>
|
<span class="ml-2">{{ confirmedCount }} / {{ eventModel.maxParticipants }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6 mb-2 flex align-items-center">
|
<div class="col-12 md:col-6 mb-2 flex align-items-center">
|
||||||
<span class="font-bold text-color-secondary">공개 URL:</span>
|
<span class="font-bold text-color-secondary">공개 URL:</span>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 md:col-6 mb-2">
|
<div class="col-12 md:col-6 mb-2">
|
||||||
<span class="font-bold text-color-secondary">공개 비밀번호:</span>
|
<span class="font-bold text-color-secondary">공개 비밀번호:</span>
|
||||||
<span class="ml-2">{{ eventModel.publicPassword }}</span>
|
<span class="ml-2">{{ eventModel.accessPassword }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,20 @@
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
:paginator="true"
|
:paginator="true"
|
||||||
:rows="10"
|
:rows="30"
|
||||||
stripedRows
|
stripedRows
|
||||||
v-model:filters="filters"
|
v-model:filters="filters"
|
||||||
|
:sortField="'participant.name'"
|
||||||
|
:sortOrder="1"
|
||||||
>
|
>
|
||||||
|
<!-- 순위 컬럼 추가 -->
|
||||||
|
<Column field="rank" header="순위" sortable style="width: 5rem;">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span :class="'rank-badge rank-' + slotProps.data.rank">
|
||||||
|
{{ slotProps.data.rank || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
<Column field="participant.name" header="참가자" sortable>
|
<Column field="participant.name" header="참가자" sortable>
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
{{ slotProps.data.participant?.name }}
|
{{ slotProps.data.participant?.name }}
|
||||||
@@ -28,9 +38,27 @@
|
|||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 총점 컬럼 추가 -->
|
||||||
|
<Column field="totalScore" header="총점" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="flex flex-column">
|
||||||
|
<span>{{ calculateTotalScore(slotProps.data.games) }}</span>
|
||||||
|
<span class="text-xs text-gray-500" v-if="calculateRawTotalScore(slotProps.data.games) !== '-' && calculateTotalHandicap(slotProps.data.games) > 0">
|
||||||
|
({{ calculateRawTotalScore(slotProps.data.games) }}+{{ calculateTotalHandicap(slotProps.data.games) }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
<Column field="average" header="평균" sortable>
|
<Column field="average" header="평균" sortable>
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
{{ calculateAverage(slotProps.data.games) }}
|
<div class="flex flex-column">
|
||||||
|
<span :class="getScoreClass(calculateAverage(slotProps.data.games))">
|
||||||
|
{{ calculateAverage(slotProps.data.games) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500" v-if="calculateRawAverage(slotProps.data.games) !== '-' && calculateTotalHandicap(slotProps.data.games) > 0">
|
||||||
|
({{ calculateRawAverage(slotProps.data.games) }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
<Column header="작업">
|
<Column header="작업">
|
||||||
@@ -55,7 +83,7 @@
|
|||||||
optionValue="id"
|
optionValue="id"
|
||||||
placeholder="참가자 선택"
|
placeholder="참가자 선택"
|
||||||
:class="{'p-invalid': submitted && !editingScore.participantId}"
|
:class="{'p-invalid': submitted && !editingScore.participantId}"
|
||||||
:disabled="!!editingScore.id"
|
:disabled="editingScore.isEdit"
|
||||||
/>
|
/>
|
||||||
<small v-if="submitted && !editingScore.participantId" class="p-error">참가자를 선택해주세요.</small>
|
<small v-if="submitted && !editingScore.participantId" class="p-error">참가자를 선택해주세요.</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,6 +150,7 @@
|
|||||||
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 { calculateIndividualRankings } from '@/utils/rankingUtils';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
eventId: {
|
eventId: {
|
||||||
@@ -368,7 +397,36 @@ const scoreDialogTitle = computed(() => {
|
|||||||
|
|
||||||
const getTotalScore = (game) => {
|
const getTotalScore = (game) => {
|
||||||
if (!game || !game.score || game.score === 0) return null;
|
if (!game || !game.score || game.score === 0) return null;
|
||||||
return game.score + (game.handicap || 0);
|
// 핸디캡 포함 점수가 300점을 넘지 않도록 제한
|
||||||
|
return Math.min(game.score + (game.handicap || 0), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotalScore = (games) => {
|
||||||
|
const validGames = games.filter(game => game && game.score && game.score > 0);
|
||||||
|
if (!validGames.length) return '-';
|
||||||
|
const total = validGames.reduce((sum, game) => sum + getTotalScore(game), 0);
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRawTotalScore = (games) => {
|
||||||
|
const validGames = games.filter(game => game && game.score && game.score > 0);
|
||||||
|
if (!validGames.length) return '-';
|
||||||
|
const total = validGames.reduce((sum, game) => sum + game.score, 0);
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotalHandicap = (games) => {
|
||||||
|
const validGames = games.filter(game => game && game.score && game.score > 0);
|
||||||
|
if (!validGames.length) return 0;
|
||||||
|
const totalHandicap = validGames.reduce((sum, game) => sum + (game.handicap || 0), 0);
|
||||||
|
return totalHandicap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRawAverage = (games) => {
|
||||||
|
const validGames = games.filter(game => game && game.score && game.score > 0);
|
||||||
|
if (!validGames.length) return '-';
|
||||||
|
const total = validGames.reduce((sum, game) => sum + game.score, 0);
|
||||||
|
return (total / validGames.length).toFixed(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateAverage = (games) => {
|
const calculateAverage = (games) => {
|
||||||
@@ -387,6 +445,32 @@ const formatScoreWithHandicap = (game) => {
|
|||||||
return `(${game.score}+${handicap})`;
|
return `(${game.score}+${handicap})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 순위 계산함수
|
||||||
|
const calculateRanks = (groupedData) => {
|
||||||
|
if (!groupedData || groupedData.length === 0) return {};
|
||||||
|
|
||||||
|
// rankingUtils에서 사용하는 형태로 데이터 변환
|
||||||
|
const participants = groupedData.map(item => ({
|
||||||
|
participantId: item.participantId,
|
||||||
|
name: item.participant.name,
|
||||||
|
handicap: calculateTotalHandicap(item.games) / item.games.filter(g => g && g.score > 0).length || 0,
|
||||||
|
birthday: item.participant.birthday || null
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 점수 프록시 데이터 생성
|
||||||
|
const scoresProxy = {};
|
||||||
|
groupedData.forEach(item => {
|
||||||
|
item.games.forEach((game, index) => {
|
||||||
|
if (game && game.score) {
|
||||||
|
scoresProxy[`${item.participantId}-${index + 1}`] = game.score;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// rankingUtils를 사용하여 순위 계산
|
||||||
|
return calculateIndividualRankings(participants, scoresProxy, props.maxGames);
|
||||||
|
};
|
||||||
|
|
||||||
const groupedScores = computed(() => {
|
const groupedScores = computed(() => {
|
||||||
if (!participants.value || !scores.value) {
|
if (!participants.value || !scores.value) {
|
||||||
return [];
|
return [];
|
||||||
@@ -418,7 +502,16 @@ const groupedScores = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.values(grouped);
|
const groupedArray = Object.values(grouped);
|
||||||
|
|
||||||
|
// 순위 계산
|
||||||
|
const ranks = calculateRanks(groupedArray);
|
||||||
|
|
||||||
|
// 순위 정보 추가
|
||||||
|
return groupedArray.map(item => ({
|
||||||
|
...item,
|
||||||
|
rank: ranks[item.participantId] || '-'
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// 점수 변경 시 호출
|
// 점수 변경 시 호출
|
||||||
|
|||||||
@@ -130,13 +130,40 @@ const props = defineProps({
|
|||||||
visible: Boolean, // Dialog의 v-model:visible과 연결
|
visible: Boolean, // Dialog의 v-model:visible과 연결
|
||||||
eventStatus: String
|
eventStatus: String
|
||||||
});
|
});
|
||||||
const emits = defineEmits(['close', 'updated', 'update:visible']);
|
const emits = defineEmits(['close', 'updated', 'update:visible', 'guest-added']);
|
||||||
|
|
||||||
const selectedMember = ref(null);
|
const selectedMember = ref(null);
|
||||||
const memberOptions = computed(() => {
|
const memberOptions = computed(() => {
|
||||||
|
// 참가자 목록에서 참가자 그룹 생성
|
||||||
|
const participantOptions = (props.participants || []).map(m => ({
|
||||||
|
...m,
|
||||||
|
group: '참가자',
|
||||||
|
id: m.participantId,
|
||||||
|
participantId: m.participantId,
|
||||||
|
memberId: m.memberId
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 미신청 그룹 생성 (참가자에 없는 회원만)
|
||||||
|
const unregisteredOptions = (props.availableMembers || [])
|
||||||
|
.filter(m => {
|
||||||
|
// 이미 참가자로 등록된 회원은 제외
|
||||||
|
return !participantOptions.some(p =>
|
||||||
|
p.memberId === m.memberId ||
|
||||||
|
(p.Member && m.name && p.Member.name === m.name)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(m => ({
|
||||||
|
...m,
|
||||||
|
group: '미신청',
|
||||||
|
id: m.memberId,
|
||||||
|
participantId: null,
|
||||||
|
memberId: m.memberId
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 두 그룹 합치기
|
||||||
const baseOptions = [
|
const baseOptions = [
|
||||||
...(props.participants || []).map(m => ({ ...m, group: '참가자', id: m.participantId, participantId: m.participantId, memberId: m.memberId })),
|
...participantOptions,
|
||||||
...(props.availableMembers || []).map(m => ({ ...m, group: '미신청', id: m.memberId, participantId: null, memberId: m.memberId }))
|
...unregisteredOptions
|
||||||
].sort((a, b) => {
|
].sort((a, b) => {
|
||||||
const nameA = a.name || '';
|
const nameA = a.name || '';
|
||||||
const nameB = b.name || '';
|
const nameB = b.name || '';
|
||||||
@@ -185,8 +212,8 @@ import PublicEventService from '@/services/PublicEventService';
|
|||||||
|
|
||||||
const newGuest = ref({ name: '', phone: '', gender: '' });
|
const newGuest = ref({ name: '', phone: '', gender: '' });
|
||||||
const genderOptions = [
|
const genderOptions = [
|
||||||
{ label: '남', value: '남성' },
|
{ label: '남성', value: '남성' },
|
||||||
{ label: '여', value: '여성' }
|
{ label: '여성', value: '여성' }
|
||||||
];
|
];
|
||||||
|
|
||||||
async function addNewGuest() {
|
async function addNewGuest() {
|
||||||
@@ -204,24 +231,66 @@ async function addNewGuest() {
|
|||||||
average: newGuest.value.average
|
average: newGuest.value.average
|
||||||
});
|
});
|
||||||
if (!res || !res.data || !res.data.memberId) throw new Error('게스트 등록 실패');
|
if (!res || !res.data || !res.data.memberId) throw new Error('게스트 등록 실패');
|
||||||
|
|
||||||
// 새 게스트를 멤버 옵션에 추가하고 자동 선택
|
// 새 게스트를 멤버 옵션에 추가하고 자동 선택
|
||||||
const guest = {
|
const guest = {
|
||||||
...res.data,
|
...res.data,
|
||||||
group: '참가자',
|
group: '참가자',
|
||||||
id: res.data.participantId,
|
id: res.data.participantId,
|
||||||
participantId: res.data.participantId,
|
participantId: res.data.participantId,
|
||||||
memberId: res.data.memberId
|
memberId: res.data.memberId,
|
||||||
|
status: res.data.status || '참가예정', // 참가 확정 상태 추가
|
||||||
|
paymentStatus: res.data.paymentStatus || '미납', // 결제 상태 추가
|
||||||
|
Member: {
|
||||||
|
name: newGuest.value.name,
|
||||||
|
gender: newGuest.value.gender,
|
||||||
|
average: newGuest.value.average || 0,
|
||||||
|
handicap: res.data.handicap || 0
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// options에 직접 push (reactivity 보장 위해 availableMembers에도 추가)
|
|
||||||
props.availableMembers.push(guest);
|
// 이미 있는 게스트 업데이트 (선택 가능한 회원 목록)
|
||||||
// 참가자 목록에도 추가
|
const existingGuestIndex = props.availableMembers.findIndex(m =>
|
||||||
props.participants.push(guest);
|
m.name === guest.Member.name && m.group === '게스트');
|
||||||
|
|
||||||
|
// 이미 참가자 목록에 있는 게스트 업데이트
|
||||||
|
const existingParticipantIndex = props.participants.findIndex(p =>
|
||||||
|
p.memberId === guest.memberId ||
|
||||||
|
(p.Member && p.Member.name === guest.Member.name && p.status === '미신청'));
|
||||||
|
|
||||||
|
// 기존 데이터 업데이트 또는 새 데이터 추가
|
||||||
|
if (existingGuestIndex !== -1) {
|
||||||
|
// 기존 게스트 정보 업데이트
|
||||||
|
Object.assign(props.availableMembers[existingGuestIndex], guest);
|
||||||
|
} else {
|
||||||
|
// 새 게스트 추가
|
||||||
|
props.availableMembers.push(guest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingParticipantIndex !== -1) {
|
||||||
|
// 기존 참가자 정보 업데이트
|
||||||
|
Object.assign(props.participants[existingParticipantIndex], guest);
|
||||||
|
} else {
|
||||||
|
// 새 참가자 추가
|
||||||
|
props.participants.push(guest);
|
||||||
|
}
|
||||||
|
|
||||||
// 선택값 세팅
|
// 선택값 세팅
|
||||||
selectedMember.value = guest;
|
selectedMember.value = guest;
|
||||||
selectedParticipantId.value = guest.participantId;
|
selectedParticipantId.value = guest.participantId;
|
||||||
selectedMemberId.value = guest.memberId;
|
selectedMemberId.value = guest.memberId;
|
||||||
|
|
||||||
|
// 참가 상태와 회비 납부 상태 자동 선택
|
||||||
|
console.log(guest);
|
||||||
|
status.value = guest.status;
|
||||||
|
paymentStatus.value = guest.paymentStatus;
|
||||||
|
comment.value = guest.comment || '';
|
||||||
|
|
||||||
// 입력폼 초기화
|
// 입력폼 초기화
|
||||||
newGuest.value = { name: '', phone: '', gender: '', average: '' };
|
newGuest.value = { name: '', phone: '', gender: '', average: '' };
|
||||||
|
|
||||||
|
// 부모 컴포넌트에 이벤트 발생
|
||||||
|
emits('guest-added', guest);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.message || '게스트 등록 중 오류 발생';
|
error.value = e.message || '게스트 등록 중 오류 발생';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
<div v-for="member in members" :key="member.participantId" class="member-row">
|
<div v-for="member in members" :key="member.participantId" class="member-row">
|
||||||
<div class="member-info-block">
|
<div class="member-info-block">
|
||||||
<div class="member-header-row">
|
<div class="member-header-row">
|
||||||
|
<span v-if="member && (member.participantId || member.id) && (memberRanksRef[member.participantId] || memberRanksRef[member.id])"
|
||||||
|
class="medal-icon-container">
|
||||||
|
<i class="pi pi-trophy medal-icon" :class="'medal-' + (memberRanksRef[member.participantId] || memberRanksRef[member.id])"></i>
|
||||||
|
<span class="medal-rank" :class="'medal-rank-' + (memberRanksRef[member.participantId] || memberRanksRef[member.id])">{{ memberRanksRef[member.participantId] || memberRanksRef[member.id] }}</span>
|
||||||
|
</span>
|
||||||
<span class="member-name">{{ member.name }}</span>
|
<span class="member-name">{{ member.name }}</span>
|
||||||
<Tag v-if="member.isGuest" value="G" severity="warning" class="ml-2" />
|
<Tag v-if="member.isGuest" value="G" severity="warning" class="ml-2" />
|
||||||
</div>
|
</div>
|
||||||
@@ -31,6 +36,14 @@
|
|||||||
<template v-for="n in gameCount" :key="n">
|
<template v-for="n in gameCount" :key="n">
|
||||||
<div class="score-input-hc-wrap">
|
<div class="score-input-hc-wrap">
|
||||||
<div class="score-input-status-wrap">
|
<div class="score-input-status-wrap">
|
||||||
|
<!-- 상태 아이콘을 입력 필드 앞으로 이동 -->
|
||||||
|
<span class="score-status-icon-container">
|
||||||
|
<i v-if="scoreStates[member.participantId + '-' + n] === 'saving'" class="pi pi-spin pi-spinner score-status-icon" />
|
||||||
|
<i v-else-if="scoreStates[member.participantId + '-' + n] === 'saved'" class="pi pi-check score-status-icon" />
|
||||||
|
<i v-else-if="scoreStates[member.participantId + '-' + n] === 'error'" class="pi pi-times score-status-icon" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- 점수 입력 필드 -->
|
||||||
<InputText
|
<InputText
|
||||||
v-if="canInputScore"
|
v-if="canInputScore"
|
||||||
v-model.number="scoresProxy[member.participantId + '-' + n]"
|
v-model.number="scoresProxy[member.participantId + '-' + n]"
|
||||||
@@ -43,29 +56,29 @@
|
|||||||
@keydown.enter="onScoreEnter(member, n, teamNumber)"
|
@keydown.enter="onScoreEnter(member, n, teamNumber)"
|
||||||
/>
|
/>
|
||||||
<span v-else>{{ member.scores?.[n - 1] ?? '-' }}</span>
|
<span v-else>{{ member.scores?.[n - 1] ?? '-' }}</span>
|
||||||
|
|
||||||
|
<!-- 핸디캡 포함 점수 표시 -->
|
||||||
<span :class="scoreColorClass(scoreStates[member.participantId + '-' + n], scoresProxy[member.participantId + '-' + n], member.handicap)"
|
<span :class="scoreColorClass(scoreStates[member.participantId + '-' + n], scoresProxy[member.participantId + '-' + n], member.handicap)"
|
||||||
class="score-hc-value score-status-text"
|
class="score-hc-value score-status-text"
|
||||||
:key="scoreStates[member.participantId + '-' + n] + '-' + scoresProxy[member.participantId + '-' + n]">
|
:key="scoreStates[member.participantId + '-' + n] + '-' + scoresProxy[member.participantId + '-' + n]">
|
||||||
{{ (scoresProxy[member.participantId + '-' + n] !== undefined && scoresProxy[member.participantId + '-' + n] !== '') ?
|
{{ (scoresProxy[member.participantId + '-' + n] !== undefined && scoresProxy[member.participantId + '-' + n] !== '') ?
|
||||||
Number(scoresProxy[member.participantId + '-' + n]) + (Number(member.handicap) || 0) : ''
|
Number(scoresProxy[member.participantId + '-' + n]) + (Number(member.handicap) || 0) : ''
|
||||||
}}
|
}}
|
||||||
<i v-if="scoreStates[member.participantId + '-' + n] === 'saving'" class="pi pi-spin pi-spinner score-status-icon" />
|
|
||||||
<i v-else-if="scoreStates[member.participantId + '-' + n] === 'saved'" class="pi pi-check score-status-icon" />
|
|
||||||
<i v-else-if="scoreStates[member.participantId + '-' + n] === 'error'" class="pi pi-times score-status-icon" />
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="member-score-summary">
|
<div class="member-score-summary">
|
||||||
<span>총점: {{ getMemberTotalScore(member) }}</span>
|
<span>총점: {{ getMemberTotalScore(member) }}</span>
|
||||||
<span class="member-score-avg">/ 에버리지: {{ getMemberAverageScore(member) }}</span>
|
<span class="member-score-avg">/ 평균: {{ getMemberAverageScore(member) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { toRefs } from 'vue'
|
import { computed, onMounted, watch, ref } from 'vue';
|
||||||
|
import { calculateIndividualRankings, getRankColorClass } from '@/utils/rankingUtils';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
members: Array,
|
members: Array,
|
||||||
@@ -80,6 +93,127 @@ const props = defineProps({
|
|||||||
onScoreEnter: Function,
|
onScoreEnter: Function,
|
||||||
getMemberTotalScore: Function,
|
getMemberTotalScore: Function,
|
||||||
getMemberAverageScore: Function,
|
getMemberAverageScore: Function,
|
||||||
scoreColorClass: Function
|
scoreColorClass: Function,
|
||||||
|
memberRanks: Object
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 디버깅을 위한 코드
|
||||||
|
// 순위 데이터를 저장할 로컬 변수
|
||||||
|
const localRanks = ref({});
|
||||||
|
|
||||||
|
// 점수 변경 감지를 위한 참조 변수
|
||||||
|
const lastScores = ref({});
|
||||||
|
|
||||||
|
// 순위 계산 함수 - 순위 계산 유틸리티 사용
|
||||||
|
const updateLocalRanks = () => {
|
||||||
|
if (!props.members || props.members.length === 0) return;
|
||||||
|
|
||||||
|
// 순위 계산 유틸리티 함수 호출
|
||||||
|
const newRanks = calculateIndividualRankings(
|
||||||
|
props.members,
|
||||||
|
props.scoresProxy || {},
|
||||||
|
props.gameCount || 3
|
||||||
|
);
|
||||||
|
|
||||||
|
// 순위 맵 갱신
|
||||||
|
localRanks.value = newRanks;
|
||||||
|
|
||||||
|
// 현재 점수 저장
|
||||||
|
lastScores.value = {};
|
||||||
|
props.members.forEach(member => {
|
||||||
|
if (member && member.participantId) {
|
||||||
|
lastScores.value[member.participantId] = props.getMemberTotalScore ? props.getMemberTotalScore(member) : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프록스와 로컬 데이터를 통합한 계산된 순위 객체
|
||||||
|
const memberRanksRef = computed(() => {
|
||||||
|
// 무조건 props.memberRanks 사용 - 최종 순위 테이블과 동일한 순위 표시를 위해
|
||||||
|
return props.memberRanks || {};
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.members && props.members.length > 0) {
|
||||||
|
// props가 undefined인 경우 기본값 설정
|
||||||
|
if (!props.memberRanks) {
|
||||||
|
updateLocalRanks();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 초기 점수 저장
|
||||||
|
props.members.forEach(member => {
|
||||||
|
if (member && member.participantId) {
|
||||||
|
lastScores.value[member.participantId] = props.getMemberTotalScore ? props.getMemberTotalScore(member) : 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// props.memberRanks 변경 감지
|
||||||
|
watch(() => props.memberRanks, (newVal) => {
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// 점수 변경 감지
|
||||||
|
watch(() => props.scoresProxy, () => {
|
||||||
|
// 점수가 변경되었는지 확인
|
||||||
|
let scoresChanged = false;
|
||||||
|
|
||||||
|
props.members.forEach(member => {
|
||||||
|
if (member && member.participantId) {
|
||||||
|
const currentScore = props.getMemberTotalScore ? props.getMemberTotalScore(member) : 0;
|
||||||
|
const previousScore = lastScores.value[member.participantId] || 0;
|
||||||
|
|
||||||
|
if (currentScore !== previousScore) {
|
||||||
|
scoresChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 점수가 변경되었으면 순위 재계산
|
||||||
|
if (scoresChanged) {
|
||||||
|
updateLocalRanks();
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.medal-icon-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal-icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 메달 색상 */
|
||||||
|
.medal-1 {
|
||||||
|
color: gold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal-2 {
|
||||||
|
color: silver;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal-3 {
|
||||||
|
color: #cd7f32; /* 동메달 색상 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.medal-rank {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -80%);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: black;
|
||||||
|
/* 글자 테두리 효과 */
|
||||||
|
text-shadow:
|
||||||
|
-1px -1px 0 rgba(255, 255, 255, 0.7),
|
||||||
|
1px -1px 0 rgba(255, 255, 255, 0.7),
|
||||||
|
-1px 1px 0 rgba(255, 255, 255, 0.7),
|
||||||
|
1px 1px 0 rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,532 @@
|
|||||||
|
/**
|
||||||
|
* 볼링 점수 기반 순위 계산 유틸리티
|
||||||
|
* 다양한 이벤트 유형(개인전, 팀전)에 대한 순위 계산 함수 제공
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ================ 점수 계산 함수 ================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 멤버별 평균 점수 계산 (입력된 점수만 평균, 소수점 1자리)
|
||||||
|
* @param {Object} member - 참가자 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Number} - 참가자의 평균 점수
|
||||||
|
*/
|
||||||
|
export const calculateMemberAverageScore = (member, scoresProxy, gameCount) => {
|
||||||
|
if (!member || !member.participantId || !gameCount) return 0;
|
||||||
|
|
||||||
|
let total = 0, count = 0;
|
||||||
|
const handicap = Number(member.handicap) || 0;
|
||||||
|
|
||||||
|
// 각 게임별 점수 합산 및 플레이한 게임 수 카운트
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
const scoreKey = `${member.participantId}-${i}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
|
||||||
|
if (score !== undefined && score !== null && score !== '' && !isNaN(Number(score))) {
|
||||||
|
total += Number(score) + handicap;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count > 0 ? Math.round((total / count) * 10) / 10 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 팀 게임별 총점 계산 함수
|
||||||
|
* @param {Object} team - 팀 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameNumber - 게임 번호
|
||||||
|
* @returns {Number} - 해당 게임의 팀 총점
|
||||||
|
*/
|
||||||
|
export const calculateTeamGameScore = (team, scoresProxy, gameNumber) => {
|
||||||
|
if (!team || !team.members) return 0;
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
// 팀원들의 점수 합산
|
||||||
|
team.members.forEach(member => {
|
||||||
|
if (!member || !member.participantId) return;
|
||||||
|
|
||||||
|
const scoreKey = `${member.participantId}-${gameNumber}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
|
||||||
|
if (score !== undefined && score !== null && score !== '') {
|
||||||
|
const memberHc = Number(member.handicap) || 0;
|
||||||
|
total += Number(score) + memberHc;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 팀 핸디캡 추가
|
||||||
|
total += Number(team.handicap) || 0;
|
||||||
|
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 팀 총 핸디캡 계산 함수
|
||||||
|
* @param {Object} team - 팀 객체
|
||||||
|
* @returns {Number} - 팀의 총 핸디캡
|
||||||
|
*/
|
||||||
|
export const calculateTeamTotalHandicap = (team) => {
|
||||||
|
if (!team || !team.members) return 0;
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
// 팀원들의 핸디캡 합산
|
||||||
|
team.members.forEach(member => {
|
||||||
|
const memberHc = Number(member.handicap) || 0;
|
||||||
|
total += memberHc;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 팀 핸디캡 추가
|
||||||
|
total += Number(team.handicap) || 0;
|
||||||
|
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참가자 총점 계산 함수
|
||||||
|
* @param {Object} participant - 참가자 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Number} - 참가자의 총점
|
||||||
|
*/
|
||||||
|
export const calculateParticipantTotalScore = (participant, scoresProxy, gameCount) => {
|
||||||
|
if (!participant || !participant.participantId) return 0;
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
const handicap = Number(participant.handicap) || 0;
|
||||||
|
|
||||||
|
// 각 게임별 점수 합산
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
const scoreKey = `${participant.participantId}-${i}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
|
||||||
|
if (score !== undefined && score !== null && score !== '') {
|
||||||
|
total += Number(score) + handicap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참가자 평균 점수 계산 함수
|
||||||
|
* @param {Object} participant - 참가자 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Number} - 참가자의 평균 점수
|
||||||
|
*/
|
||||||
|
export const calculateParticipantAverageScore = (participant, scoresProxy, gameCount) => {
|
||||||
|
if (!participant || !participant.participantId) return 0;
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
let playedGames = 0;
|
||||||
|
const handicap = Number(participant.handicap) || 0;
|
||||||
|
|
||||||
|
// 각 게임별 점수 합산 및 플레이한 게임 수 카운트
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
const scoreKey = `${participant.participantId}-${i}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
|
||||||
|
if (score !== undefined && score !== null && score !== '') {
|
||||||
|
total += Number(score) + handicap;
|
||||||
|
playedGames++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playedGames > 0 ? Math.round(total / playedGames) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 개인전 순위 계산 함수
|
||||||
|
* @param {Array} participants - 참가자 배열
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Object} - 참가자 ID를 키로, 순위를 값으로 하는 객체
|
||||||
|
*/
|
||||||
|
export const calculateIndividualRankings = (participants, scoresProxy, gameCount) => {
|
||||||
|
if (!participants || participants.length === 0) return {};
|
||||||
|
|
||||||
|
// 참가자별 총점 계산
|
||||||
|
const participantsWithScores = participants.map(participant => ({
|
||||||
|
participantId: participant.participantId,
|
||||||
|
name: participant.name,
|
||||||
|
totalScore: calculateParticipantTotalScore(participant, scoresProxy, gameCount),
|
||||||
|
// 동점자 처리를 위한 추가 정보
|
||||||
|
highestGame: getHighestGameScore(participant, scoresProxy, gameCount),
|
||||||
|
handicap: Number(participant.handicap) || 0,
|
||||||
|
scoreDeviation: getScoreDeviation(participant, scoresProxy, gameCount),
|
||||||
|
birthday: participant.birthday ? new Date(participant.birthday) : new Date('9999-12-31')
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 정렬 기준 적용
|
||||||
|
// 1. 총점 내림차순
|
||||||
|
// 2. 동점시 핸디캡이 낮은 순
|
||||||
|
// 3. 핸디캡도 같으면 점수 편차가 적은 순
|
||||||
|
// 4. 점수 편차도 같으면 생년월일이 빠른 순
|
||||||
|
participantsWithScores.sort((a, b) => {
|
||||||
|
// 1. 총점 비교
|
||||||
|
if (b.totalScore !== a.totalScore) {
|
||||||
|
return b.totalScore - a.totalScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 핸디캡 비교 (낮은 순)
|
||||||
|
if (a.handicap !== b.handicap) {
|
||||||
|
return a.handicap - b.handicap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 점수 편차 비교 (적은 순)
|
||||||
|
if (a.scoreDeviation !== b.scoreDeviation) {
|
||||||
|
return a.scoreDeviation - b.scoreDeviation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 생년월일 비교 (빠른 순)
|
||||||
|
return a.birthday - b.birthday;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 동점자 비교 함수 - 모든 기준을 적용하여 동점자 판별
|
||||||
|
const compareParticipants = (a, b) => {
|
||||||
|
// 총점, 핸디캡, 점수 편차, 생년월일이 모두 같을 때만 동점으로 처리
|
||||||
|
return a.totalScore === b.totalScore &&
|
||||||
|
a.handicap === b.handicap &&
|
||||||
|
a.scoreDeviation === b.scoreDeviation &&
|
||||||
|
a.birthday.getTime() === b.birthday.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
// assignRanks 함수를 사용하여 순위 부여 (동점자 처리 포함)
|
||||||
|
return assignRanks(participantsWithScores, 'participantId', compareParticipants);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 팀전 순위 계산 함수
|
||||||
|
* @param {Array} teams - 팀 배열
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Object} - 팀 ID를 키로, 순위를 값으로 하는 객체
|
||||||
|
*/
|
||||||
|
export const calculateTeamRankings = (teams, scoresProxy, gameCount) => {
|
||||||
|
if (!teams || teams.length === 0) return {};
|
||||||
|
|
||||||
|
// 팀별 총점 계산
|
||||||
|
const teamsWithScores = teams.map(team => {
|
||||||
|
let teamTotalScore = 0;
|
||||||
|
|
||||||
|
// 팀원들의 점수 합산
|
||||||
|
if (team.members && team.members.length > 0) {
|
||||||
|
team.members.forEach(member => {
|
||||||
|
teamTotalScore += calculateParticipantTotalScore(member, scoresProxy, gameCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
teamId: team.id || team.teamNumber,
|
||||||
|
teamNumber: team.teamNumber,
|
||||||
|
totalScore: teamTotalScore,
|
||||||
|
// 추후 동점자 처리를 위한 추가 정보
|
||||||
|
memberCount: team.members ? team.members.length : 0,
|
||||||
|
teamHandicap: Number(team.handicap) || 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 총점 기준 내림차순 정렬
|
||||||
|
teamsWithScores.sort((a, b) => b.totalScore - a.totalScore);
|
||||||
|
|
||||||
|
// 순위 맵 생성
|
||||||
|
const rankMap = {};
|
||||||
|
teamsWithScores.forEach((team, index) => {
|
||||||
|
if (team.teamId) {
|
||||||
|
rankMap[team.teamId] = index + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rankMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 게임별 순위 계산 함수
|
||||||
|
* @param {Array} participants - 참가자 배열
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameNumber - 게임 번호
|
||||||
|
* @returns {Object} - 참가자 ID를 키로, 해당 게임의 순위를 값으로 하는 객체
|
||||||
|
*/
|
||||||
|
export const calculateGameRankings = (participants, scoresProxy, gameNumber) => {
|
||||||
|
if (!participants || participants.length === 0 || !gameNumber) return {};
|
||||||
|
|
||||||
|
// 참가자별 해당 게임 점수 계산
|
||||||
|
const participantsWithGameScore = participants.map(participant => {
|
||||||
|
const scoreKey = `${participant.participantId}-${gameNumber}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
const handicap = Number(participant.handicap) || 0;
|
||||||
|
|
||||||
|
let gameScore = 0;
|
||||||
|
if (score !== undefined && score !== null && score !== '') {
|
||||||
|
gameScore = Number(score) + handicap;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
participantId: participant.participantId,
|
||||||
|
name: participant.name,
|
||||||
|
gameScore: gameScore,
|
||||||
|
handicap: handicap
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 게임 점수 기준 내림차순 정렬
|
||||||
|
participantsWithGameScore.sort((a, b) => b.gameScore - a.gameScore);
|
||||||
|
|
||||||
|
// 동점자 비교 함수 - 게임 점수가 같은 경우 동점으로 처리
|
||||||
|
const compareParticipants = (a, b) => {
|
||||||
|
return a.gameScore === b.gameScore;
|
||||||
|
};
|
||||||
|
|
||||||
|
// assignRanks 함수를 사용하여 순위 부여 (동점자 처리 포함)
|
||||||
|
return assignRanks(participantsWithGameScore, 'participantId', compareParticipants);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참가자의 최고 게임 점수 구하기
|
||||||
|
* @param {Object} participant - 참가자 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Number} - 최고 게임 점수
|
||||||
|
*/
|
||||||
|
export const getHighestGameScore = (participant, scoresProxy, gameCount) => {
|
||||||
|
if (!participant || !participant.participantId) return 0;
|
||||||
|
|
||||||
|
let highestScore = 0;
|
||||||
|
const handicap = Number(participant.handicap) || 0;
|
||||||
|
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
const scoreKey = `${participant.participantId}-${i}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
|
||||||
|
if (score !== undefined && score !== null && score !== '') {
|
||||||
|
const gameScore = Number(score) + handicap;
|
||||||
|
if (gameScore > highestScore) {
|
||||||
|
highestScore = gameScore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return highestScore;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참가자의 최저 게임 점수 구하기
|
||||||
|
* @param {Object} participant - 참가자 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Number} - 최저 게임 점수
|
||||||
|
*/
|
||||||
|
export const getLowestGameScore = (participant, scoresProxy, gameCount) => {
|
||||||
|
if (!participant || !participant.participantId) return 0;
|
||||||
|
|
||||||
|
let lowestScore = Number.MAX_SAFE_INTEGER;
|
||||||
|
const handicap = Number(participant.handicap) || 0;
|
||||||
|
let hasValidScore = false;
|
||||||
|
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
const scoreKey = `${participant.participantId}-${i}`;
|
||||||
|
const score = scoresProxy[scoreKey];
|
||||||
|
|
||||||
|
if (score !== undefined && score !== null && score !== '') {
|
||||||
|
const gameScore = Number(score) + handicap;
|
||||||
|
if (gameScore < lowestScore) {
|
||||||
|
lowestScore = gameScore;
|
||||||
|
hasValidScore = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasValidScore ? lowestScore : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참가자의 게임 점수 편차 계산
|
||||||
|
* @param {Object} participant - 참가자 객체
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Number} - 점수 편차 (최고 점수 - 최저 점수)
|
||||||
|
*/
|
||||||
|
export const getScoreDeviation = (participant, scoresProxy, gameCount) => {
|
||||||
|
const highestScore = getHighestGameScore(participant, scoresProxy, gameCount);
|
||||||
|
const lowestScore = getLowestGameScore(participant, scoresProxy, gameCount);
|
||||||
|
|
||||||
|
return highestScore - lowestScore;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 순위 부여 함수 - 동점자 처리 포함
|
||||||
|
* @param {Array} items - 정렬된 아이템 배열
|
||||||
|
* @param {String} idKey - 아이템의 고유 ID를 나타내는 키 이름
|
||||||
|
* @param {Function} compareFunc - 두 아이템이 동점인지 비교하는 함수 (a, b) => boolean
|
||||||
|
* @returns {Object} - 아이템 ID를 키로, 순위를 값으로 하는 객체
|
||||||
|
*/
|
||||||
|
export const assignRanks = (items, idKey, compareFunc) => {
|
||||||
|
if (!items || items.length === 0) return {};
|
||||||
|
|
||||||
|
const rankMap = {};
|
||||||
|
|
||||||
|
// 예외 처리: 아이템이 하나만 있는 경우
|
||||||
|
if (items.length === 1 && items[0][idKey]) {
|
||||||
|
rankMap[items[0][idKey]] = 1;
|
||||||
|
return rankMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 동점자 그룹 처리
|
||||||
|
const groups = [];
|
||||||
|
let currentGroup = [];
|
||||||
|
|
||||||
|
// 첫 번째 아이템 처리
|
||||||
|
if (items[0][idKey]) {
|
||||||
|
currentGroup.push(items[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 동점자 그룹화
|
||||||
|
for (let i = 1; i < items.length; i++) {
|
||||||
|
const current = items[i];
|
||||||
|
const prev = items[i-1];
|
||||||
|
|
||||||
|
// 아이템 ID가 없는 경우 건너뛰기
|
||||||
|
if (!current[idKey]) continue;
|
||||||
|
|
||||||
|
// 동점 여부 확인
|
||||||
|
if (compareFunc(current, prev)) {
|
||||||
|
// 동점자는 현재 그룹에 추가
|
||||||
|
currentGroup.push(current);
|
||||||
|
} else {
|
||||||
|
// 동점이 아니면 현재 그룹 저장하고 새 그룹 시작
|
||||||
|
if (currentGroup.length > 0) {
|
||||||
|
groups.push(currentGroup);
|
||||||
|
}
|
||||||
|
currentGroup = [current];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막 그룹 추가
|
||||||
|
if (currentGroup.length > 0) {
|
||||||
|
groups.push(currentGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그룹별 순위 부여
|
||||||
|
let nextRank = 1;
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
// 현재 그룹의 모든 아이템에 동일한 순위 부여
|
||||||
|
for (const item of group) {
|
||||||
|
if (item[idKey]) {
|
||||||
|
rankMap[item[idKey]] = nextRank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다음 순위는 현재 그룹 크기를 더한 값
|
||||||
|
nextRank += group.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rankMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 순위 색상 클래스 결정 함수
|
||||||
|
* @param {Number} rank - 순위
|
||||||
|
* @returns {String} - 순위에 따른 CSS 클래스 이름
|
||||||
|
*/
|
||||||
|
export const getRankColorClass = (rank) => {
|
||||||
|
if (!rank || rank <= 0) return 'rank-other';
|
||||||
|
|
||||||
|
if (rank <= 5) {
|
||||||
|
return `rank-${rank}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'rank-other';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 팀 전체 순위 계산 함수
|
||||||
|
* @param {Array} teams - 팀 배열
|
||||||
|
* @param {Object} scoresProxy - 점수 프록시 객체
|
||||||
|
* @param {Number} gameCount - 게임 수
|
||||||
|
* @returns {Object} - 팀 ID를 키로, 순위를 값으로 하는 객체
|
||||||
|
*/
|
||||||
|
export const calculateTeamOverallRankings = (teams, scoresProxy, gameCount) => {
|
||||||
|
if (!teams || teams.length === 0) return {};
|
||||||
|
|
||||||
|
// 팀별 총점 계산
|
||||||
|
const teamTotals = teams.map(team => {
|
||||||
|
// 각 게임별 점수 합산
|
||||||
|
let totalScore = 0;
|
||||||
|
|
||||||
|
// 팀의 게임별 점수 및 편차 계산을 위한 배열
|
||||||
|
const gameScores = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
const gameScore = calculateTeamGameScore(team, scoresProxy, i);
|
||||||
|
totalScore += gameScore;
|
||||||
|
gameScores.push(gameScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 팀 점수 편차 계산 (최고 점수 - 최저 점수)
|
||||||
|
let scoreDeviation = 0;
|
||||||
|
if (gameScores.length > 0) {
|
||||||
|
const maxScore = Math.max(...gameScores.filter(score => score > 0));
|
||||||
|
const minScore = Math.min(...gameScores.filter(score => score > 0));
|
||||||
|
scoreDeviation = maxScore - minScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 팀원들의 평균 생년월일 계산
|
||||||
|
let averageBirthday = new Date('9999-12-31');
|
||||||
|
if (team.members && team.members.length > 0) {
|
||||||
|
const validBirthdays = team.members
|
||||||
|
.filter(member => member.birthday)
|
||||||
|
.map(member => new Date(member.birthday));
|
||||||
|
|
||||||
|
if (validBirthdays.length > 0) {
|
||||||
|
// 평균 샜산을 위해 총 밀리초 계산
|
||||||
|
const totalMs = validBirthdays.reduce((sum, date) => sum + date.getTime(), 0);
|
||||||
|
averageBirthday = new Date(totalMs / validBirthdays.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
teamId: team.id || team.teamNumber,
|
||||||
|
totalScore: totalScore,
|
||||||
|
handicapSum: calculateTeamTotalHandicap(team),
|
||||||
|
memberCount: (team.members ? team.members.length : 0),
|
||||||
|
scoreDeviation: scoreDeviation,
|
||||||
|
averageBirthday: averageBirthday
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 정렬 기준 적용
|
||||||
|
// 1. 총점 내림차순
|
||||||
|
// 2. 동점시 핸디캡이 낮은 순
|
||||||
|
// 3. 핸디캡도 같으면 점수 편차가 적은 순
|
||||||
|
// 4. 점수 편차도 같으면 생년월일 평균이 빠른 순
|
||||||
|
teamTotals.sort((a, b) => {
|
||||||
|
// 1. 총점 비교
|
||||||
|
if (b.totalScore !== a.totalScore) return b.totalScore - a.totalScore;
|
||||||
|
|
||||||
|
// 2. 동점시 핸디캡이 적은 팀이 순위가 높음
|
||||||
|
if (a.handicapSum !== b.handicapSum) return a.handicapSum - b.handicapSum;
|
||||||
|
|
||||||
|
// 3. 핸디캡도 같으면 점수 편차가 적은 팀이 순위가 높음
|
||||||
|
if (a.scoreDeviation !== b.scoreDeviation) return a.scoreDeviation - b.scoreDeviation;
|
||||||
|
|
||||||
|
// 4. 점수 편차도 같으면 팀원들의 평균 생년월일이 빠른 팀이 순위가 높음
|
||||||
|
return a.averageBirthday - b.averageBirthday;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 동점자 비교 함수 - 모든 기준을 적용하여 동점자 판별
|
||||||
|
const compareTeams = (a, b) => {
|
||||||
|
// 총점, 핸디캡 합계, 점수 편차, 평균 생년월일이 모두 같을 때만 동점으로 처리
|
||||||
|
return a.totalScore === b.totalScore &&
|
||||||
|
a.handicapSum === b.handicapSum &&
|
||||||
|
a.scoreDeviation === b.scoreDeviation &&
|
||||||
|
a.averageBirthday.getTime() === b.averageBirthday.getTime();
|
||||||
|
};
|
||||||
|
|
||||||
|
// assignRanks 함수를 사용하여 순위 부여 (동점자 처리 포함)
|
||||||
|
return assignRanks(teamTotals, 'teamId', compareTeams);
|
||||||
|
};
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
<Button type="submit" label="클럽 생성하기" class="w-full" :loading="loading" />
|
<Button type="submit" label="클럽 생성하기" class="w-full" :loading="loading" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<Toast />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
@remove-participant="removeParticipant"
|
@remove-participant="removeParticipant"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Toast />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,6 @@
|
|||||||
<Button label="예" icon="pi pi-check" class="p-button-danger" @click="deleteEvent" />
|
<Button label="예" icon="pi pi-check" class="p-button-danger" @click="deleteEvent" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Toast />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,17 @@
|
|||||||
sortField="name"
|
sortField="name"
|
||||||
:sortOrder="1"
|
:sortOrder="1"
|
||||||
stripedRows responsiveLayout="scroll">
|
stripedRows responsiveLayout="scroll">
|
||||||
<Column field="name" header="이름" sortable style="width: 15%"></Column>
|
<Column field="name" header="이름" sortable style="width: 15%">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="member-name">
|
||||||
|
<div class="name">{{ slotProps.data.name }}</div>
|
||||||
|
<div class="contact-info">
|
||||||
|
<div v-if="slotProps.data.phone" class="phone">{{ slotProps.data.phone }}</div>
|
||||||
|
<div v-if="slotProps.data.email" class="email">{{ slotProps.data.email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
<Column field="gender" header="성별" sortable style="width: 10%"></Column>
|
<Column field="gender" header="성별" sortable style="width: 10%"></Column>
|
||||||
<Column field="memberType" header="회원 유형" sortable style="width: 15%">
|
<Column field="memberType" header="회원 유형" sortable style="width: 15%">
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
@@ -177,13 +187,12 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
|
||||||
import apiClient from '@/services/api';
|
|
||||||
import clubService from '@/services/clubService';
|
import clubService from '@/services/clubService';
|
||||||
import { useClubStore } from '@/stores/clubStore';
|
import { useClubStore } from '@/stores/clubStore';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
|
||||||
const toast = useToast();
|
|
||||||
const clubStore = useClubStore();
|
const clubStore = useClubStore();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
// 회원 유형 목록
|
// 회원 유형 목록
|
||||||
const memberTypes = ref([
|
const memberTypes = ref([
|
||||||
@@ -412,23 +421,27 @@ const hideDialog = () => {
|
|||||||
// 회원 저장
|
// 회원 저장
|
||||||
const saveMember = async () => {
|
const saveMember = async () => {
|
||||||
try {
|
try {
|
||||||
const memberData = {
|
const memberData = { ...member.value };
|
||||||
...member.value
|
|
||||||
};
|
|
||||||
|
|
||||||
if (dialogMode.value === 'new') {
|
const isNew = dialogMode.value === 'new';
|
||||||
await clubService.addClubMember(clubId.value, memberData);
|
const response = isNew
|
||||||
toast.add({ severity: 'success', summary: '성공', detail: '새 회원이 추가되었습니다.', life: 3000 });
|
? await clubService.addClubMember(clubId.value, memberData)
|
||||||
} else {
|
: await clubService.updateClubMember(clubId.value, memberData.id, memberData);
|
||||||
await clubService.updateClubMember(clubId.value, memberData.id, memberData);
|
|
||||||
toast.add({ severity: 'success', summary: '성공', detail: '회원 정보가 수정되었습니다.', life: 3000 });
|
toast.add({
|
||||||
}
|
severity: 'success',
|
||||||
|
summary: '성공',
|
||||||
|
detail: isNew ? '새 회원이 추가되었습니다.' : '회원 정보가 수정되었습니다.',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
|
||||||
hideDialog();
|
hideDialog();
|
||||||
await loadMembers();
|
await loadMembers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('회원 저장 중 오류:', error);
|
console.error('회원 저장 중 오류:', error);
|
||||||
toast.add({ severity: 'error', summary: '오류', detail: '회원 정보 저장 중 오류가 발생했습니다.', life: 3000 });
|
// 서버에서 오류 메시지가 있는 경우 해당 메시지 표시
|
||||||
|
const errorMessage = error.response?.data?.message || '회원 정보 저장 중 오류가 발생했습니다.';
|
||||||
|
toast.add({ severity: 'error', summary: '오류', detail: errorMessage, life: 3000 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -498,6 +511,31 @@ const statusOptions = ref([
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 회원 이름, 연락처 스타일 */
|
||||||
|
.member-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name .name {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name .contact-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name .phone,
|
||||||
|
.member-name .email {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* 필터 다이얼로그 스타일 */
|
/* 필터 다이얼로그 스타일 */
|
||||||
:deep(.p-dialog-content) {
|
:deep(.p-dialog-content) {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|||||||
@@ -283,19 +283,16 @@
|
|||||||
|
|
||||||
/* 순위별 색상 */
|
/* 순위별 색상 */
|
||||||
.rank-badge.rank-1 {
|
.rank-badge.rank-1 {
|
||||||
color: var(--p-yellow-500);
|
color: gold;
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
.rank-badge.rank-2 {
|
.rank-badge.rank-2 {
|
||||||
color: var(--p-gray-400);
|
color: silver;
|
||||||
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
.rank-badge.rank-3 {
|
.rank-badge.rank-3 {
|
||||||
color: var(--p-orange-500);
|
color: #cd7f32;
|
||||||
}
|
font-weight: 900;
|
||||||
.rank-badge.rank-4 {
|
|
||||||
color: var(--p-blue-400);
|
|
||||||
}
|
|
||||||
.rank-badge.rank-5 {
|
|
||||||
color: var(--p-green-400);
|
|
||||||
}
|
}
|
||||||
/* 필요시 더 추가 가능 */
|
/* 필요시 더 추가 가능 */
|
||||||
|
|
||||||
@@ -324,9 +321,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rank-badge {
|
.rank-badge {
|
||||||
font-weight: 900;
|
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--p-yellow-500);
|
color: var(--p-black-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-table th,
|
.score-table th,
|
||||||
@@ -456,6 +452,14 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.score-status-icon-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.score-input-hc-wrap > .score-input,
|
.score-input-hc-wrap > .score-input,
|
||||||
@@ -499,6 +503,9 @@
|
|||||||
.member-score-summary {
|
.member-score-summary {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<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>
|
||||||
<div class="event-public-card">
|
<div class="event-public-card">
|
||||||
<div v-if="loading" class="text-center">로딩 중...</div>
|
<div v-if="loading" class="text-center">로딩 중...</div>
|
||||||
<div v-else-if="error" class="text-center">
|
<div v-else-if="error" class="text-center text-4xl">
|
||||||
<i class="pi pi-exclamation-triangle text-red block"></i>
|
<i class="pi pi-exclamation-triangle text-red block text-8xl"></i>
|
||||||
<span class="text-red">{{ error }}</span>
|
<span class="text-red">{{ error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!isAuthenticated">
|
<div v-else-if="!isAuthenticated">
|
||||||
@@ -63,11 +63,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-text-secondary mb-4">{{ event.description || '' }}</div>
|
<div class="p-text-secondary mb-4">{{ event.description || '' }}</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
<!-- 팀별 게임별 점수/등수 표 -->
|
|
||||||
</div>
|
</div>
|
||||||
<template v-if="teams.length > 0">
|
<template v-if="teams.length > 0">
|
||||||
<Card class="team-score-table-wrapper mb-4">
|
<Card class="team-score-table-wrapper mb-4">
|
||||||
<template #content>
|
<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">
|
||||||
@@ -99,14 +99,67 @@
|
|||||||
</DataTable>
|
</DataTable>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
<div v-for="team in getAllTeamsWithUnassigned()" :key="team.id" class="mb-4">
|
<template v-if="event.status != '완료'">
|
||||||
|
<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"
|
||||||
|
:memberRanks="memberRankMap"
|
||||||
|
/>
|
||||||
|
<!-- 팀 카드 요약(총점/등수/게임별 점수)은 '팀'만 노출 -->
|
||||||
|
<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>
|
||||||
|
<template v-else>
|
||||||
|
<template v-if="event.status != '완료'">
|
||||||
<Card>
|
<Card>
|
||||||
<template #content>
|
<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"
|
||||||
@@ -117,56 +170,66 @@
|
|||||||
:getMemberTotalScore="getMemberTotalScore"
|
:getMemberTotalScore="getMemberTotalScore"
|
||||||
:getMemberAverageScore="getMemberAverageScore"
|
:getMemberAverageScore="getMemberAverageScore"
|
||||||
:scoreColorClass="scoreColorClass"
|
:scoreColorClass="scoreColorClass"
|
||||||
|
:memberRanks="memberRankMap"
|
||||||
/>
|
/>
|
||||||
<!-- 팀 카드 요약(총점/등수/게임별 점수)은 '팀'만 노출 -->
|
|
||||||
<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>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-if="event.status == '완료'">
|
||||||
<Card>
|
<Card>
|
||||||
|
<template #title>
|
||||||
|
<div class="card-title-with-button">
|
||||||
|
<h2>최종 순위</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<MemberListCard
|
<DataTable :value="getFinalRankingData()" stripedRows :paginator="true" :rows="30"
|
||||||
title="참가자"
|
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||||
:members="sortedParticipants"
|
:rowsPerPageOptions="[5,10,20,30,50]" responsiveLayout="scroll">
|
||||||
:gameCount="event.gameCount"
|
|
||||||
:canInputScore="canInputScore"
|
<!-- 순위 열 -->
|
||||||
:scoresProxy="scoresProxy"
|
<Column field="rank" header="순위" :sortable="true" style="width: 3%;">
|
||||||
:scoreStates="scoreStates"
|
<template #body="slotProps">
|
||||||
:onScoreInput="onScoreInput"
|
<span :class="'rank-badge rank-' + slotProps.data.rank">
|
||||||
:onScoreBlur="onScoreBlur"
|
{{ slotProps.data.rank }}
|
||||||
:onScoreEnter="onScoreEnter"
|
</span>
|
||||||
:getMemberTotalScore="getMemberTotalScore"
|
</template>
|
||||||
:getMemberAverageScore="getMemberAverageScore"
|
</Column>
|
||||||
:scoreColorClass="scoreColorClass"
|
|
||||||
/>
|
<!-- 이름 열 -->
|
||||||
|
<Column field="name" header="이름" :sortable="true" style="min-width: 7rem;"></Column>
|
||||||
|
|
||||||
|
<!-- 핸디캡 열 -->
|
||||||
|
<Column field="handicap" header="핸디캡" :sortable="true"></Column>
|
||||||
|
|
||||||
|
<!-- 게임별 점수 열 -->
|
||||||
|
<template v-for="n in event.gameCount" :key="n">
|
||||||
|
<Column :field="'game' + n" :header="n + 'G'" :sortable="true">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span :class="{ 'score-red': slotProps.data['game' + n] >= 200 }">
|
||||||
|
{{ slotProps.data['game' + n] }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 총점 열 -->
|
||||||
|
<Column field="totalScore" header="총점" :sortable="true">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<b>{{ slotProps.data.totalScore }}</b>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<!-- 에버리지 열 -->
|
||||||
|
<Column field="average" header="에버리지" :sortable="true">
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span :class="{ 'score-red': slotProps.data.average >= 200 }">
|
||||||
|
{{ slotProps.data.average }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
@@ -176,12 +239,21 @@
|
|||||||
<JoinDialog v-if="showJoinDialog && canJoin" v-model:visible="showJoinDialog" :publicHash="publicHash"
|
<JoinDialog v-if="showJoinDialog && canJoin" v-model:visible="showJoinDialog" :publicHash="publicHash"
|
||||||
:participants="participants"
|
:participants="participants"
|
||||||
:availableMembers="availableMembers" :eventStatus="event.status"
|
:availableMembers="availableMembers" :eventStatus="event.status"
|
||||||
@close="showJoinDialog = false" @updated="reload" />
|
@close="showJoinDialog = false" @updated="reload" @guest-added="handleGuestAdded" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, reactive, onMounted } from 'vue';
|
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
calculateIndividualRankings,
|
||||||
|
calculateGameRankings,
|
||||||
|
calculateParticipantTotalScore,
|
||||||
|
calculateMemberAverageScore,
|
||||||
|
calculateTeamGameScore,
|
||||||
|
calculateTeamTotalHandicap,
|
||||||
|
calculateTeamOverallRankings
|
||||||
|
} from '@/utils/rankingUtils';
|
||||||
import { debounce } from 'lodash-es'; // lodash 설치 필요 (이미 있다면 무시)
|
import { debounce } from 'lodash-es'; // lodash 설치 필요 (이미 있다면 무시)
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import MemberListCard from '@/components/public/MemberListCard.vue';
|
import MemberListCard from '@/components/public/MemberListCard.vue';
|
||||||
@@ -236,6 +308,12 @@ const joinButtonLabel = computed(() => {
|
|||||||
// 점수 상태 관리: idle, saving, saved, error
|
// 점수 상태 관리: idle, saving, saved, error
|
||||||
const scoreStates = reactive({});
|
const scoreStates = reactive({});
|
||||||
|
|
||||||
|
// 총점 기준 순위 맵 (reactive 값)
|
||||||
|
const memberRankMap = reactive({});
|
||||||
|
|
||||||
|
// 게임별 순위 맵
|
||||||
|
const gameRanks = reactive({});
|
||||||
|
|
||||||
// 점수 색상 클래스 계산
|
// 점수 색상 클래스 계산
|
||||||
// 점수 색상 클래스 계산 (핸디캡 포함)
|
// 점수 색상 클래스 계산 (핸디캡 포함)
|
||||||
function scoreColorClass(state, value, handicap) {
|
function scoreColorClass(state, value, handicap) {
|
||||||
@@ -271,12 +349,81 @@ const saveScore = debounce(async (member, n, oldValue, newValue, teamNumber) =>
|
|||||||
member.scores[n-1] = Number(newValue);
|
member.scores[n-1] = Number(newValue);
|
||||||
}
|
}
|
||||||
setTimeout(() => { scoreStates[key] = 'idle'; }, 1200); // 일정 시간 후 아이콘 사라지게
|
setTimeout(() => { scoreStates[key] = 'idle'; }, 1200); // 일정 시간 후 아이콘 사라지게
|
||||||
|
|
||||||
|
// 점수 저장 후 순위 업데이트
|
||||||
|
updateMemberRanks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
scoreStates[key] = 'error';
|
scoreStates[key] = 'error';
|
||||||
setTimeout(() => { scoreStates[key] = 'idle'; }, 2000);
|
setTimeout(() => { scoreStates[key] = 'idle'; }, 2000);
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
// 최종 순위 데이터 생성 함수
|
||||||
|
const getFinalRankingData = () => {
|
||||||
|
if (!participants.value || participants.value.length === 0) return [];
|
||||||
|
|
||||||
|
// 순위 계산
|
||||||
|
const activeParticipants = participants.value.filter(p => p && p.status !== '취소');
|
||||||
|
const ranks = calculateIndividualRankings(activeParticipants, scoresProxy.value, event.value.gameCount);
|
||||||
|
|
||||||
|
// 순위 맵을 memberRankMap에도 적용하여 팀 카드에서도 동일한 순위 사용
|
||||||
|
Object.keys(ranks).forEach(participantId => {
|
||||||
|
memberRankMap[participantId] = ranks[participantId];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 참가자별 데이터 생성
|
||||||
|
const rankingData = activeParticipants.map(participant => {
|
||||||
|
const participantId = participant.participantId;
|
||||||
|
const handicap = Number(participant.handicap) || 0;
|
||||||
|
|
||||||
|
// 게임별 점수 계산
|
||||||
|
const gameScores = {};
|
||||||
|
let totalScore = 0;
|
||||||
|
let playedGames = 0;
|
||||||
|
|
||||||
|
for (let i = 1; i <= event.value.gameCount; i++) {
|
||||||
|
const scoreKey = `${participantId}-${i}`;
|
||||||
|
const rawScore = scoresProxy.value[scoreKey];
|
||||||
|
|
||||||
|
if (rawScore !== undefined && rawScore !== null && rawScore !== '') {
|
||||||
|
const score = Number(rawScore);
|
||||||
|
// 핸디캡 포함 점수가 300점을 넘지 않도록 제한
|
||||||
|
const scoreWithHc = Math.min(score + handicap, 300);
|
||||||
|
|
||||||
|
gameScores[`rawGame${i}`] = score;
|
||||||
|
gameScores[`game${i}`] = scoreWithHc;
|
||||||
|
totalScore += scoreWithHc;
|
||||||
|
playedGames++;
|
||||||
|
} else {
|
||||||
|
gameScores[`rawGame${i}`] = 0;
|
||||||
|
gameScores[`game${i}`] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 평균 점수 계산 (소수점 1자리까지 표시)
|
||||||
|
const average = playedGames > 0 ? (totalScore / playedGames).toFixed(1) : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
participantId,
|
||||||
|
rank: ranks[participantId] || '-',
|
||||||
|
name: participant.name || '이름없음',
|
||||||
|
handicap,
|
||||||
|
...gameScores,
|
||||||
|
totalScore,
|
||||||
|
average
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 순위로 정렬
|
||||||
|
rankingData.sort((a, b) => {
|
||||||
|
if (a.rank === '-') return 1;
|
||||||
|
if (b.rank === '-') return -1;
|
||||||
|
return a.rank - b.rank;
|
||||||
|
});
|
||||||
|
|
||||||
|
return rankingData;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
function onScoreBlur(member, n, teamNumber) {
|
function onScoreBlur(member, n, teamNumber) {
|
||||||
const key = member.participantId + '-' + n;
|
const key = member.participantId + '-' + n;
|
||||||
@@ -306,31 +453,123 @@ function onScoreEnter(member, n, teamNumber) {
|
|||||||
saveScore(member, n, member.scores?.[n-1] ?? '', val, teamNumber);
|
saveScore(member, n, member.scores?.[n-1] ?? '', val, teamNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 멤버별 평균 점수 계산 (입력된 점수만 평균, 소수점 1자리)
|
// 멤버별 평균 점수 계산 (입력된 점수만 평균, 소수점 1자리) - rankingUtils 사용
|
||||||
function getMemberAverageScore(member) {
|
function getMemberAverageScore(member) {
|
||||||
if (!member || !event.value.gameCount) return 0;
|
if (!member || !event.value.gameCount) return 0;
|
||||||
let total = 0, count = 0;
|
return calculateMemberAverageScore(member, scoresProxy.value, event.value.gameCount);
|
||||||
for (let i = 1; i <= event.value.gameCount; i++) {
|
|
||||||
const score = Number(scoresProxy.value[member.participantId + '-' + i]);
|
|
||||||
const hc = Number(member.handicap) || 0;
|
|
||||||
if (!isNaN(score) && score > 0) {
|
|
||||||
total += score + hc;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count > 0 ? Math.round((total / count) * 10) / 10 : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 멤버별 핸디캡 포함 총점 계산
|
// 멤버별 핸디캡 포함 총점 계산 - rankingUtils 사용
|
||||||
function getMemberTotalScore(member) {
|
function getMemberTotalScore(member) {
|
||||||
if (!member || !event.value.gameCount) return 0;
|
if (!member || !event.value.gameCount) return 0;
|
||||||
let total = 0;
|
return calculateParticipantTotalScore(member, scoresProxy.value, event.value.gameCount);
|
||||||
for (let i = 1; i <= event.value.gameCount; i++) {
|
}
|
||||||
const score = Number(scoresProxy.value[member.participantId + '-' + i]) || 0;
|
|
||||||
const hc = Number(member.handicap) || 0;
|
// 총점 기준 순위 계산 (일반 함수로 변경)
|
||||||
total += score + hc;
|
function updateMemberRanks() {
|
||||||
|
// 순위 계산에 사용할 참가자 목록 가져오기
|
||||||
|
const activeParticipants = [];
|
||||||
|
|
||||||
|
// 참가자 배열 직접 사용
|
||||||
|
if (participants.value && participants.value.length > 0) {
|
||||||
|
// 취소되지 않은 참가자만 필터링
|
||||||
|
activeParticipants.push(...participants.value.filter(p => p && p.status !== '취소'));
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// 백업 방법: 팀과 미배정 멤버 사용
|
||||||
|
if (teams.value && teams.value.length > 0) {
|
||||||
|
teams.value.forEach(team => {
|
||||||
|
if (team.members && team.members.length > 0) {
|
||||||
|
activeParticipants.push(...team.members.filter(m => m && m.status !== '취소'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unassignedMembers.value && unassignedMembers.value.length > 0) {
|
||||||
|
activeParticipants.push(...unassignedMembers.value.filter(m => m && m.status !== '취소'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return total;
|
|
||||||
|
// 순위 계산 유틸리티 함수 호출
|
||||||
|
const ranks = calculateIndividualRankings(
|
||||||
|
activeParticipants,
|
||||||
|
scoresProxy.value, // .value 추가
|
||||||
|
event.value?.gameCount || 3
|
||||||
|
);
|
||||||
|
|
||||||
|
// 순위 맵 초기화
|
||||||
|
Object.keys(memberRankMap).forEach(key => {
|
||||||
|
delete memberRankMap[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 순위 맵에 계산된 순위 적용
|
||||||
|
Object.keys(ranks).forEach(participantId => {
|
||||||
|
memberRankMap[participantId] = ranks[participantId];
|
||||||
|
|
||||||
|
// 참가자 이름 찾기
|
||||||
|
const participant = activeParticipants.find(p => p.participantId == participantId);
|
||||||
|
const name = participant ? (participant.name || '이름없음') : '알 수 없음';
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 게임별 순위도 계산
|
||||||
|
updateGameRankings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 게임별 순위 계산 함수
|
||||||
|
function updateGameRankings() {
|
||||||
|
// 게임 수 가져오기
|
||||||
|
const gameCount = event.value?.gameCount || 3;
|
||||||
|
|
||||||
|
// 팀 데이터 가져오기 (미배정 포함)
|
||||||
|
const allTeams = getAllTeamsWithUnassigned();
|
||||||
|
|
||||||
|
// 각 게임별로 순위 계산
|
||||||
|
for (let i = 1; i <= gameCount; i++) {
|
||||||
|
// 게임별 팀 점수 계산
|
||||||
|
const teamsWithGameScores = allTeams.map(team => {
|
||||||
|
const gameScore = getTeamTotalScore(team, i);
|
||||||
|
return {
|
||||||
|
teamId: team.id || team.teamNumber,
|
||||||
|
teamNumber: team.teamNumber,
|
||||||
|
gameScore: gameScore,
|
||||||
|
isUnassigned: team.isUnassigned || false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 게임 점수 기준 내림차순 정렬
|
||||||
|
teamsWithGameScores.sort((a, b) => b.gameScore - a.gameScore);
|
||||||
|
|
||||||
|
// 순위 맵 초기화
|
||||||
|
gameRanks[i] = {};
|
||||||
|
|
||||||
|
// 순위 부여
|
||||||
|
let rank = 1;
|
||||||
|
let prevScore = -1;
|
||||||
|
let sameRankCount = 0;
|
||||||
|
|
||||||
|
teamsWithGameScores.forEach((team, index) => {
|
||||||
|
// 동점자 처리
|
||||||
|
if (prevScore === team.gameScore) {
|
||||||
|
sameRankCount++;
|
||||||
|
} else {
|
||||||
|
rank = index + 1 - sameRankCount;
|
||||||
|
prevScore = team.gameScore;
|
||||||
|
sameRankCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 순위 저장
|
||||||
|
gameRanks[i][team.teamId] = rank;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 게임별 순위 가져오기
|
||||||
|
function getRanksByGame(gameNumber) {
|
||||||
|
if (!gameRanks[gameNumber]) {
|
||||||
|
updateGameRankings();
|
||||||
|
}
|
||||||
|
return gameRanks[gameNumber] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 각 참가자별로 점수 배열(scores) 생성 (점수 미정시 null)
|
// 각 참가자별로 점수 배열(scores) 생성 (점수 미정시 null)
|
||||||
@@ -423,6 +662,30 @@ async function fetchEventData(password) {
|
|||||||
// 점수 상태는 항상 'idle'로 초기화 (저장됨 아이콘이 안나오게)
|
// 점수 상태는 항상 'idle'로 초기화 (저장됨 아이콘이 안나오게)
|
||||||
scoreStates[key] = 'idle';
|
scoreStates[key] = 'idle';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 순위 업데이트
|
||||||
|
updateMemberRanks();
|
||||||
|
|
||||||
|
// 순위 맵이 비어있으면 순위 계산 유틸리티 함수 사용
|
||||||
|
if (Object.keys(memberRankMap).length === 0) {
|
||||||
|
// 참가자 데이터가 있는 경우
|
||||||
|
if (participants.value && participants.value.length > 0) {
|
||||||
|
// 취소되지 않은 참가자만 필터링
|
||||||
|
const activeParticipants = participants.value.filter(p => p && p.status !== '취소');
|
||||||
|
|
||||||
|
// 순위 계산 유틸리티 함수 호출
|
||||||
|
const initialRanks = calculateIndividualRankings(
|
||||||
|
activeParticipants,
|
||||||
|
scoresProxy.value, // .value 추가
|
||||||
|
event.value?.gameCount || 3
|
||||||
|
);
|
||||||
|
|
||||||
|
// 계산된 순위 적용
|
||||||
|
Object.keys(initialRanks).forEach(participantId => {
|
||||||
|
memberRankMap[participantId] = initialRanks[participantId];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.message;
|
error.value = e.message;
|
||||||
event.value = null;
|
event.value = null;
|
||||||
@@ -459,6 +722,12 @@ function reload() {
|
|||||||
fetchEventData();
|
fetchEventData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 게스트 추가 시 처리 함수
|
||||||
|
function handleGuestAdded(guest) {
|
||||||
|
// 게스트 추가 후 전체 데이터 다시 불러오기
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const token = sessionStorage.getItem('event_token_' + publicHash);
|
const token = sessionStorage.getItem('event_token_' + publicHash);
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -502,93 +771,34 @@ function getAllTeamsWithUnassigned() {
|
|||||||
return teamList;
|
return teamList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 팀별 게임 총점 계산 함수 - rankingUtils 사용
|
||||||
function getTeamTotalScore(team, gameIdx) {
|
function getTeamTotalScore(team, gameIdx) {
|
||||||
let total = 0;
|
|
||||||
if (!team || !team.members) return 0;
|
if (!team || !team.members) return 0;
|
||||||
team.members.forEach(member => {
|
return calculateTeamGameScore(team, scoresProxy.value, gameIdx);
|
||||||
const score = Number(scoresProxy.value[member.participantId + '-' + gameIdx]) || 0;
|
|
||||||
const memberHc = Number(member.handicap) || 0;
|
|
||||||
total += score + memberHc;
|
|
||||||
});
|
|
||||||
total += Number(team.handicap) || 0;
|
|
||||||
return total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 팀별 핸디캡 합산 계산 함수 (단순 값 반환)
|
// 팀별 핸디캡 합산 계산 함수 - rankingUtils 사용
|
||||||
function getTeamTotalHandicap(team) {
|
function getTeamTotalHandicap(team) {
|
||||||
let total = 0;
|
|
||||||
if (!team || !team.members) return 0;
|
if (!team || !team.members) return 0;
|
||||||
team.members.forEach(member => {
|
return calculateTeamTotalHandicap(team);
|
||||||
const memberHc = Number(member.handicap) || 0;
|
|
||||||
total += memberHc;
|
|
||||||
});
|
|
||||||
total += Number(team.handicap) || 0;
|
|
||||||
return total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전체 게임 합계 기준 팀별 등수 계산 함수
|
// 전체 게임 합계 기준 팀별 등수 계산 함수 - rankingUtils 사용
|
||||||
function getTeamOverallRank(team) {
|
function getTeamOverallRank(team) {
|
||||||
if (!team || !team.id) return '';
|
if (!team || !team.id) return '';
|
||||||
|
|
||||||
|
// 실제 팀만 포함하여 순위 계산
|
||||||
const allTeams = getAllTeamsWithUnassigned();
|
const allTeams = getAllTeamsWithUnassigned();
|
||||||
const teamTotals = allTeams.map(t => ({
|
const rankMap = calculateTeamOverallRankings(allTeams, scoresProxy.value, event.value.gameCount);
|
||||||
teamId: t.id || t.teamNumber,
|
|
||||||
total: Array.from({ length: event.value.gameCount }, (_, i) => getTeamTotalScore(t, i + 1)).reduce((a, b) => a + b, 0),
|
// 해당 팀의 순위 반환
|
||||||
handicapSum: getTeamTotalHandicap(t),
|
const teamId = team.id || team.teamNumber;
|
||||||
memberCount: (t.members ? t.members.length : 0)
|
return rankMap[teamId] || '';
|
||||||
}));
|
|
||||||
teamTotals.sort((a, b) => {
|
|
||||||
if (b.total !== a.total) return b.total - a.total;
|
|
||||||
if (a.handicapSum !== b.handicapSum) return a.handicapSum - b.handicapSum;
|
|
||||||
return b.memberCount - a.memberCount;
|
|
||||||
});
|
|
||||||
let rank = 1;
|
|
||||||
for (let i = 0; i < teamTotals.length; i++) {
|
|
||||||
if (
|
|
||||||
i > 0 && (
|
|
||||||
teamTotals[i].total !== teamTotals[i - 1].total ||
|
|
||||||
teamTotals[i].handicapSum !== teamTotals[i - 1].handicapSum ||
|
|
||||||
teamTotals[i].memberCount !== teamTotals[i - 1].memberCount
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
rank = i + 1;
|
|
||||||
}
|
|
||||||
if ((teamTotals[i].teamId + '') === (team.id + '')) {
|
|
||||||
return rank;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 게임별 등수 계산 함수 (computed)
|
// 게임별 팀 등수 계산 함수 (rankingUtils로 대체됨)
|
||||||
// 통합 등수 산정 함수
|
// 이 함수는 삭제되었으며 새로운 getRanksByGame 함수를 사용합니다.
|
||||||
function getRanksByGame(gameIdx) {
|
|
||||||
const allTeams = getAllTeamsWithUnassigned();
|
|
||||||
const scores = allTeams.map(team => ({
|
|
||||||
teamId: team.id,
|
|
||||||
score: getTeamTotalScore(team, gameIdx),
|
|
||||||
handicapSum: getTeamTotalHandicap(team),
|
|
||||||
memberCount: (team.members ? team.members.length : 0)
|
|
||||||
}));
|
|
||||||
scores.sort((a, b) => {
|
|
||||||
if (b.score !== a.score) return b.score - a.score;
|
|
||||||
if (a.handicapSum !== b.handicapSum) return a.handicapSum - b.handicapSum;
|
|
||||||
return b.memberCount - a.memberCount;
|
|
||||||
});
|
|
||||||
const ranks = {};
|
|
||||||
let rank = 1;
|
|
||||||
scores.forEach((item, i) => {
|
|
||||||
if (
|
|
||||||
i > 0 && (
|
|
||||||
item.score !== scores[i - 1].score ||
|
|
||||||
item.handicapSum !== scores[i - 1].handicapSum ||
|
|
||||||
item.memberCount !== scores[i - 1].memberCount
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
rank = i + 1;
|
|
||||||
}
|
|
||||||
ranks[item.teamId] = rank;
|
|
||||||
});
|
|
||||||
return ranks;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style src="./EventPublicPage.css"></style>
|
<style src="./EventPublicPage.css"></style>
|
||||||
Reference in New Issue
Block a user