/** * 클럽 컨트롤러 * 클럽 관련 기능을 처리하는 컨트롤러 */ 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 EventParticipant = require('../models/EventParticipant'); const EventScore = require('../models/EventScore'); const { Op } = require('sequelize'); // 관리자 권한 체크 함수 const isAdmin = (user) => { return user && (user.role === 'admin' || user.role === 'superadmin'); }; // 모든 클럽 조회 exports.getAllClubs = async (req, res) => { try { const clubs = await Club.findAll({ where: { status: 'active' }, include: [ { model: Member, where: { memberType: '모임장' }, 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, ownerEmail, contact, features, sendInvite } = req.body; // 필수 필드 검증 if (!name || !ownerEmail) { return res.status(400).json({ message: '클럽 이름과 모임장 이메일은 필수 항목입니다.' }); } // 이메일로 사용자 조회 const owner = await User.findOne({ where: { email: ownerEmail } }); if (!owner) { return res.status(404).json({ message: '해당 이메일로 등록된 사용자를 찾을 수 없습니다.' }); } // 클럽 생성 const newClub = await Club.create({ name, location, description, contact, ownerId: owner.id, memberCount: 1, // 모임장 포함 status: 'active' }); // 클럽 모임장을 클럽 멤버로 추가 await Member.create({ name: owner.name, gender: '남성', userId: owner.id, clubId: newClub.id, memberType: '모임장', joinDate: new Date(), status: 'active' }); // 선택된 기능 추가 if (features && features.length > 0) { const featurePromises = features.map(featureId => ClubFeature.create({ clubId: newClub.id, featureId, enabled: true }) ); await Promise.all(featurePromises); } // 초대 이메일 발송 로직 (sendInvite가 true인 경우) if (sendInvite) { // TODO: 초대 이메일 발송 로직 구현 console.log(`${ownerEmail}에게 초대 이메일 발송 예정`); } res.status(201).json(newClub); } catch (error) { console.error('클럽 생성 오류:', error); res.status(500).json({ message: '클럽을 생성하는 중 오류가 발생했습니다.' }); } }; // 클럽 정보 업데이트 exports.updateClub = async (req, res) => { try { const { id, name, location, description, contact, ownerEmail, status } = req.body; const clubId = id || req.params.id; const club = await Club.findByPk(clubId); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } // 이메일이 변경된 경우 새 모임장 확인 let newOwnerId = club.ownerId; let ownerChanged = false; if (ownerEmail && ownerEmail !== club.ownerEmail) { const newOwner = await User.findOne({ where: { email: ownerEmail } }); if (!newOwner) { return res.status(404).json({ message: '해당 이메일로 등록된 사용자를 찾을 수 없습니다.' }); } newOwnerId = newOwner.id; 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, ownerId: newOwnerId }); // 모임장이 변경된 경우 Members 테이블 업데이트 if (ownerChanged) { // 기존 모임장을 정회원으로 변경 await Member.update( { memberType: '정회원' }, { where: { clubId: clubId, userId: club.ownerId, memberType: '모임장' } } ); // 새 모임장 확인 및 업데이트 const newOwnerMember = await Member.findOne({ where: { clubId: clubId, userId: newOwnerId } }); if (newOwnerMember) { // 기존 회원인 경우 모임장으로 변경 await newOwnerMember.update({ memberType: '모임장' }); } else { // 새로운 사용자 정보 가져오기 const newOwnerUser = await User.findByPk(newOwnerId); if (!newOwnerUser) { return res.status(404).json({ message: '새 모임장 사용자 정보를 찾을 수 없습니다.' }); } // 새 회원으로 모임장 추가 await Member.create({ userId: newOwnerId, clubId: clubId, name: newOwnerUser.name || ownerEmail.split('@')[0], // 이름이 없는 경우 이메일 아이디 사용 gender: '남성', // 성별 정보가 없는 경우 기본값 memberType: '모임장', joinDate: new Date(), status: 'active' }); } } 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 formattedMembers = members.map(member => { const memberData = member.toJSON(); return { id: memberData.id, 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 }; }); 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 } = 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 || '준회원', email, phone }); // 클럽의 회원 수 업데이트 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, status } = 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: '회원을 찾을 수 없습니다.' }); } // 모임장은 한 명만 존재할 수 있음 if (memberType === '모임장') { const existingOwner = await Member.findOne({ where: { clubId: clubId, memberType: '모임장', status: 'active', id: { [Op.ne]: memberId } } }); if (existingOwner) { return res.status(400).json({ message: '이미 다른 모임장이 존재합니다.' }); } } // 회원 정보 업데이트 await member.update({ name: name || member.name, gender: gender || member.gender, memberType: memberType || member.memberType, email: email || member.email, phone: phone || member.phone, status: status || member.status }); // 클럽의 회원 수 업데이트 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: '회원 정보를 수정하는 중 오류가 발생했습니다.' }); } }; // 클럽 회원 삭제 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 === '정회원').length, associateCount: members.filter(m => m.memberType === '준회원').length }; res.json(stats); } catch (error) { console.error('회원 통계 조회 오류:', error); res.status(500).json({ message: '회원 통계를 가져오는 중 오류가 발생했습니다.' }); } }; // 다음 모임 정보 조회 exports.getNextMeeting = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const nextMeeting = await Event.findOne({ where: { clubId, startDate: { [Op.gt]: new Date() }, status: 'active' }, include: [{ model: EventParticipant, where: { status: { [Op.ne]: '취소' } }, required: false }], order: [['startDate', 'ASC']] }); if (!nextMeeting) { return res.json(null); } const meetingData = { date: nextMeeting.startDate, participantsCount: nextMeeting.participants ? nextMeeting.participants.length : 0, title: nextMeeting.title, location: nextMeeting.location }; res.json(meetingData); } 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]: '취소' } }, required: false }], order: [['startDate', 'DESC']] }); if (!lastMeeting) { return res.json(null); } const meetingData = { date: lastMeeting.startDate, participantsCount: lastMeeting.participants ? lastMeeting.participants.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.participants.forEach(participant => { if (participant.scores && participant.scores.length > 0) { allScores = allScores.concat(participant.scores.map(score => score.score)); totalGames += participant.scores.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 topMembers = await Member.findAll({ where: { clubId, status: 'active', games: { [Op.gt]: 0 } // 최소 1게임 이상 플레이한 회원 }, order: [ ['average', 'DESC'] ], limit: 5, attributes: ['id', 'name', 'average', 'games', 'handicap', 'memberType'] }); 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: 'active', startDate: { [Op.lt]: new Date() } }, include: [{ model: EventParticipant, where: { status: { [Op.ne]: '취소' } }, required: false }], order: [['startDate', 'DESC']], limit: 5 }); const formattedMeetings = recentMeetings.map(meeting => ({ date: meeting.startDate, title: meeting.title, participants: meeting.participants ? meeting.participants.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 { // 사용자가 속한 클럽 목록 조회 (Member 테이블 조인) // admin이면 모든 클럽을, 아니면 자신의 클럽만 조회 const hasAdminAccess = isAdmin(req.user); const queryOptions = { where: { status: 'active' } }; // admin이 아닌 경우에만 Member 조건 추가 if (!hasAdminAccess) { queryOptions.include = [{ model: Member, where: { userId: req.user.id }, 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: EventParticipant, include: [{ model: EventScore }] }] }); // 3. 점수 데이터 수집 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; } }); } }); // 4. 점수 분포 계산 const scoreDistribution = Array(10).fill(0); allScores.forEach(score => { const index = Math.floor(score / 30); if (index >= 0 && index < 10) { scoreDistribution[index]++; } }); // 5. 참석률 추이 계산 const attendanceTrend = events.map(event => ({ date: event.startDate, attendance: Math.round((event.participants ? event.participants.length : 0) / members.length * 100) })); // 6. 회원 순위 계산 const memberRankings = await Member.findAll({ where: { clubId, status: 'active', games: { [Op.gt]: 0 } }, order: [['average', 'DESC']], limit: 10, attributes: ['id', 'name', 'average', 'games', 'handicap', 'memberType'] }); // 각 회원의 참석률 계산 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; }); }); const rankingsWithAttendance = memberRankings.map(member => ({ ...member.toJSON(), 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.participants ? event.participants.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) : 0 })); // 8. 응답 데이터 구성 const response = { summary: { totalMembers: members.length, activeMembers: members.filter(m => m.games > 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 }; res.json(response); } catch (error) { console.error('통계 데이터 조회 오류:', error); res.status(500).json({ message: '통계 데이터를 가져오는 중 오류가 발생했습니다.' }); } };