오류 수정, 통계 보강

This commit is contained in:
2025-06-08 20:21:37 +09:00
parent 1a3706d7a5
commit 141a0de6d0
42 changed files with 2645 additions and 799 deletions
+304 -78
View File
@@ -7,9 +7,12 @@ const User = require('../models/User');
const Member = require('../models/Member');
const ClubFeature = require('../models/ClubFeature');
const Event = require('../models/Event');
const EventTeam = require('../models/EventTeam');
const EventTeamMember = require('../models/EventTeamMember');
const EventParticipant = require('../models/EventParticipant');
const EventScore = require('../models/EventScore');
const { Op } = require('sequelize');
const averageService = require('../services/averageService');
// 관리자 권한 체크 함수
const isAdmin = (user) => {
@@ -40,7 +43,7 @@ exports.selectClub = async (req, res) => {
where: {
clubId,
userId,
memberType: ['모임장', '운영진']
memberType: ['owner', 'manager']
}
});
isClubManager = !!member;
@@ -66,7 +69,7 @@ exports.getAllClubs = async (req, res) => {
include: [
{
model: Member,
where: { memberType: '모임장' },
where: { memberType: 'owner' },
required: false,
attributes: ['id', 'name', 'email', 'phone', 'memberType']
}
@@ -161,10 +164,10 @@ exports.createClub = async (req, res) => {
// 클럽 모임장을 클럽 멤버로 추가
await Member.create({
name: owner.name,
gender: '남성',
gender: 'male',
userId: owner.id,
clubId: newClub.id,
memberType: '모임장',
memberType: 'owner',
joinDate: new Date(),
status: 'active'
});
@@ -228,7 +231,7 @@ exports.createClub = async (req, res) => {
// 클럽 정보 업데이트
exports.updateClub = async (req, res) => {
try {
const { clubId, name, location, description, contact, ownerEmail, status, femaleHandicap, ownerId } = req.body;
const { clubId, name, location, description, contact, ownerEmail, status, femaleHandicap, ownerId, averageCalculationPeriod } = req.body;
const club = await Club.findByPk(clubId);
if (!club) {
@@ -262,6 +265,7 @@ exports.updateClub = async (req, res) => {
contact: contact !== undefined ? contact : club.contact,
status: status || club.status,
femaleHandicap: femaleHandicap || club.femaleHandicap,
averageCalculationPeriod: averageCalculationPeriod || club.averageCalculationPeriod,
ownerId: newOwnerId
});
@@ -358,24 +362,47 @@ exports.getClubMembers = async (req, res) => {
]
});
// 클럽 설정에 따른 에버리지 계산
const membersWithAverage = await Promise.all(
members.map(async (member) => {
try {
// 모든 회원에 대해 averageService 호출
const averageData = await averageService.calculateMemberAverageByClubSetting(member.id, clubId);
return {
id: member.id,
userId: member.userId || null,
name: member.name,
gender: member.gender,
memberType: member.memberType,
handicap: member.handicap,
average: averageData.average,
games: averageData.games,
phone: member.phone,
email: member.user ? member.user.email : member.email,
joinDate: member.joinDate,
status: member.status
};
} catch (error) {
return {
id: member.id,
userId: member.userId || null,
name: member.name,
gender: member.gender,
memberType: member.memberType,
handicap: member.handicap,
average: 0,
games: 0,
phone: member.phone,
email: member.user ? member.user.email : member.email,
joinDate: member.joinDate,
status: member.status
};
}
})
);
// 응답 데이터 형식화
const formattedMembers = members.map(member => {
const memberData = member.toJSON();
return {
id: memberData.id,
userId: memberData.userId || null,
name: memberData.name,
gender: memberData.gender,
memberType: memberData.memberType,
handicap: memberData.handicap,
average: memberData.average,
games: memberData.games,
phone: memberData.phone,
email: memberData.user ? memberData.user.email : memberData.email,
joinDate: memberData.joinDate,
status: memberData.status
};
});
const formattedMembers = membersWithAverage;
res.json(formattedMembers);
} catch (error) {
@@ -404,7 +431,7 @@ exports.addClubMember = async (req, res) => {
userId,
name,
gender,
memberType: memberType || '준회원',
memberType: memberType || 'associate',
email,
phone,
handicap,
@@ -443,7 +470,7 @@ exports.updateClubMember = async (req, res) => {
}
// 모임장으로 변경 금지 - 모임장 변경은 별도 API로 처리
if (memberType === '모임장' && member.memberType !== '모임장') {
if (memberType === 'owner' && member.memberType !== 'owner') {
return res.status(403).json({ message: '모임장 변경은 클럽 설정에서만 가능합니다.' });
}
@@ -451,7 +478,7 @@ exports.updateClubMember = async (req, res) => {
await member.update({
name: name || member.name,
gender: gender || member.gender,
memberType: (member.memberType === '모임장' && memberType !== '모임장') ? '모임장' : memberType || member.memberType,
memberType: (member.memberType === 'owner' && memberType !== 'owner') ? 'owner' : memberType || member.memberType,
email: email || member.email,
phone: phone || member.phone,
handicap: handicap || member.handicap,
@@ -480,11 +507,11 @@ const changeClubOwnerInternal = async (clubId, newOwnerId, ownerEmail = null) =>
try {
// 기존 모임장을 운영진으로 변경
await Member.update(
{ memberType: '운영진' },
{ memberType: 'manager' },
{
where: {
clubId: clubId,
memberType: '모임장',
memberType: 'owner',
status: 'active'
},
transaction: t
@@ -502,7 +529,7 @@ const changeClubOwnerInternal = async (clubId, newOwnerId, ownerEmail = null) =>
if (newOwnerMember) {
// 기존 회원인 경우 모임장으로 변경
await newOwnerMember.update({ memberType: '모임장' }, { transaction: t });
await newOwnerMember.update({ memberType: 'owner' }, { transaction: t });
} else {
// 새로운 사용자 정보 가져오기
const newOwnerUser = await User.findByPk(newOwnerId);
@@ -516,8 +543,8 @@ const changeClubOwnerInternal = async (clubId, newOwnerId, ownerEmail = null) =>
userId: newOwnerId,
clubId: clubId,
name: newOwnerUser.name || (ownerEmail ? ownerEmail.split('@')[0] : ''), // 이름이 없는 경우 이메일 아이디 사용
gender: '남성', // 성별 정보가 없는 경우 기본값
memberType: '모임장',
gender: 'male', // 성별 정보가 없는 경우 기본값
memberType: 'owner',
joinDate: new Date(),
status: 'active'
}, { transaction: t });
@@ -588,8 +615,8 @@ exports.getMemberStats = async (req, res) => {
const stats = {
totalCount: members.length,
regularCount: members.filter(m => m.memberType === '정회원').length,
associateCount: members.filter(m => m.memberType === '준회원').length
regularCount: members.filter(m => m.memberType === 'regular').length,
associateCount: members.filter(m => m.memberType === 'associate').length
};
res.json(stats);
@@ -745,19 +772,48 @@ exports.getTopMembers = async (req, res) => {
if (!clubId) {
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
}
// 클럽 설정 가져오기
const club = await Club.findByPk(clubId);
if (!club) {
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
}
const topMembers = await Member.findAll({
// 활동적인 회원들 가져오기 (게임 수로 정렬)
const activeMembers = await Member.findAll({
where: {
clubId,
status: 'active',
games: { [Op.gt]: 0 } // 최소 1게임 이상 플레이한 회원
},
order: [
['average', 'DESC']
],
limit: 5,
attributes: ['id', 'name', 'average', 'games', 'handicap', 'memberType']
attributes: ['id', 'name', 'games', 'handicap', 'memberType'],
order: [['games', 'DESC']],
limit: 20 // 에버리지 계산을 위해 여유있게 가져오기
});
// 각 회원의 에버리지 계산
const membersWithAverage = await Promise.all(
activeMembers.map(async (member) => {
try {
const averageData = await averageService.calculateMemberAverageByClubSetting(member.id, clubId);
return {
...member.toJSON(),
average: averageData.average
};
} catch (error) {
console.error(`회원 ${member.id}의 에버리지 계산 오류:`, error);
return {
...member.toJSON(),
average: 0
};
}
})
);
// 에버리지로 정렬하고 상위 5명만 반환
const topMembers = membersWithAverage
.sort((a, b) => b.average - a.average)
.slice(0, 5);
res.json(topMembers);
} catch (error) {
@@ -881,31 +937,48 @@ exports.getStatistics = async (req, res) => {
// 2. 이벤트 및 참가자 통계
const events = await Event.findAll({
where: {
clubId,
startDate: { [Op.gte]: startDate },
status: 'active'
},
include: [{
model: EventParticipant,
include: [{
model: EventScore
}]
}]
});
where: {
clubId,
startDate: { [Op.gte]: startDate },
status: 'active'
},
include: [
{
model: EventTeam,
include: [
{
model: EventTeamMember,
include: [
{
model: EventParticipant,
include: [
{ model: Member },
{ model: EventScore }
]
}
]
}
]
},
{
model: EventParticipant,
include: [{ model: EventScore }]
}
]
});
// 3. 점수 데이터 수집
// 3. 점수 데이터 수집 (EventParticipants, EventScores 기준)
let allScores = [];
let totalParticipants = 0;
let totalGames = 0;
events.forEach(event => {
if (event.participants) {
totalParticipants += event.participants.length;
event.participants.forEach(participant => {
if (participant.scores) {
allScores = allScores.concat(participant.scores.map(score => score.score));
totalGames += participant.scores.length;
if (event.EventParticipants) {
totalParticipants += event.EventParticipants.length;
event.EventParticipants.forEach(participant => {
if (participant.EventScores) {
allScores = allScores.concat(participant.EventScores.map(score => score.score));
totalGames += participant.EventScores.length;
}
});
}
@@ -920,36 +993,62 @@ exports.getStatistics = async (req, res) => {
}
});
// 5. 참석률 추이 계산
// 5. 참석률 추이 계산 (EventParticipants 기준)
const attendanceTrend = events.map(event => ({
date: event.startDate,
attendance: Math.round((event.participants ? event.participants.length : 0) / members.length * 100)
attendance: Math.round(((event.EventParticipants ? event.EventParticipants.length : 0) / members.length) * 100)
}));
// 6. 회원 순위 계산
const memberRankings = await Member.findAll({
const activeMembers = await Member.findAll({
where: {
clubId,
status: 'active',
games: { [Op.gt]: 0 }
status: 'active'
},
order: [['average', 'DESC']],
limit: 10,
attributes: ['id', 'name', 'average', 'games', 'handicap', 'memberType']
attributes: ['id', 'name', 'handicap', 'memberType'],
limit: 20 // 에버리지 계산을 위해 여유있게 가져오기
});
// 각 회원의 에버리지 계산
const membersWithAverage = await Promise.all(
activeMembers.map(async (member) => {
try {
const averageData = await averageService.calculateMemberAverageByClubSetting(member.id, clubId);
return {
...member.toJSON(),
average: averageData.average,
games: averageData.games,
};
} catch (error) {
console.error(`회원 ${member.id}의 에버리지 계산 오류:`, error);
return {
...member.toJSON(),
average: 0,
games: 0,
};
}
})
);
// 에버리지로 정렬하고 상위 10명만 반환
const memberRankings = membersWithAverage
.sort((a, b) => b.average - a.average)
.slice(0, 10);
// 각 회원의 참석률 계산
const memberAttendance = {};
events.forEach(event => {
event.participants.forEach(participant => {
memberAttendance[participant.memberId] = memberAttendance[participant.memberId] || { attended: 0, total: 0 };
memberAttendance[participant.memberId].attended++;
memberAttendance[participant.memberId].total = events.length;
});
if (Array.isArray(event.participants)) {
event.participants.forEach(participant => {
memberAttendance[participant.memberId] = memberAttendance[participant.memberId] || { attended: 0, total: 0 };
memberAttendance[participant.memberId].attended++;
memberAttendance[participant.memberId].total = events.length;
});
}
});
const rankingsWithAttendance = memberRankings.map(member => ({
...member.toJSON(),
...member,
attendance: memberAttendance[member.id]
? Math.round(memberAttendance[member.id].attended / memberAttendance[member.id].total * 100)
: 0
@@ -959,20 +1058,141 @@ exports.getStatistics = async (req, res) => {
const meetingStats = events.map(event => ({
date: event.startDate,
title: event.title,
participants: event.participants ? event.participants.length : 0,
participants: event.EventParticipants ? event.EventParticipants.length : 0,
games: event.gameCount || 0,
averageScore: event.participants && event.participants.length > 0
? Math.round(event.participants.reduce((sum, p) =>
sum + (p.scores ? p.scores.reduce((s, score) => s + score.score, 0) / p.scores.length : 0),
0) / event.participants.length)
averageScore: event.EventParticipants && event.EventParticipants.length > 0
? Math.round(
event.EventParticipants.reduce(
(sum, p) =>
sum +
(p.EventScores && p.EventScores.length > 0
? p.EventScores.reduce((s, score) => s + score.score, 0) / p.EventScores.length
: 0),
0
) / event.EventParticipants.length
)
: 0
}));
// 8. 추가 통계 계산
// 1. 회원별 최고/최저/총점
const memberScoreStats = await Promise.all(
members.map(async member => {
let scores = [];
events.forEach(event => {
if (event.EventParticipants) {
event.EventParticipants.forEach(participant => {
if (participant.memberId === member.id && participant.EventScores) {
scores = scores.concat(participant.EventScores.map(s => s.score));
}
});
}
});
return {
memberId: member.id,
name: member.name,
maxScore: scores.length > 0 ? Math.max(...scores) : 0,
minScore: scores.length > 0 ? Math.min(...scores) : 0,
totalScore: scores.reduce((a, b) => a + b, 0)
};
})
);
// 2. 모임별 최고/최저/평균점수
const meetingScoreStats = events.map(event => {
let scores = [];
if (event.EventParticipants) {
event.EventParticipants.forEach(participant => {
if (participant.EventScores) {
scores = scores.concat(participant.EventScores.map(s => s.score));
}
});
}
return {
eventId: event.id,
title: event.title,
maxScore: scores.length > 0 ? Math.max(...scores) : 0,
minScore: scores.length > 0 ? Math.min(...scores) : 0,
avgScore: scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 0
};
});
// 4. 참가자 수 변화 추이
const participantTrend = events.map(event => ({
date: event.startDate,
participantCount: event.EventParticipants ? event.EventParticipants.length : 0
}));
// 5. 회원별 모임 참가 횟수
const memberAttendanceStats = members.map(member => ({
memberId: member.id,
name: member.name,
attendCount: events.reduce((cnt, event) =>
cnt + (event.EventParticipants ? event.EventParticipants.filter(p => p.memberId === member.id).length : 0), 0)
}));
// 6. 팀별 통계 (이벤트별 팀별 평균점수/최고점/참가자수)
let teamStats = [];
events.forEach(event => {
(event.EventTeams || []).forEach(team => {
let scores = [];
let members = [];
(team.EventTeamMembers || []).forEach(tm => {
const participant = tm.EventParticipant;
if (participant && participant.EventScores) {
scores = scores.concat(participant.EventScores.map(s => s.score));
}
if (participant && participant.Member) {
members.push({
id: participant.Member.id,
name: participant.Member.name,
participantId: participant.id
});
}
});
teamStats.push({
eventId: event.id,
teamId: team.id,
teamNumber: team.teamNumber,
handicap: team.handicap,
memberCount: members.length,
participantCount: members.length, // 참가자수 필드 명시적으로 추가
members,
avgScore: scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 0,
maxScore: scores.length > 0 ? Math.max(...scores) : 0
});
});
});
// 7. 핸디캡 변화 추이 (회원별, 모임별)
const handicapTrend = members.map(member => {
const trend = events.map(event => {
let handicaps = [];
if (event.EventParticipants) {
event.EventParticipants.forEach(participant => {
if (participant.memberId === member.id && participant.EventScores) {
handicaps = handicaps.concat(participant.EventScores.map(s => s.handicap));
}
});
}
return {
eventId: event.id,
date: event.startDate,
avgHandicap: handicaps.length > 0 ? Math.round(handicaps.reduce((a, b) => a + b, 0) / handicaps.length) : 0
};
});
return {
memberId: member.id,
name: member.name,
trend
};
});
// 8. 응답 데이터 구성
const response = {
summary: {
totalMembers: members.length,
activeMembers: members.filter(m => m.games > 0).length,
activeMembers: membersWithAverage.filter(m => (m.games || 0) > 0).length,
averageAttendance: events.length > 0
? Math.round(totalParticipants / (events.length * members.length) * 100)
: 0,
@@ -1004,7 +1224,13 @@ exports.getStatistics = async (req, res) => {
})),
attendanceTrend,
memberRankings: rankingsWithAttendance,
meetingStats
meetingStats,
// === 추가 통계 ===
memberScoreStats,
meetingScoreStats,
participantTrend,
memberAttendanceStats,
teamStats
};
res.json(response);