오류 수정, 통계 보강
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user