diff --git a/backend/controllers/publicController.js b/backend/controllers/publicController.js index e1c0e93..6cdb456 100644 --- a/backend/controllers/publicController.js +++ b/backend/controllers/publicController.js @@ -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; } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 50abe00..5a6e7a7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,4 +1,5 @@ + + diff --git a/frontend/src/utils/rankingUtils.js b/frontend/src/utils/rankingUtils.js new file mode 100644 index 0000000..66089c8 --- /dev/null +++ b/frontend/src/utils/rankingUtils.js @@ -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); +}; diff --git a/frontend/src/views/club/ClubCreate.vue b/frontend/src/views/club/ClubCreate.vue index 947a332..c273fc8 100644 --- a/frontend/src/views/club/ClubCreate.vue +++ b/frontend/src/views/club/ClubCreate.vue @@ -30,7 +30,6 @@