/** * 클럽 컨트롤러 * 클럽 관련 기능을 처리하는 컨트롤러 */ const Club = require('../models/Club'); 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 { sequelize } = require('../models/index'); // 관리자 권한 체크 함수 const isAdmin = (user) => { return user && (user.role === 'admin' || user.role === 'superadmin'); }; // 클럽 선택 const allowedMemberTypes = ['owner', 'manager']; exports.selectClub = async (req, res) => { try { const { clubId } = req.body; const userId = req.user && req.user.id; // 클럽 존재 여부 확인 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 관리자 여부 체크 const isAdmin = req.user && (req.user.role === 'admin' || req.user.role === 'superadmin'); let isClubManager = false; if (!isAdmin) { // Members 테이블에서 모임장/운영진 여부 확인 const member = await Member.findOne({ where: { clubId, userId, memberType: ['owner', 'manager'] } }); isClubManager = !!member; } if (!isAdmin && !isClubManager) { return res.status(403).json({ message: '해당 클럽에 대한 권한이 없습니다.' }); } req.session.clubId = clubId; // console.log(`Session clubId: ${clubId}`); res.status(200).json({ message: '클럽을 선택했습니다.' }); } catch (error) { console.error('클럽 선택 오류:', error); res.status(500).json({ message: '클럽을 선택하는 중 오류가 발생했습니다.' }); } }; // 모든 클럽 조회 exports.getAllClubs = async (req, res) => { try { const clubs = await Club.findAll({ where: { status: 'active' }, include: [ { model: Member, where: { memberType: 'owner' }, required: false, attributes: ['id', 'name', 'email', 'phone', 'memberType'] } ], order: [['createdAt', 'DESC']] }); const formattedClubs = clubs.map(club => { const clubData = club.toJSON(); // 모임장 정보 추가 const ownerMember = clubData.Members && clubData.Members.length > 0 ? clubData.Members[0] : null; return { ...clubData, ownerName: ownerMember ? ownerMember.name : '알 수 없음', ownerEmail: ownerMember ? ownerMember.email : '', ownerPhone: ownerMember ? ownerMember.phone : '', ownerMemberType: ownerMember ? ownerMember.memberType : '' }; }); res.json(formattedClubs); } catch (error) { console.error('클럽 목록 조회 오류:', error); res.status(500).json({ message: '클럽 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 특정 클럽 조회 exports.getClubById = async (req, res) => { try { const { clubId } = req.body; // console.log(`clubId: ${clubId}`); if (!clubId) { return res.status(400).json({ message: "유효하지 않은 클럽 ID입니다." }); } const club = await Club.findByPk(clubId); // console.log(`club: ${club}`); if (club) { // 클럽 소유자 정보 조회 const owner = await Member.findOne({where: {userId: club.ownerId}}, { attributes: ['name', 'email', 'memberType'] }); const clubData = club.toJSON(); const formattedClub = { ...clubData, ownerEmail: owner ? owner.email : '', ownerName: owner ? owner.name : '', ownerMemberType: owner ? owner.memberType : '' }; res.json(formattedClub); } else { res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } } catch (error) { console.error('클럽 조회 오류:', error); res.status(500).json({ message: '클럽 정보를 가져오는 중 오류가 발생했습니다.' }); } }; // 새 클럽 생성 exports.createClub = async (req, res) => { try { const { name, location, description, averageCalculationPeriod } = req.body; // 필수 필드 검증 if (!name) { return res.status(400).json({ message: '클럽 이름은 필수 항목입니다.' }); } // 이메일로 사용자 조회 const owner = await User.findOne({ where: { id: req.user.id } }); if (!owner) { return res.status(404).json({ message: '해당 사용자를 찾을 수 없습니다.' }); } // 클럽 생성 const newClub = await Club.create({ name, location, description, averageCalculationPeriod, ownerId: owner.id, memberCount: 1, // 모임장 포함 status: 'active' }); // 클럽 모임장을 클럽 멤버로 추가 await Member.create({ name: owner.name, gender: 'male', userId: owner.id, clubId: newClub.id, memberType: 'owner', joinDate: new Date(), status: 'active' }); // 베이직 1개월 플랜 자동 추가 try { // 베이직 플랜 조회 (ID가 1이라고 가정, 실제 환경에 맞게 조정 필요) const basicPlan = await require('../models').SubscriptionPlan.findOne({ where: { name: 'Basic' } }); if (basicPlan) { const startDate = new Date(); const endDate = new Date(); endDate.setFullYear(endDate.getFullYear() + 1); // 1년 추가 await require('../models').ClubSubscription.create({ clubId: newClub.id, planId: basicPlan.id, startDate, endDate, status: 'active', price: basicPlan.price || 0 }); console.log(`클럽 ${newClub.id}에 베이직 1개월 플랜 자동 추가됨`); } else { console.warn('베이직 플랜을 찾을 수 없어 자동 구독이 추가되지 않았습니다.'); } } catch (subscriptionError) { console.error('자동 구독 추가 오류:', subscriptionError); // 구독 추가 실패해도 클럽 생성은 성공으로 처리 } res.status(201).json(newClub); } catch (error) { console.error('클럽 생성 오류:', error); res.status(500).json({ message: '클럽을 생성하는 중 오류가 발생했습니다.' }); } }; // 클럽 정보 업데이트 exports.updateClub = async (req, res) => { try { const { clubId, name, location, description, contact, ownerEmail, status, femaleHandicap, ownerId, averageCalculationPeriod } = req.body; const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 모임장 변경 확인 (직접 ownerId를 전달받거나 이메일로 변경) let newOwnerId = club.ownerId; let ownerChanged = false; // ownerId가 직접 전달된 경우 (클럽 설정에서 모임장 선택) if (ownerId && ownerId !== club.ownerId) { newOwnerId = ownerId; ownerChanged = true; } // 클럽 정보 업데이트 await club.update({ name: name || club.name, location: location || club.location, description: description !== undefined ? description : club.description, contact: contact !== undefined ? contact : club.contact, status: status || club.status, femaleHandicap: femaleHandicap || club.femaleHandicap, averageCalculationPeriod: averageCalculationPeriod || club.averageCalculationPeriod, ownerId: newOwnerId }); // 모임장이 변경된 경우 모임장 변경 메소드 호출 if (ownerChanged) { try { // 모임장 변경 내부 메소드 호출 await changeClubOwnerInternal(clubId, newOwnerId, ownerEmail); } catch (error) { throw error; } } res.json(club); } catch (error) { console.error('클럽 정보 업데이트 오류:', error); res.status(500).json({ message: '클럽 정보를 업데이트하는 중 오류가 발생했습니다.' }); } }; // 클럽 삭제 exports.deleteClub = async (req, res) => { try { const { clubId } = req.body; // 클럽 존재 여부 확인 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 소프트 삭제 (상태만 변경) await club.update({ status: 'inactive' }); res.status(204).send(); } catch (error) { console.error('클럽 삭제 오류:', error); res.status(500).json({ message: '클럽을 삭제하는 중 오류가 발생했습니다.' }); } }; // 클럽 기능 관리 exports.manageClubFeatures = async (req, res) => { try { const { clubId, features } = req.body; // 클럽 존재 여부 확인 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 클럽 기능 업데이트 await club.update({ features }); res.json({ id: club.id, features: club.features, updatedAt: club.updatedAt }); } catch (error) { console.error('클럽 기능 관리 오류:', error); res.status(500).json({ message: '클럽 기능을 업데이트하는 중 오류가 발생했습니다.' }); } }; // 클럽 회원 목록 조회 exports.getClubMembers = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } // 클럽 존재 여부 확인 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } const members = await Member.findAll({ where: { clubId, status: { [Op.ne]: 'deleted' } }, order: [ ['memberType', 'ASC'], ['name', 'ASC'] ] }); // 클럽 설정에 따른 에버리지 계산 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 = membersWithAverage; res.json(formattedMembers); } catch (error) { console.error('클럽 회원 목록 조회 오류:', error); res.status(500).json({ message: '클럽 회원 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 클럽 회원 추가 exports.addClubMember = async (req, res) => { try { const { clubId, userId, name, gender, memberType, email, phone, handicap, status } = req.body; if (!clubId || !name || !gender) { return res.status(400).json({ message: '필수 정보가 누락되었습니다.' }); } const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 회원 생성 const newMember = await Member.create({ clubId, userId, name, gender, memberType: memberType || 'associate', email, phone, handicap, status: status || 'active' }); // 클럽의 회원 수 업데이트 const memberCount = await Member.count({ where: { clubId: clubId, status: 'active' } }); await club.update({ memberCount }); res.status(201).json(newMember); } catch (error) { console.error('클럽 회원 추가 오류:', error); res.status(500).json({ message: '회원을 추가하는 중 오류가 발생했습니다.' }); } }; // 클럽 회원 정보 수정 exports.updateClubMember = async (req, res) => { try { const { clubId, memberId, name, gender, memberType, email, phone, handicap, status, joinDate } = req.body; if (!clubId || !memberId) { return res.status(400).json({ message: '잘못된 클럽 또는 회원 ID입니다.' }); } const member = await Member.findOne({ where: { id: memberId, clubId: clubId } }); if (!member) { return res.status(404).json({ message: '회원을 찾을 수 없습니다.' }); } // 모임장으로 변경 금지 - 모임장 변경은 별도 API로 처리 if (memberType === 'owner' && member.memberType !== 'owner') { return res.status(403).json({ message: '모임장 변경은 클럽 설정에서만 가능합니다.' }); } // 회원 정보 업데이트 (모임장 유지 또는 다른 유형으로 변경만 허용) await member.update({ name: name !== undefined ? name : member.name, gender: gender !== undefined ? gender : member.gender, memberType: (member.memberType === 'owner' && memberType !== 'owner') ? 'owner' : (memberType !== undefined ? memberType : member.memberType), email: email !== undefined ? email : member.email, phone: phone !== undefined ? phone : member.phone, handicap: handicap !== undefined ? handicap : member.handicap, status: status !== undefined ? status : member.status, joinDate: joinDate !== undefined ? joinDate : member.joinDate }); // 클럽의 회원 수 업데이트 const club = await Club.findByPk(clubId); if (club) { const memberCount = await Member.count({ where: { clubId: clubId, status: 'active' } }); await club.update({ memberCount }); } res.json(member); } catch (error) { console.error('클럽 회원 정보 수정 오류:', error); res.status(500).json({ message: '회원 정보를 수정하는 중 오류가 발생했습니다.' }); } }; // 모임장 변경 내부 메소드 const changeClubOwnerInternal = async (clubId, newOwnerId, ownerEmail = null) => { // 트랜잭션 시작 const t = await sequelize.transaction(); try { // 기존 모임장을 운영진으로 변경 await Member.update( { memberType: 'manager' }, { where: { clubId: clubId, memberType: 'owner', status: 'active' }, transaction: t } ); // 새 모임장 확인 및 업데이트 const newOwnerMember = await Member.findOne({ where: { clubId: clubId, userId: newOwnerId, status: 'active' } }); if (newOwnerMember) { // 기존 회원인 경우 모임장으로 변경 await newOwnerMember.update({ memberType: 'owner' }, { transaction: t }); } else { // 새로운 사용자 정보 가져오기 const newOwnerUser = await User.findByPk(newOwnerId); if (!newOwnerUser) { await t.rollback(); throw new Error('새 모임장 사용자 정보를 찾을 수 없습니다.'); } // 새 회원으로 모임장 추가 await Member.create({ userId: newOwnerId, clubId: clubId, name: newOwnerUser.name || (ownerEmail ? ownerEmail.split('@')[0] : ''), // 이름이 없는 경우 이메일 아이디 사용 gender: 'male', // 성별 정보가 없는 경우 기본값 memberType: 'owner', joinDate: new Date(), status: 'active' }, { transaction: t }); } // 트랜잭션 커밋 await t.commit(); return true; } catch (error) { // 오류 발생 시 트랜잭션 롤백 await t.rollback(); throw error; } }; // 클럽 회원 삭제 exports.deleteClubMember = async (req, res) => { try { const { clubId, memberId } = req.body; if (!clubId || !memberId) { return res.status(400).json({ message: '잘못된 클럽 또는 회원 ID입니다.' }); } const member = await Member.findOne({ where: { id: memberId, clubId: clubId } }); if (!member) { return res.status(404).json({ message: '회원을 찾을 수 없습니다.' }); } // 회원 상태를 deleted로 변경 await member.update({ status: 'deleted' }); // 클럽의 회원 수 감소 const club = await Club.findByPk(clubId); if (club) { const memberCount = await Member.count({ where: { clubId: clubId, status: 'active' } }); await club.update({ memberCount }); } res.json({ message: '회원이 성공적으로 삭제되었습니다.' }); } catch (error) { console.error('클럽 회원 삭제 오류:', error); res.status(500).json({ message: '회원을 삭제하는 중 오류가 발생했습니다.' }); } }; // 회원 통계 조회 exports.getMemberStats = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const members = await Member.findAll({ where: { clubId, status: 'active' } }); const stats = { totalCount: members.length, regularCount: members.filter(m => m.memberType === 'regular').length, associateCount: members.filter(m => m.memberType === 'associate').length }; res.json(stats); } catch (error) { console.error('회원 통계 조회 오류:', error); res.status(500).json({ message: '회원 통계를 가져오는 중 오류가 발생했습니다.' }); } }; // 다음 모임 정보 여러 개 조회 exports.getNextMeetings = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } // 오늘 이후의 예정된 모임 중 status가 active인 것 3개까지 오름차순 정렬 const nextMeetings = await Event.findAll({ where: { clubId, startDate: { [Op.gt]: new Date() }, status: { [Op.or]: ['ready', 'active'] } }, include: [{ model: EventParticipant, where: { status: { [Op.ne]: 'canceled' } }, required: false }], order: [['startDate', 'ASC']], limit: 3 }); const meetings = nextMeetings.map(meeting => ({ date: meeting.startDate, participantsCount: meeting.EventParticipants ? meeting.EventParticipants.length : 0, title: meeting.title, location: meeting.location })); res.json(meetings); } catch (error) { console.error('다음 모임 정보(여러 개) 조회 오류:', error); res.status(500).json({ message: '다음 모임 정보를 가져오는 중 오류가 발생했습니다.' }); } }; // 최근 모임 정보 조회 exports.getLastMeeting = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const lastMeeting = await Event.findOne({ where: { clubId, startDate: { [Op.lt]: new Date() }, status: 'active' }, include: [{ model: EventParticipant, where: { status: { [Op.ne]: 'canceled' } }, required: false }], order: [['startDate', 'DESC']] }); if (!lastMeeting) { return res.json(null); } const meetingData = { date: lastMeeting.startDate, participantsCount: lastMeeting.EventParticipants ? lastMeeting.EventParticipants.length : 0, title: lastMeeting.title, games: lastMeeting.gameCount || 0 }; res.json(meetingData); } catch (error) { console.error('최근 모임 정보 조회 오류:', error); res.status(500).json({ message: '최근 모임 정보를 가져오는 중 오류가 발생했습니다.' }); } }; // 점수 통계 조회 exports.getScoreStats = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } // 최근 3개월 내의 이벤트 점수만 집계 const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); const events = await Event.findAll({ where: { clubId, startDate: { [Op.gte]: threeMonthsAgo }, status: 'active' }, include: [{ model: EventParticipant, include: [{ model: EventScore }] }] }); let allScores = []; let totalGames = 0; events.forEach(event => { (event.EventParticipants || []).forEach(participant => { if (participant.EventScores && participant.EventScores.length > 0) { allScores = allScores.concat(participant.EventScores.map(score => score.score)); totalGames += participant.EventScores.length; } }); }); const stats = { topGame: allScores.length > 0 ? Math.max(...allScores) : 0, averageScore: allScores.length > 0 ? Math.round(allScores.reduce((a, b) => a + b, 0) / allScores.length) : 0, totalGames: totalGames }; res.json(stats); } catch (error) { console.error('점수 통계 조회 오류:', error); res.status(500).json({ message: '점수 통계를 가져오는 중 오류가 발생했습니다.' }); } }; // 상위 회원 목록 조회 exports.getTopMembers = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } // 클럽 설정 가져오기 const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 활동 회원들 전체 조회 (games 컬럼 제거) const members = await Member.findAll({ where: { clubId, status: 'active' }, attributes: ['id', 'name', 'handicap', 'memberType'], limit: 100 // 충분히 넉넉하게 조회 }); // 각 회원별 실제 게임 수 집계 (EventScore → EventParticipant 조인) const memberIds = members.map(m => m.id); // 1. 회원별 EventParticipant 조회 const participants = await EventParticipant.findAll({ where: { memberId: memberIds }, attributes: ['id', 'memberId'] }); const participantIdToMemberId = {}; participants.forEach(p => { participantIdToMemberId[p.id] = p.memberId; }); // 2. EventScore에서 참가자별 점수 조회 const scores = await EventScore.findAll({ where: { participantId: Object.keys(participantIdToMemberId) }, attributes: ['participantId'] }); // 3. memberId별로 게임수 카운트 const gamesMap = {}; scores.forEach(s => { const memberId = participantIdToMemberId[s.participantId]; gamesMap[memberId] = (gamesMap[memberId] || 0) + 1; }); // 1게임 이상 플레이한 회원만 필터링 const filteredMembers = members.filter(m => (gamesMap[m.id] || 0) > 0); // 각 회원의 에버리지 계산 const membersWithAverage = await Promise.all( filteredMembers.map(async (member) => { try { const averageData = await averageService.calculateMemberAverageByClubSetting(member.id, clubId); return { ...member.toJSON(), games: gamesMap[member.id] || 0, average: averageData.average }; } catch (error) { console.error(`회원 ${member.id}의 에버리지 계산 오류:`, error); return { ...member.toJSON(), games: gamesMap[member.id] || 0, average: 0 }; } }) ); // 에버리지로 정렬하고 상위 5명만 반환 const topMembers = membersWithAverage .sort((a, b) => b.average - a.average) .slice(0, 5); res.json(topMembers); } catch (error) { console.error('상위 회원 목록 조회 오류:', error); res.status(500).json({ message: '상위 회원 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 최근 모임 목록 조회 exports.getRecentMeetings = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const recentMeetings = await Event.findAll({ where: { clubId, status: { [Op.notIn]: ['canceled', 'deleted'] }, startDate: { [Op.lt]: new Date() } }, include: [{ model: EventParticipant, where: { status: { [Op.ne]: 'canceled' } }, required: false }], order: [['startDate', 'DESC']], limit: 5 }); const formattedMeetings = recentMeetings.map(meeting => ({ date: meeting.startDate, title: meeting.title, participantsCount: meeting.EventParticipants ? meeting.EventParticipants.length : 0, games: meeting.gameCount || 0 })); res.json(formattedMeetings); } catch (error) { console.error('최근 모임 목록 조회 오류:', error); res.status(500).json({ message: '최근 모임 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 사용자 클럽 목록 조회 exports.getUserClubs = async (req, res) => { try { const { userId } = req.body; // admin이면 모든 클럽을, 아니면 자신의 클럽만 조회 const hasAdminAccess = isAdmin(req.user); const queryOptions = { where: { status: 'active' } }; // admin이 아닌 경우에만 Member 조건 추가 if (!hasAdminAccess) { queryOptions.include = [{ model: Member, where: { userId: userId }, attributes: ['memberType'] }]; } // order 옵션 추가 queryOptions.order = [['createdAt', 'DESC']]; const clubs = await Club.findAll(queryOptions); // 응답 데이터 가공 const formattedClubs = clubs.map(club => { const clubData = club.toJSON(); let memberType = ''; // admin이 아닌 경우에만 Members가 있음 if (!hasAdminAccess && clubData.Members && clubData.Members.length > 0) { memberType = clubData.Members[0].memberType; } else if (hasAdminAccess) { memberType = 'admin'; } return { id: clubData.id, name: clubData.name, location: clubData.location, memberType }; }); res.json(formattedClubs); } catch (error) { console.error('클럽 목록 조회 오류:', error); res.status(500).json({ message: '클럽 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 클럽 통계 조회 exports.getStatistics = async (req, res) => { try { const { clubId, days } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const startDate = new Date(); startDate.setDate(startDate.getDate() - (days || 90)); // 기본값: 90일 // 1. 회원 통계 const members = await Member.findAll({ where: { clubId, status: 'active' } }); // 2. 이벤트 및 참가자 통계 const events = await Event.findAll({ 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. 점수 데이터 수집 (EventParticipants, EventScores 기준) let allScores = []; let totalParticipants = 0; let totalGames = 0; events.forEach(event => { 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; } }); } }); // 4. 점수 분포 계산 const scoreDistribution = Array(10).fill(0); allScores.forEach(score => { const index = Math.floor(score / 30); if (index >= 0 && index < 10) { scoreDistribution[index]++; } }); // 5. 참석률 추이 계산 (EventParticipants 기준) const attendanceTrend = events.map(event => ({ date: event.startDate, attendance: Math.round(((event.EventParticipants ? event.EventParticipants.length : 0) / members.length) * 100) })); // 6. 회원 순위 계산 const activeMembers = await Member.findAll({ where: { clubId, status: 'active' }, 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 => { 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, attendance: memberAttendance[member.id] ? Math.round(memberAttendance[member.id].attended / memberAttendance[member.id].total * 100) : 0 })); // 7. 모임별 통계 const meetingStats = events.map(event => ({ date: event.startDate, title: event.title, participants: event.EventParticipants ? event.EventParticipants.length : 0, games: event.gameCount || 0, 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: membersWithAverage.filter(m => (m.games || 0) > 0).length, averageAttendance: events.length > 0 ? Math.round(totalParticipants / (events.length * members.length) * 100) : 0, highestGame: allScores.length > 0 ? Math.max(...allScores) : 0, averageScore: allScores.length > 0 ? Math.round(allScores.reduce((a, b) => a + b, 0) / allScores.length) : 0, totalGames: totalGames, totalMeetings: events.length, averageParticipants: events.length > 0 ? Math.round(totalParticipants / events.length) : 0, averageGamesPerMeeting: events.length > 0 ? Math.round(totalGames / events.length) : 0, averageHandicap: members.length > 0 ? Math.round(members.reduce((sum, m) => sum + m.handicap, 0) / members.length) : 0, highestHandicap: members.length > 0 ? Math.max(...members.map(m => m.handicap)) : 0, lowestHandicap: members.length > 0 ? Math.min(...members.map(m => m.handicap)) : 0 }, scoreDistribution: scoreDistribution.map((count, index) => ({ range: `${index * 30}-${(index + 1) * 30 - 1}`, count })), attendanceTrend, memberRankings: rankingsWithAttendance, meetingStats, // === 추가 통계 === memberScoreStats, meetingScoreStats, participantTrend, memberAttendanceStats, teamStats }; res.json(response); } catch (error) { console.error('통계 데이터 조회 오류:', error); res.status(500).json({ message: '통계 데이터를 가져오는 중 오류가 발생했습니다.' }); } };