랭킹표시 수정
This commit is contained in:
@@ -84,11 +84,11 @@ exports.getEventByPublicHash = async (event, token = null) => {
|
||||
try {
|
||||
const allMembers = await Member.findAll({
|
||||
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({
|
||||
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 participantMemberIds = [
|
||||
@@ -293,7 +293,7 @@ exports.addPublicGuest = async (req, res) => {
|
||||
}
|
||||
|
||||
// 여성 핸디캡 적용
|
||||
let handicap = null;
|
||||
let handicap = 0;
|
||||
if (gender === '여성' && club.femaleHandicap) {
|
||||
handicap = club.femaleHandicap;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<Toast />
|
||||
<template v-if="isRouterReady">
|
||||
<AppPublicLayout v-if="isPublicLayout">
|
||||
<router-view />
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</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>
|
||||
<span class="ml-2">{{ confirmedCount }} / {{ 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>
|
||||
@@ -47,7 +47,7 @@
|
||||
</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>
|
||||
<span class="ml-2">{{ eventModel.accessPassword }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,20 @@
|
||||
:loading="loading"
|
||||
class="mt-4"
|
||||
:paginator="true"
|
||||
:rows="10"
|
||||
:rows="30"
|
||||
stripedRows
|
||||
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>
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.participant?.name }}
|
||||
@@ -28,9 +38,27 @@
|
||||
</template>
|
||||
</Column>
|
||||
</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>
|
||||
<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>
|
||||
</Column>
|
||||
<Column header="작업">
|
||||
@@ -55,7 +83,7 @@
|
||||
optionValue="id"
|
||||
placeholder="참가자 선택"
|
||||
:class="{'p-invalid': submitted && !editingScore.participantId}"
|
||||
:disabled="!!editingScore.id"
|
||||
:disabled="editingScore.isEdit"
|
||||
/>
|
||||
<small v-if="submitted && !editingScore.participantId" class="p-error">참가자를 선택해주세요.</small>
|
||||
</div>
|
||||
@@ -122,6 +150,7 @@
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import eventService from '@/services/eventService';
|
||||
import { calculateIndividualRankings } from '@/utils/rankingUtils';
|
||||
|
||||
const props = defineProps({
|
||||
eventId: {
|
||||
@@ -368,7 +397,36 @@ const scoreDialogTitle = computed(() => {
|
||||
|
||||
const getTotalScore = (game) => {
|
||||
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) => {
|
||||
@@ -387,6 +445,32 @@ const formatScoreWithHandicap = (game) => {
|
||||
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(() => {
|
||||
if (!participants.value || !scores.value) {
|
||||
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과 연결
|
||||
eventStatus: String
|
||||
});
|
||||
const emits = defineEmits(['close', 'updated', 'update:visible']);
|
||||
const emits = defineEmits(['close', 'updated', 'update:visible', 'guest-added']);
|
||||
|
||||
const selectedMember = ref(null);
|
||||
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 = [
|
||||
...(props.participants || []).map(m => ({ ...m, group: '참가자', id: m.participantId, participantId: m.participantId, memberId: m.memberId })),
|
||||
...(props.availableMembers || []).map(m => ({ ...m, group: '미신청', id: m.memberId, participantId: null, memberId: m.memberId }))
|
||||
...participantOptions,
|
||||
...unregisteredOptions
|
||||
].sort((a, b) => {
|
||||
const nameA = a.name || '';
|
||||
const nameB = b.name || '';
|
||||
@@ -185,8 +212,8 @@ import PublicEventService from '@/services/PublicEventService';
|
||||
|
||||
const newGuest = ref({ name: '', phone: '', gender: '' });
|
||||
const genderOptions = [
|
||||
{ label: '남', value: '남성' },
|
||||
{ label: '여', value: '여성' }
|
||||
{ label: '남성', value: '남성' },
|
||||
{ label: '여성', value: '여성' }
|
||||
];
|
||||
|
||||
async function addNewGuest() {
|
||||
@@ -204,24 +231,66 @@ async function addNewGuest() {
|
||||
average: newGuest.value.average
|
||||
});
|
||||
if (!res || !res.data || !res.data.memberId) throw new Error('게스트 등록 실패');
|
||||
|
||||
// 새 게스트를 멤버 옵션에 추가하고 자동 선택
|
||||
const guest = {
|
||||
...res.data,
|
||||
group: '참가자',
|
||||
id: 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);
|
||||
// 참가자 목록에도 추가
|
||||
props.participants.push(guest);
|
||||
|
||||
// 이미 있는 게스트 업데이트 (선택 가능한 회원 목록)
|
||||
const existingGuestIndex = props.availableMembers.findIndex(m =>
|
||||
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;
|
||||
selectedParticipantId.value = guest.participantId;
|
||||
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: '' };
|
||||
|
||||
// 부모 컴포넌트에 이벤트 발생
|
||||
emits('guest-added', guest);
|
||||
} catch (e) {
|
||||
error.value = e.message || '게스트 등록 중 오류 발생';
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
<div v-for="member in members" :key="member.participantId" class="member-row">
|
||||
<div class="member-info-block">
|
||||
<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>
|
||||
<Tag v-if="member.isGuest" value="G" severity="warning" class="ml-2" />
|
||||
</div>
|
||||
@@ -31,6 +36,14 @@
|
||||
<template v-for="n in gameCount" :key="n">
|
||||
<div class="score-input-hc-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
|
||||
v-if="canInputScore"
|
||||
v-model.number="scoresProxy[member.participantId + '-' + n]"
|
||||
@@ -43,29 +56,29 @@
|
||||
@keydown.enter="onScoreEnter(member, n, teamNumber)"
|
||||
/>
|
||||
<span v-else>{{ member.scores?.[n - 1] ?? '-' }}</span>
|
||||
|
||||
<!-- 핸디캡 포함 점수 표시 -->
|
||||
<span :class="scoreColorClass(scoreStates[member.participantId + '-' + n], scoresProxy[member.participantId + '-' + n], member.handicap)"
|
||||
class="score-hc-value score-status-text"
|
||||
:key="scoreStates[member.participantId + '-' + n] + '-' + scoresProxy[member.participantId + '-' + n]">
|
||||
{{ (scoresProxy[member.participantId + '-' + n] !== undefined && scoresProxy[member.participantId + '-' + n] !== '') ?
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="member-score-summary">
|
||||
<span>총점: {{ getMemberTotalScore(member) }}</span>
|
||||
<span class="member-score-avg">/ 에버리지: {{ getMemberAverageScore(member) }}</span>
|
||||
<span class="member-score-avg">/ 평균: {{ getMemberAverageScore(member) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { toRefs } from 'vue'
|
||||
import { computed, onMounted, watch, ref } from 'vue';
|
||||
import { calculateIndividualRankings, getRankColorClass } from '@/utils/rankingUtils';
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
members: Array,
|
||||
@@ -80,6 +93,127 @@ const props = defineProps({
|
||||
onScoreEnter: Function,
|
||||
getMemberTotalScore: 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>
|
||||
|
||||
<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" />
|
||||
</form>
|
||||
</div>
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
@remove-participant="removeParticipant"
|
||||
/>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -103,7 +103,6 @@
|
||||
<Button label="예" icon="pi pi-check" class="p-button-danger" @click="deleteEvent" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<Toast />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -25,7 +25,17 @@
|
||||
sortField="name"
|
||||
:sortOrder="1"
|
||||
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="memberType" header="회원 유형" sortable style="width: 15%">
|
||||
<template #body="slotProps">
|
||||
@@ -177,13 +187,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import apiClient from '@/services/api';
|
||||
import clubService from '@/services/clubService';
|
||||
import { useClubStore } from '@/stores/clubStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
const toast = useToast();
|
||||
const clubStore = useClubStore();
|
||||
const toast = useToast();
|
||||
|
||||
// 회원 유형 목록
|
||||
const memberTypes = ref([
|
||||
@@ -412,23 +421,27 @@ const hideDialog = () => {
|
||||
// 회원 저장
|
||||
const saveMember = async () => {
|
||||
try {
|
||||
const memberData = {
|
||||
...member.value
|
||||
};
|
||||
const memberData = { ...member.value };
|
||||
|
||||
if (dialogMode.value === 'new') {
|
||||
await clubService.addClubMember(clubId.value, memberData);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: '새 회원이 추가되었습니다.', life: 3000 });
|
||||
} else {
|
||||
await clubService.updateClubMember(clubId.value, memberData.id, memberData);
|
||||
toast.add({ severity: 'success', summary: '성공', detail: '회원 정보가 수정되었습니다.', life: 3000 });
|
||||
}
|
||||
const isNew = dialogMode.value === 'new';
|
||||
const response = isNew
|
||||
? await clubService.addClubMember(clubId.value, memberData)
|
||||
: await clubService.updateClubMember(clubId.value, memberData.id, memberData);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: '성공',
|
||||
detail: isNew ? '새 회원이 추가되었습니다.' : '회원 정보가 수정되었습니다.',
|
||||
life: 3000
|
||||
});
|
||||
|
||||
hideDialog();
|
||||
await loadMembers();
|
||||
} catch (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;
|
||||
}
|
||||
|
||||
/* 회원 이름, 연락처 스타일 */
|
||||
.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) {
|
||||
padding: 1rem;
|
||||
|
||||
@@ -283,19 +283,16 @@
|
||||
|
||||
/* 순위별 색상 */
|
||||
.rank-badge.rank-1 {
|
||||
color: var(--p-yellow-500);
|
||||
color: gold;
|
||||
font-weight: 900;
|
||||
}
|
||||
.rank-badge.rank-2 {
|
||||
color: var(--p-gray-400);
|
||||
color: silver;
|
||||
font-weight: 900;
|
||||
}
|
||||
.rank-badge.rank-3 {
|
||||
color: var(--p-orange-500);
|
||||
}
|
||||
.rank-badge.rank-4 {
|
||||
color: var(--p-blue-400);
|
||||
}
|
||||
.rank-badge.rank-5 {
|
||||
color: var(--p-green-400);
|
||||
color: #cd7f32;
|
||||
font-weight: 900;
|
||||
}
|
||||
/* 필요시 더 추가 가능 */
|
||||
|
||||
@@ -324,9 +321,8 @@
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--p-yellow-500);
|
||||
color: var(--p-black-500);
|
||||
}
|
||||
|
||||
.score-table th,
|
||||
@@ -456,6 +452,14 @@
|
||||
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,
|
||||
@@ -499,6 +503,9 @@
|
||||
.member-score-summary {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
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>
|
||||
<div class="event-public-card">
|
||||
<div v-if="loading" class="text-center">로딩 중...</div>
|
||||
<div v-else-if="error" class="text-center">
|
||||
<i class="pi pi-exclamation-triangle text-red block"></i>
|
||||
<div v-else-if="error" class="text-center text-4xl">
|
||||
<i class="pi pi-exclamation-triangle text-red block text-8xl"></i>
|
||||
<span class="text-red">{{ error }}</span>
|
||||
</div>
|
||||
<div v-else-if="!isAuthenticated">
|
||||
@@ -63,11 +63,11 @@
|
||||
</div>
|
||||
<div class="p-text-secondary mb-4">{{ event.description || '' }}</div>
|
||||
<Divider />
|
||||
<!-- 팀별 게임별 점수/등수 표 -->
|
||||
</div>
|
||||
<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'">
|
||||
<Column header="팀">
|
||||
<template #body="slotProps">
|
||||
@@ -99,14 +99,67 @@
|
||||
</DataTable>
|
||||
</template>
|
||||
</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>
|
||||
<template #content>
|
||||
<MemberListCard
|
||||
:title="parseInt(team.teamNumber) ? '팀 ' + team.teamNumber : '미배정 인원'"
|
||||
:members="team.members"
|
||||
:teamHandicap="team.handicap"
|
||||
:teamNumber="team.teamNumber"
|
||||
title="참가자"
|
||||
:members="sortedParticipants"
|
||||
:gameCount="event.gameCount"
|
||||
:canInputScore="canInputScore"
|
||||
:scoresProxy="scoresProxy"
|
||||
@@ -117,71 +170,90 @@
|
||||
: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>
|
||||
<template #title>
|
||||
<div class="card-title-with-button">
|
||||
<h2>최종 순위</h2>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<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"
|
||||
/>
|
||||
<DataTable :value="getFinalRankingData()" stripedRows :paginator="true" :rows="30"
|
||||
paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
|
||||
:rowsPerPageOptions="[5,10,20,30,50]" responsiveLayout="scroll">
|
||||
|
||||
<!-- 순위 열 -->
|
||||
<Column field="rank" header="순위" :sortable="true" style="width: 3%;">
|
||||
<template #body="slotProps">
|
||||
<span :class="'rank-badge rank-' + slotProps.data.rank">
|
||||
{{ slotProps.data.rank }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<!-- 이름 열 -->
|
||||
<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>
|
||||
</Card>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<JoinDialog v-if="showJoinDialog && canJoin" v-model:visible="showJoinDialog" :publicHash="publicHash"
|
||||
:participants="participants"
|
||||
:availableMembers="availableMembers" :eventStatus="event.status"
|
||||
@close="showJoinDialog = false" @updated="reload" />
|
||||
@close="showJoinDialog = false" @updated="reload" @guest-added="handleGuestAdded" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { useRoute } from 'vue-router';
|
||||
import MemberListCard from '@/components/public/MemberListCard.vue';
|
||||
@@ -236,6 +308,12 @@ const joinButtonLabel = computed(() => {
|
||||
// 점수 상태 관리: idle, saving, saved, error
|
||||
const scoreStates = reactive({});
|
||||
|
||||
// 총점 기준 순위 맵 (reactive 값)
|
||||
const memberRankMap = reactive({});
|
||||
|
||||
// 게임별 순위 맵
|
||||
const gameRanks = reactive({});
|
||||
|
||||
// 점수 색상 클래스 계산
|
||||
// 점수 색상 클래스 계산 (핸디캡 포함)
|
||||
function scoreColorClass(state, value, handicap) {
|
||||
@@ -271,12 +349,81 @@ const saveScore = debounce(async (member, n, oldValue, newValue, teamNumber) =>
|
||||
member.scores[n-1] = Number(newValue);
|
||||
}
|
||||
setTimeout(() => { scoreStates[key] = 'idle'; }, 1200); // 일정 시간 후 아이콘 사라지게
|
||||
|
||||
// 점수 저장 후 순위 업데이트
|
||||
updateMemberRanks();
|
||||
} catch (e) {
|
||||
scoreStates[key] = 'error';
|
||||
setTimeout(() => { scoreStates[key] = 'idle'; }, 2000);
|
||||
}
|
||||
}, 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) {
|
||||
const key = member.participantId + '-' + n;
|
||||
@@ -306,31 +453,123 @@ function onScoreEnter(member, n, teamNumber) {
|
||||
saveScore(member, n, member.scores?.[n-1] ?? '', val, teamNumber);
|
||||
}
|
||||
|
||||
// 멤버별 평균 점수 계산 (입력된 점수만 평균, 소수점 1자리)
|
||||
// 멤버별 평균 점수 계산 (입력된 점수만 평균, 소수점 1자리) - rankingUtils 사용
|
||||
function getMemberAverageScore(member) {
|
||||
if (!member || !event.value.gameCount) return 0;
|
||||
let total = 0, count = 0;
|
||||
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;
|
||||
return calculateMemberAverageScore(member, scoresProxy.value, event.value.gameCount);
|
||||
}
|
||||
|
||||
// 멤버별 핸디캡 포함 총점 계산
|
||||
// 멤버별 핸디캡 포함 총점 계산 - rankingUtils 사용
|
||||
function getMemberTotalScore(member) {
|
||||
if (!member || !event.value.gameCount) return 0;
|
||||
let total = 0;
|
||||
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;
|
||||
return calculateParticipantTotalScore(member, scoresProxy.value, event.value.gameCount);
|
||||
}
|
||||
|
||||
// 총점 기준 순위 계산 (일반 함수로 변경)
|
||||
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)
|
||||
@@ -423,6 +662,30 @@ async function fetchEventData(password) {
|
||||
// 점수 상태는 항상 '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) {
|
||||
error.value = e.message;
|
||||
event.value = null;
|
||||
@@ -459,6 +722,12 @@ function reload() {
|
||||
fetchEventData();
|
||||
}
|
||||
|
||||
// 게스트 추가 시 처리 함수
|
||||
function handleGuestAdded(guest) {
|
||||
// 게스트 추가 후 전체 데이터 다시 불러오기
|
||||
reload();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const token = sessionStorage.getItem('event_token_' + publicHash);
|
||||
if (token) {
|
||||
@@ -502,93 +771,34 @@ function getAllTeamsWithUnassigned() {
|
||||
return teamList;
|
||||
}
|
||||
|
||||
// 팀별 게임 총점 계산 함수 - rankingUtils 사용
|
||||
function getTeamTotalScore(team, gameIdx) {
|
||||
let total = 0;
|
||||
if (!team || !team.members) return 0;
|
||||
team.members.forEach(member => {
|
||||
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;
|
||||
return calculateTeamGameScore(team, scoresProxy.value, gameIdx);
|
||||
}
|
||||
|
||||
// 팀별 핸디캡 합산 계산 함수 (단순 값 반환)
|
||||
// 팀별 핸디캡 합산 계산 함수 - rankingUtils 사용
|
||||
function getTeamTotalHandicap(team) {
|
||||
let total = 0;
|
||||
if (!team || !team.members) return 0;
|
||||
team.members.forEach(member => {
|
||||
const memberHc = Number(member.handicap) || 0;
|
||||
total += memberHc;
|
||||
});
|
||||
total += Number(team.handicap) || 0;
|
||||
return total;
|
||||
return calculateTeamTotalHandicap(team);
|
||||
}
|
||||
|
||||
// 전체 게임 합계 기준 팀별 등수 계산 함수
|
||||
// 전체 게임 합계 기준 팀별 등수 계산 함수 - rankingUtils 사용
|
||||
function getTeamOverallRank(team) {
|
||||
if (!team || !team.id) return '';
|
||||
|
||||
// 실제 팀만 포함하여 순위 계산
|
||||
const allTeams = getAllTeamsWithUnassigned();
|
||||
const teamTotals = allTeams.map(t => ({
|
||||
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),
|
||||
memberCount: (t.members ? t.members.length : 0)
|
||||
}));
|
||||
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 '';
|
||||
const rankMap = calculateTeamOverallRankings(allTeams, scoresProxy.value, event.value.gameCount);
|
||||
|
||||
// 해당 팀의 순위 반환
|
||||
const teamId = team.id || team.teamNumber;
|
||||
return rankMap[teamId] || '';
|
||||
}
|
||||
|
||||
// 게임별 등수 계산 함수 (computed)
|
||||
// 통합 등수 산정 함수
|
||||
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;
|
||||
}
|
||||
// 게임별 팀 등수 계산 함수 (rankingUtils로 대체됨)
|
||||
// 이 함수는 삭제되었으며 새로운 getRanksByGame 함수를 사용합니다.
|
||||
|
||||
|
||||
</script>
|
||||
<style src="./EventPublicPage.css"></style>
|
||||
Reference in New Issue
Block a user