/** * 이벤트 컨트롤러 * 이벤트 관련 기능을 처리하는 컨트롤러 */ const { Event, EventParticipant, Member, Club, EventScore, ClubMember, EventTeam, EventTeamMember } = require('../models'); const { Op } = require('sequelize'); const { sequelize } = require('../models'); const averageService = require('../services/averageService'); // 이벤트 목록 조회 exports.getAllEvents = async (req, res) => { try { const { clubId } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const events = await Event.findAll({ where: { clubId, status: { [Op.ne]: 'deleted' } }, include: [ { model: Club, attributes: ['id', 'name'] }, { model: EventParticipant, attributes: ['id', 'status', 'paymentStatus'], where: { status: { [Op.ne]: 'canceled' } }, required: false, include: [{ model: Member, attributes: ['id', 'name', 'memberType', 'gender'] }] } ], order: [['startDate', 'ASC']] }); const formattedEvents = events.map(event => { const eventData = event.toJSON(); return { ...eventData, participantCount: eventData.participants ? eventData.participants.length : 0, clubName: eventData.Club ? eventData.Club.name : '' }; }); res.json(formattedEvents); } catch (error) { console.error('이벤트 목록 조회 오류:', error); res.status(500).json({ message: '이벤트 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 특정 이벤트 조회 exports.getEventById = async (req, res) => { try { const eventId = req.params.id; if (!eventId) { return res.status(400).json({ message: '이벤트 ID가 필요합니다.' }); } const event = await Event.findOne({ where: { id: eventId, status: { [Op.ne]: 'deleted' } }, include: [ { model: Club, attributes: ['id', 'name', 'location'] }, { model: EventParticipant, attributes: ['id', 'status', 'paymentStatus'], where: { status: { [Op.ne]: 'canceled' } }, required: false, include: [ { model: Member, attributes: ['id', 'name'] } ] } ] }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } const eventData = event.toJSON(); // 참가자들의 동적 에버리지 계산 if (eventData.participants && eventData.participants.length > 0) { const participantsWithDynamicAverage = await Promise.all( eventData.participants.map(async (participant) => { if (participant.Member && participant.Member.id) { try { const averageData = await averageService.calculateMemberAverageByClubSetting( participant.Member.id, eventData.clubId ); return { ...participant, Member: { ...participant.Member, average: averageData.average } }; } catch (error) { console.error(`회원 ${participant.Member.id}의 에버리지 계산 오류:`, error); return { ...participant, Member: { ...participant.Member, average: 0 } }; } } return participant; }) ); eventData.participants = participantsWithDynamicAverage; } const formattedEvent = { ...eventData, participantCount: eventData.participants ? eventData.participants.length : 0, clubName: eventData.Club ? eventData.Club.name : '' }; res.json(formattedEvent); } catch (error) { console.error('이벤트 조회 오류:', error); res.status(500).json({ message: '이벤트 정보를 가져오는 중 오류가 발생했습니다.' }); } }; // 새 이벤트 생성 exports.createEvent = async (req, res) => { try { const { clubId, title, description, startDate, endDate, location, maxParticipants, eventType, entryFee, gameCount, laneCount, status } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const club = await Club.findOne({ where: { id: clubId, status: 'active' } }); if (!club) { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } const endDateVal = endDate ? new Date(endDate) : null; // 랜덤 해시 생성 함수 const generateRandomHash = async () => { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let hash; let exists = true; while (exists) { hash = Array.from({ length: 16 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); // 중복 체크 exists = await Event.findOne({ where: { publicHash: hash } }); } return hash; }; const publicHash = await generateRandomHash(); const accessPassword = req.body.accessPassword || null; const event = await Event.create({ clubId, title, description, startDate: startDate ? startDate : null, endDate: endDateVal, location, maxParticipants: maxParticipants || 0, eventType: eventType || '일반', entryFee: entryFee || 0, gameCount: gameCount || 3, laneCount: laneCount || 1, status: status || '활성', publicHash, accessPassword }); const createdEvent = await Event.findOne({ where: { id: event.id }, include: [ { model: Club, attributes: ['id', 'name'] } ] }); res.status(201).json({ message: '이벤트가 생성되었습니다.', event: { ...createdEvent.toJSON(), participantCount: 0, clubName: createdEvent.Club ? createdEvent.Club.name : '' } }); } catch (error) { console.error('이벤트 생성 오류:', error); res.status(500).json({ message: '이벤트 생성 중 오류가 발생했습니다.' }); } }; // 공개 이벤트 조회 및 비밀번호 인증 exports.getEventByPublicHash = async (req, res) => { const { publicHash } = req.params; const { password } = req.body || {}; try { // 1. 이벤트 조회 const event = await Event.findOne({ where: { publicHash }, include: [{ model: Club, attributes: ['id', 'name'] }] }); if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); if (event.status === 'canceled') return res.status(403).json({ message: '취소된 이벤트입니다.' }); // 2. 비밀번호 필요 여부 및 인증 if (event.accessPassword) { if (!password) return res.status(401).json({ needPassword: true, message: '비밀번호 필요' }); if (event.accessPassword !== password) return res.status(401).json({ needPassword: true, message: '비밀번호 불일치' }); } // 3. 전체 회원 명단 추출 (clubId 사용, clubId는 응답에 포함 X) const allMembers = await Member.findAll({ where: { clubId: event.clubId }, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender'] }); // 전체 회원의 동적 에버리지 계산 const allMembersWithAverage = await Promise.all( allMembers.map(async (member) => { try { const averageData = await averageService.calculateMemberAverageByClubSetting( member.dataValues.memberId, event.clubId ); return { ...member.dataValues, average: averageData.average }; } catch (error) { console.error(`회원 ${member.dataValues.memberId}의 에버리지 계산 오류:`, error); return { ...member.dataValues, average: 0 }; } }) ); console.log('allMembersWithAverage', allMembersWithAverage); // 4. 참가자 명단 추출 (참가자 테이블 + Member 조인) const participants = await EventParticipant.findAll({ where: { eventId: event.id, status: { [Op.ne]: 'canceled' } }, include: [ { model: Member, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender'] } ] }); // 참가자들의 동적 에버리지 계산 const participantsWithAverage = await Promise.all( participants.map(async (participant) => { if (participant.Member && participant.Member.dataValues.memberId) { try { const averageData = await averageService.calculateMemberAverageByClubSetting( participant.Member.dataValues.memberId, event.clubId ); // 기존 객체 복사 const participantObj = {...participant.dataValues}; participantObj.Member = { ...participant.Member.dataValues, average: averageData.average }; return participantObj; } catch (error) { console.error(`참가자 회원 ${participant.Member.dataValues.memberId}의 에버리지 계산 오류:`, error); const participantObj = {...participant.dataValues}; participantObj.Member = { ...participant.Member.dataValues, average: 0 }; return participantObj; } } return participant.dataValues; }) ); // 5. 참가 신청 가능 명단(아직 참가하지 않은 회원) // '취소'가 아닌 status가 하나라도 있으면 참가자로 간주 (중복 제거) const participantMemberIds = [ ...new Set( participantsWithAverage .filter(p => p.status !== '취소') .map(p => parseInt(p.memberId)) ) ]; const availableMembers = allMembersWithAverage.filter(m => !participantMemberIds.includes(parseInt(m.memberId))); // 6. 팀 정보 등 추가 조회 (기존 코드 유지) const teams = await EventTeam.findAll({ where: { eventId: event.id }, order: [['teamNumber', 'ASC']], include: [ { model: EventTeamMember, include: [ { model: EventParticipant, include: [{ model: Member, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'handicap'] }] } ] } ] }); // 점수 const scores = await EventScore.findAll({ where: { eventId: event.id } }); // 팀별 그룹핑 let teamList = []; if (teams.length > 0) { // 팀 리스트 처리 const teamsWithMembers = []; for (const team of teams) { const membersWithAverage = []; for (const tm of (team.EventTeamMembers || [])) { const participant = tm.EventParticipant; const member = participant?.Member; const scoreObj = scores.find(s => s.participantId === participant?.id); // 회원의 동적 에버리지 계산 let dynamicAverage = 0; if (member && member.memberId) { try { const averageData = await averageService.calculateMemberAverageByClubSetting( member.memberId, event.clubId ); dynamicAverage = averageData.average; } catch (error) { console.error(`팀 멤버 ${member.memberId}의 에버리지 계산 오류:`, error); } } membersWithAverage.push({ participantId: participant?.id, memberId: member?.memberId, name: member?.name || `참가자 ${participant?.id}`, gender: member?.gender, memberType: member?.memberType, average: dynamicAverage, handicap: member?.handicap || 0, status: participant?.status, paymentStatus: participant?.paymentStatus, comment: participant?.comment, isGuest: member?.memberType === '게스트', score: scoreObj ? scoreObj.score : null }); } teamsWithMembers.push({ teamId: team.id, teamNumber: team.teamNumber, handicap: team.handicap, members: membersWithAverage }); } teamList = teamsWithMembers; } // 참가자 리스트(팀 없는 경우) let participantList = []; // 참가자들의 동적 에버리지 계산 participantList = participantsWithAverage.map(p => { const member = p.Member; const scoreObj = scores.find(s => s.participantId === p.id); return { participantId: p.id, memberId: p.memberId, name: member?.name || `참가자 ${p.id}`, gender: member?.gender, memberType: member?.memberType, handicap: member?.handicap || 0, average: member?.average || 0, // 이미 participantsWithAverage에서 동적 계산된 값 status: p.status, paymentStatus: p.paymentStatus, comment: p.comment, isGuest: member?.memberType === '게스트', score: scoreObj ? scoreObj.score : null }; }); // 반환 데이터 res.json({ clubName: event.Club.name, event: { id: event.id, title: event.title, description: event.description, eventType: event.eventType, startDate: event.startDate, endDate: event.endDate, location: event.location, gameCount: event.gameCount, registrationDeadline: event.registrationDeadline, maxParticipants: event.maxParticipants, participantFee: event.participantFee, status: event.status }, teams: teamList, participants: participantList, availableMembers, needPassword: !!event.accessPassword }); } catch (e) { console.error('공개 이벤트 조회 오류:', e); res.status(500).json({ message: '이벤트 정보를 가져오는 중 오류가 발생했습니다.' }); } }; // 공개 URL(publicHash) 재생성 exports.regeneratePublicHash = async (req, res) => { const { id } = req.params; try { const event = await Event.findOne({ where: { id } }); if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); // 랜덤 해시 생성 (중복 없는 값) const generateRandomHash = async () => { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let hash; let exists = true; while (exists) { hash = Array.from({ length: 16 }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); exists = await Event.findOne({ where: { publicHash: hash } }); } return hash; }; const newHash = await generateRandomHash(); await event.update({ publicHash: newHash }); res.json({ publicHash: newHash }); } catch (e) { console.error('공개 URL 재생성 오류:', e); res.status(500).json({ message: '공개 URL 재생성 중 오류가 발생했습니다.' }); } }; // 공개 참가자 등록/수정 exports.registerPublicParticipant = async (req, res) => { const { publicHash } = req.params; const { participantId, memberId, status, paymentStatus, comment } = req.body || {}; try { const event = await Event.findOne({ where: { publicHash } }); if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); if (event.status === '취소') return res.status(403).json({ message: '취소된 이벤트입니다.' }); // 기존 참가자 있는지 검사(이름+연락처 기준) let member = await Member.findOne({ where: { id:memberId } }); // if (!member) { // member = await Member.create({ memberId, memberType: '일반' }); // } let participant = await EventParticipant.findOne({ where: { id: participantId, memberId: member.id } }); if (!participant) { participant = await EventParticipant.create({ eventId: event.id, memberId: member.id, comment, status, paymentStatus }); } else { await participant.update({ comment, status, paymentStatus }); } res.json({ message: '참가 신청/수정이 완료되었습니다.' }); } catch (e) { console.error('공개 참가자 등록 오류:', e); res.status(500).json({ message: '참가 신청/수정 중 오류가 발생했습니다.' }); } }; // 공개 게스트 추가 exports.addPublicGuest = async (req, res) => { const { publicHash } = req.params; const { name, password } = req.body || {}; try { const event = await Event.findOne({ where: { publicHash } }); if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); if (event.status === '취소') return res.status(403).json({ message: '취소된 이벤트입니다.' }); if (event.accessPassword) { if (!password || event.accessPassword !== password) return res.status(401).json({ message: '비밀번호가 올바르지 않습니다.' }); } // 게스트 멤버 생성 const member = await Member.create({ name, memberType: '게스트' }); await EventParticipant.create({ eventId: event.id, memberId: member.id }); res.json({ message: '게스트가 추가되었습니다.' }); } catch (e) { console.error('공개 게스트 추가 오류:', e); res.status(500).json({ message: '게스트 추가 중 오류가 발생했습니다.' }); } }; // 공개 점수 수정 exports.updatePublicScore = async (req, res) => { const { publicHash } = req.params; const { participantId, score, password } = req.body || {}; try { const event = await Event.findOne({ where: { publicHash } }); if (!event) return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); if (event.status !== '준비' && event.status !== '활성') return res.status(403).json({ message: '점수 수정이 불가능한 상태입니다.' }); if (event.accessPassword) { if (!password || event.accessPassword !== password) return res.status(401).json({ message: '비밀번호가 올바르지 않습니다.' }); } let eventScore = await EventScore.findOne({ where: { eventId: event.id, participantId } }); if (!eventScore) { eventScore = await EventScore.create({ eventId: event.id, participantId, score }); } else { await eventScore.update({ score }); } res.json({ message: '점수가 저장되었습니다.' }); } catch (e) { console.error('공개 점수 수정 오류:', e); res.status(500).json({ message: '점수 저장 중 오류가 발생했습니다.' }); } }; // 이벤트 정보 업데이트 exports.updateEvent = async (req, res) => { try { const { clubId, title, description, startDate, location, maxParticipants, eventType, entryFee, gameCount, laneCount, status, registrationDeadline, publicHash, accessPassword } = req.body; const eventId = req.body.id; let endDate = req.body.endDate; if (!eventId || !clubId) { return res.status(400).json({ message: '이벤트 ID와 클럽 ID가 필요합니다.' }); } const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } if(endDate == 'Invalid Date') { endDate = null; } const updateData = { title, description, startDate: startDate ? startDate : event.startDate, endDate: endDate || null, location, maxParticipants: maxParticipants || event.maxParticipants, eventType: eventType || event.eventType, entryFee: entryFee !== undefined ? entryFee : event.entryFee, gameCount: gameCount || event.gameCount, laneCount: laneCount || event.laneCount, status: status || event.status, registrationDeadline: registrationDeadline || event.registrationDeadline, publicHash: publicHash || event.publicHash, accessPassword: accessPassword || null }; await event.update(updateData); const updatedEvent = await Event.findOne({ where: { id: eventId }, include: [ { model: Club, attributes: ['id', 'name'] }, { model: EventParticipant, attributes: ['id', 'status'], where: { status: { [Op.ne]: 'canceled' } }, required: false } ] }); if (!updatedEvent) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } res.json({ message: '이벤트가 수정되었습니다.', event: { ...updatedEvent.toJSON(), participantCount: updatedEvent.participants ? updatedEvent.participants.length : 0, clubName: updatedEvent.Club ? updatedEvent.Club.name : '' } }); } catch (error) { console.error('이벤트 수정 오류:', error); res.status(500).json({ message: '이벤트 수정 중 오류가 발생했습니다.' }); } }; // 이벤트 삭제 (소프트 삭제) exports.deleteEvent = async (req, res) => { try { const eventId = req.params.id; const { clubId } = req.body; console.log(eventId, clubId, req.body, req.params); if (!eventId || !clubId) { return res.status(400).json({ message: '이벤트 ID와 클럽 ID가 필요합니다.' }); } const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } }, include: [ { model: Club, attributes: ['id', 'name'] } ] }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 참가자 정보도 함께 삭제 처리 await EventParticipant.update( { status: '취소' }, { where: { eventId, status: { [Op.ne]: 'canceled' } } } ); await event.update({ status: '삭제' }); res.json({ message: '이벤트가 삭제되었습니다.', event: { id: event.id, title: event.title, clubId: event.clubId, clubName: event.Club ? event.Club.name : '' } }); } catch (error) { console.error('이벤트 삭제 오류:', error); res.status(500).json({ message: '이벤트 삭제 중 오류가 발생했습니다.' }); } }; // 이벤트 점수 조회 exports.getEventScores = async (req, res) => { try { const eventId = req.params.id; if (!eventId) { return res.status(400).json({ message: '이벤트 ID가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } const scores = await EventScore.findAll({ where: { eventId }, include: [ { model: EventParticipant, attributes: ['id', 'status'], include: [{ model: Member, attributes: ['id', 'name'] }] } ], order: [ ['gameNumber', 'ASC'], ['score', 'DESC'] ] }); return res.status(200).json(scores); } catch (error) { console.error('점수 조회 오류:', error); res.status(500).json({ message: '점수 조회 중 오류가 발생했습니다.' }); } }; // 점수 등록 exports.addEventScore = async (req, res) => { try { const eventId = req.params.id; const { participantId, gameNumber, score, handicap = 0 } = req.body; if (!eventId || !participantId || !gameNumber || score === undefined) { return res.status(400).json({ message: '이벤트 ID, 참가자 ID, 게임 번호, 점수가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 참가자 확인 const participant = await EventParticipant.findOne({ where: { id: participantId, eventId, status: { [Op.ne]: 'canceled' } } }); if (!participant) { return res.status(400).json({ message: '이벤트 참가자가 아닙니다.' }); } // 게임 번호 유효성 검사 if (gameNumber < 1 || gameNumber > event.gameCount) { return res.status(400).json({ message: `게임 번호는 1부터 ${event.gameCount}까지만 입력 가능합니다.` }); } // 점수 유효성 검사 if (score < 0 || score > 300) { return res.status(400).json({ message: '점수는 0에서 300 사이여야 합니다.' }); } // 동일한 게임의 점수가 이미 있는지 확인 const existingScore = await EventScore.findOne({ where: { eventId, participantId, gameNumber } }); if (existingScore) { return res.status(400).json({ message: '해당 게임의 점수가 이미 등록되어 있습니다.' }); } // 점수 등록 const totalScore = score + handicap; const newScore = await EventScore.create({ eventId, participantId, gameNumber, score, handicap, totalScore }); // 참가자 평균 점수 업데이트 const allScores = await EventScore.findAll({ where: { eventId, participantId } }); const totalGames = allScores.length; const averageScore = Math.round(allScores.reduce((sum, s) => sum + s.score, 0) / totalGames); await participant.update({ games: totalGames, average: averageScore }); // 점수 등록 후 에버리지 캐시 무효화 await averageService.invalidateMemberAverageCache(participant.memberId); res.status(201).json({ message: '점수가 등록되었습니다.', score: { id: newScore.id, eventId: newScore.eventId, participantId: newScore.participantId, gameNumber: newScore.gameNumber, score: newScore.score, handicap: newScore.handicap, totalScore: newScore.totalScore, createdAt: newScore.createdAt } }); } catch (error) { console.error('점수 등록 오류:', error); res.status(500).json({ message: '점수 등록 중 오류가 발생했습니다.' }); } }; // 점수 수정 exports.updateEventScore = async (req, res) => { try { const eventId = Number(req.params.id); const scoreId = Number(req.params.scoreId); const { gameNumber, score, handicap = 0 } = req.body; if (!eventId || !scoreId || !gameNumber || score === undefined || score === null) { return res.status(400).json({ message: '이벤트 ID, 점수 ID, 게임 번호, 점수가 필요합니다.', received: { eventId, scoreId, gameNumber, score } }); } // 점수 레코드 확인 const scoreRecord = await EventScore.findOne({ where: { id: scoreId, eventId } }); // 점수 레코드가 없으면 참가자 ID를 가져와서 새로 생성 if (!scoreRecord) { // 기존 점수에서 참가자 ID 가져오기 const existingScore = await EventScore.findOne({ where: { eventId }, order: [['createdAt', 'DESC']] }); if (!existingScore) { return res.status(404).json({ message: '참가자 정보를 찾을 수 없습니다.' }); } // 새로운 게임 점수 생성 const newScore = await EventScore.create({ eventId, participantId: existingScore.participantId, gameNumber, score, handicap, totalScore: score + handicap }); return res.json({ message: '새로운 게임 점수가 등록되었습니다.', score: newScore }); } // 기존 레코드 업데이트 await scoreRecord.update({ gameNumber, score, handicap, totalScore: score + handicap }); // 캐시 무효화 추가 await averageService.invalidateMemberAverageCache(scoreRecord.participantId); // 업데이트된 레코드 조회 const updatedScore = await EventScore.findOne({ where: { id: scoreId, eventId } }); res.json({ message: '점수가 수정되었습니다.', score: updatedScore }); } catch (error) { console.error('점수 수정 오류:', error, JSON.stringify(error, Object.getOwnPropertyNames(error))); res.status(500).json({ message: '점수 수정 중 오류가 발생했습니다.', error: error.message || error.toString() || error }); } }; // 점수 삭제 exports.deleteEventScore = async (req, res) => { try { const { eventId, clubId, scoreId } = req.body; if (!eventId || !clubId || !scoreId) { return res.status(400).json({ message: '이벤트 ID, 클럽 ID, 점수 ID가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 점수 존재 여부 확인 const score = await EventScore.findOne({ where: { id: scoreId, eventId }, include: [ { model: EventParticipant, attributes: ['id', 'status'], include: [{ model: Member, attributes: ['id', 'name'] }] } ] }); if (!score) { return res.status(404).json({ message: '점수를 찾을 수 없습니다.' }); } await score.destroy(); // 점수 삭제 시 해당 회원의 에버리지 캡시 무효화 if (score.participant && score.participant.Member) { try { await averageService.invalidateMemberAverageCache(score.participant.Member.id); console.log(`회원 ${score.participant.Member.id}의 에버리지 캡시 무효화 완료`); } catch (error) { console.error(`회원 ${score.participant.Member.id}의 에버리지 캡시 무효화 오류:`, error); } } res.json({ message: '점수가 삭제되었습니다.', score: { id: score.id, userId: score.userId, userName: score.participant.Member ? score.participant.Member.name : '알 수 없음', gameNumber: score.gameNumber, frameNumber: score.frameNumber } }); } catch (error) { console.error('점수 삭제 오류:', error); res.status(500).json({ message: '점수 삭제 중 오류가 발생했습니다.' }); } }; // 참가자의 모든 점수 삭제 exports.deleteParticipantScores = async (req, res) => { try { const eventId = Number(req.params.id); const participantId = Number(req.params.participantId); if (!eventId || !participantId) { return res.status(400).json({ message: '이벤트 ID와 참가자 ID가 필요합니다.' }); } // 점수 삭제 await EventScore.destroy({ where: { eventId, participantId } }); res.json({ message: '점수가 삭제되었습니다.' }); } catch (error) { console.error('점수 삭제 오류:', error); res.status(500).json({ message: '점수 삭제 중 오류가 발생했습니다.' }); } }; // 이벤트 통계 조회 exports.getEventStats = async (req, res) => { try { const eventId = parseInt(req.params.id); const { clubId } = req.query; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const stats = await EventScore.findAll({ where: { eventId }, attributes: [ 'userId', [sequelize.fn('AVG', sequelize.col('score')), 'averageScore'], [sequelize.fn('MAX', sequelize.col('score')), 'highScore'], [sequelize.fn('COUNT', sequelize.literal('CASE WHEN "isStrike" = true THEN 1 END')), 'strikeCount'], [sequelize.fn('COUNT', sequelize.literal('CASE WHEN "isSpare" = true THEN 1 END')), 'spareCount'] ], include: [{ model: EventParticipant, attributes: [], include: [{ model: Member, attributes: ['name'] }] }], group: ['userId', 'participant.id', 'participant.Member.id', 'participant.Member.name'] }); res.json(stats); } catch (error) { console.error('통계 조회 오류:', error); res.status(500).json({ message: '통계 조회 중 오류가 발생했습니다.' }); } }; // 캘린더용 이벤트 조회 exports.getCalendarEvents = async (req, res) => { try { const { clubId } = req.query; const { start, end } = req.query; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const where = { clubId, status: { [Op.ne]: 'deleted' } }; if (start && end) { where.startDate = { [Op.between]: [new Date(start), new Date(end)] }; } const events = await Event.findAll({ where, attributes: [ 'id', 'title', 'description', 'startDate', 'endDate', 'location', 'eventType', 'status' ], order: [['startDate', 'ASC']] }); const calendarEvents = events.map(event => ({ id: event.id, title: event.title, start: event.startDate, end: event.endDate, description: event.description, location: event.location, eventType: event.eventType, status: event.status })); res.json(calendarEvents); } catch (error) { console.error('캘린더 이벤트 조회 오류:', error); res.status(500).json({ message: '캘린더 이벤트 조회 중 오류가 발생했습니다.' }); } }; // 이벤트 참가 신청 exports.registerForEvent = async (req, res) => { try { const eventId = parseInt(req.params.id); const { clubId } = req.query; const { userId, teamNumber } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 이미 등록된 참가자인지 확인 const existingParticipant = await EventParticipant.findOne({ where: { eventId, userId, status: { [Op.ne]: 'canceled' } } }); if (existingParticipant) { return res.status(400).json({ message: '이미 등록된 참가자입니다.' }); } const participant = await EventParticipant.create({ eventId, userId, teamNumber, status: 'registered', registeredAt: new Date() }); res.status(201).json(participant); } catch (error) { console.error('참가 신청 오류:', error); res.status(500).json({ message: '참가 신청 중 오류가 발생했습니다.' }); } }; // 참가자 제거 exports.removeParticipant = async (req, res) => { try { const { id: eventId, userId } = req.params; const { clubId } = req.query; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const participant = await EventParticipant.findOne({ where: { eventId, userId, status: { [Op.ne]: 'canceled' } }, include: [{ model: Event, where: { clubId, status: { [Op.ne]: 'deleted' } } }] }); if (!participant) { return res.status(404).json({ message: '참가자를 찾을 수 없습니다.' }); } await participant.update({ status: '취소' }); res.json({ message: '참가자가 제거되었습니다.' }); } catch (error) { console.error('참가자 제거 오류:', error); res.status(500).json({ message: '참가자 제거 중 오류가 발생했습니다.' }); } }; // 참가자 상태 업데이트 exports.updateParticipantStatus = async (req, res) => { try { const { id: eventId, userId } = req.params; const { clubId } = req.query; const { status, attendance, paymentStatus, paymentAmount } = req.body; if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } const participant = await EventParticipant.findOne({ where: { eventId, userId, status: { [Op.ne]: 'canceled' } }, include: [{ model: Event, where: { clubId, status: { [Op.ne]: 'deleted' } } }] }); if (!participant) { return res.status(404).json({ message: '참가자를 찾을 수 없습니다.' }); } await participant.update({ status, attendance, paymentStatus, paymentAmount }); res.json(participant); } catch (error) { console.error('참가자 상태 업데이트 오류:', error); res.status(500).json({ message: '참가자 상태 업데이트 중 오류가 발생했습니다.' }); } }; // 클럽별 이벤트 조회 exports.getEventsByClub = async (req, res) => { try { const clubId = parseInt(req.params.clubId); const events = await Event.findAll({ where: { clubId, status: 'active' }, order: [['startDate', 'ASC']] }); res.json(events); } catch (error) { console.error('클럽별 이벤트 조회 오류:', error); res.status(500).json({ message: '클럽별 이벤트 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 사용자별 참가 이벤트 조회 exports.getUserEvents = async (req, res) => { try { const userId = req.user.id; const events = await Event.findAll({ include: [ { model: EventParticipant, where: { userId }, attributes: [] }, { model: Club, attributes: ['id', 'name'] } ], order: [['startDate', 'ASC']] }); res.json(events); } catch (error) { console.error('사용자별 이벤트 조회 오류:', error); res.status(500).json({ message: '사용자별 이벤트 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 이벤트 참가자 목록 조회 exports.getEventParticipants = async (req, res) => { try { const { eventId, clubId } = req.body; if (!eventId || !clubId) { return res.status(400).json({ message: '이벤트 ID와 클럽 ID가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } const participants = await EventParticipant.findAll({ where: { eventId, status: { [Op.ne]: 'canceled' } }, include: [ { model: Member, attributes: ['id', 'name', 'email', 'phone', 'memberType', 'gender'] } ], order: [ ['createdAt', 'ASC'] ] }); // 참가자들의 동적 에버리지 계산 const participantsWithAverage = await Promise.all( participants.map(async (participant) => { const participantData = participant.toJSON(); if (participantData.Member && participantData.Member.id) { try { const averageData = await averageService.calculateMemberAverageByClubSetting( participantData.Member.id, clubId ); return { ...participantData, Member: { ...participantData.Member, average: averageData.average } }; } catch (error) { console.error(`회원 ${participantData.Member.id}의 에버리지 계산 오류:`, error); return { ...participantData, Member: { ...participantData.Member, average: 0 } }; } } return participantData; }) ); res.json({ message: '참가자 목록 조회가 완료되었습니다.', participants: participantsWithAverage }); } catch (error) { console.error('참가자 목록 조회 오류:', error); res.status(500).json({ message: '참가자 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // 참가자 정보 핵심 update 함수 (중복 제거) async function updateEventParticipantCore({ id, eventId, memberId, clubId, status, paymentStatus, comment }) { const [updatedCount] = await EventParticipant.update( { status, paymentStatus, comment }, { where: { id, eventId, memberId } } ); return updatedCount; } // 이벤트 참가자 등록 (이미 있으면 update, 없으면 create) exports.addEventParticipant = async (req, res) => { try { const { clubId, memberId, status, paymentStatus, comment } = req.body; const eventId = req.params.id; if (!eventId || !clubId || !memberId) { return res.status(400).json({ message: '이벤트 ID, 클럽 ID, 사용자 ID가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 클럽 회원 확인 const member = await Member.findOne({ where: { id: memberId, clubId, status: 'active' } }); if (!member) { return res.status(400).json({ message: '클럽 회원이 아닙니다.' }); } // 이미 참가 신청했는지 확인 const existingParticipant = await EventParticipant.findOne({ where: { eventId, memberId, status: { [Op.ne]: 'canceled' } } }); let participant; let isUpdate = false; if (existingParticipant) { // 이미 있으면 update (핵심 update 함수 활용) await updateEventParticipantCore({ id: existingParticipant.id, eventId, memberId, clubId, status: status || existingParticipant.status, paymentStatus: paymentStatus || existingParticipant.paymentStatus, comment: comment || existingParticipant.comment }); participant = await EventParticipant.findOne({ where: { id: existingParticipant.id }, include: [ { model: Member, attributes: ['id', 'name'] } ] }); isUpdate = true; } else { // 없으면 create participant = await EventParticipant.create({ eventId, memberId, status: status || '참가예정', paymentStatus: paymentStatus || '미납', comment: comment || null, createdAt: new Date(), updatedAt: new Date() }); participant = await EventParticipant.findOne({ where: { id: participant.id }, include: [ { model: Member, attributes: ['id', 'name'] } ] }); } res.status(201).json({ message: isUpdate ? '기존 참가자 정보를 수정했습니다.' : '참가자가 등록되었습니다.', participant }); } catch (error) { console.error('참가자 등록/수정 오류:', error); res.status(500).json({ message: '참가자 등록/수정 중 오류가 발생했습니다.' }); } }; // 참가자 정보 수정 exports.updateEventParticipant = async (req, res) => { try { const eventId = req.params.id; const { id, clubId, memberId, status, paymentStatus, comment } = req.body; if (!eventId || !clubId || !memberId || !id) { return res.status(400).json({ message: '이벤트 ID, 클럽 ID, 사용자 ID가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, clubId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 참가자 정보 수정 const [updatedCount] = await EventParticipant.update( { status, paymentStatus, comment }, { where: { id, eventId, memberId } } ); if (updatedCount === 0) { return res.status(404).json({ message: '수정할 참가자 정보를 찾을 수 없습니다.' }); } // 수정된 참가자 정보 조회 const updatedParticipant = await EventParticipant.findOne({ where: { id }, include: [ { model: Member, attributes: ['id', 'name', 'email', 'phone', 'memberType'] } ] }); res.json({ message: '참가자 정보가 수정되었습니다.', participant: { ...updatedParticipant.toJSON(), userName: updatedParticipant.Member ? updatedParticipant.Member.name : '알 수 없음', userEmail: updatedParticipant.Member ? updatedParticipant.Member.email : '', userPhone: updatedParticipant.Member ? updatedParticipant.Member.phone : '', userType: updatedParticipant.Member ? updatedParticipant.Member.memberType : '' } }); } catch (error) { console.error('참가자 정보 수정 오류:', error); res.status(500).json({ message: '참가자 정보 수정 중 오류가 발생했습니다.' }); } }; // [팀 생성 기능] 이벤트별 팀 목록 조회 exports.getEventTeams = async (req, res) => { try { const eventId = req.params.id; if (!eventId) { return res.status(400).json({ message: '이벤트 ID가 필요합니다.' }); } const teams = await EventTeam.findAll({ where: { eventId }, include: [ { model: EventTeamMember, include: [ { model: EventParticipant, include: [{ model: Member }] } ] } ], order: [['teamNumber', 'ASC']] }); const result = teams.map(team => ({ id: team.id, teamNumber: team.teamNumber, handicap: team.handicap, members: (team.EventTeamMembers || []) .sort((a, b) => a.order - b.order) .map(tm => ({ id: tm.eventParticipantId, name: tm.EventParticipant?.Member?.name || `참가자 ${tm.eventParticipantId}`, order: tm.order })) })); res.json({ teams: result }); } catch (error) { console.error('팀 목록 조회 오류:', error); res.status(500).json({ message: '팀 목록을 가져오는 중 오류가 발생했습니다.' }); } }; // [팀 생성 기능] 이벤트별 팀 배정 저장 exports.createOrUpdateEventTeams = async (req, res) => { const eventId = req.params.id; const { teams } = req.body; // [{teamNumber, handicap, members:[id]}] if (!eventId || !Array.isArray(teams)) { return res.status(400).json({ message: '이벤트 ID와 팀 정보가 필요합니다.' }); } try { await sequelize.transaction(async (t) => { // 기존 팀 및 팀멤버 데이터 삭제 await EventTeam.destroy({ where: { eventId }, transaction: t }); await EventTeamMember.destroy({ where: {}, // eventId로만 삭제하려면, eventTeamId로 팀 id 목록을 먼저 조회해서 삭제 transaction: t }); // 새 팀 및 팀멤버 데이터 저장 for (const team of teams) { // 팀 생성 const createdTeam = await EventTeam.create({ eventId, teamNumber: team.teamNumber, handicap: team.handicap || 0 }, { transaction: t }); // 팀 멤버(순서 포함) 저장 if (Array.isArray(team.members) && team.members.length > 0) { const teamMemberRows = team.members .filter(m => m.id != null) .map(m => ({ eventTeamId: createdTeam.id, eventParticipantId: m.id, order: m.order ?? 1 })); if (teamMemberRows.length > 0) { await EventTeamMember.bulkCreate(teamMemberRows, { transaction: t }); } } } }); res.json({ message: '팀 배정이 저장되었습니다.' }); } catch (error) { console.error('팀 배정 저장 오류:', error); res.status(500).json({ message: '팀 배정 저장 중 오류가 발생했습니다.' }); } }; // 참가자 삭제 (소프트 삭제) exports.deleteEventParticipant = async (req, res) => { try { const eventId = req.params.id; const { id } = req.body; if (!eventId || !id) { return res.status(400).json({ message: '이벤트 ID, 참가자 ID가 필요합니다.' }); } // 이벤트 존재 여부 확인 const event = await Event.findOne({ where: { id: eventId, status: { [Op.ne]: 'deleted' } } }); if (!event) { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } // 참가자 존재 여부 확인 const participant = await EventParticipant.findOne({ where: { eventId, id, status: { [Op.ne]: 'canceled' } }, include: [ { model: Member, attributes: ['id', 'name', 'memberType'] } ] }); if (!participant) { return res.status(404).json({ message: '참가자를 찾을 수 없습니다.' }); } // 참가자의 점수도 함께 삭제 await EventScore.destroy({ where: { eventId, participantId: id } }); await participant.update({ status: '취소' }); res.json({ message: '참가자가 삭제되었습니다.', participant: { id: participant.id } }); } catch (error) { console.error('참가자 삭제 오류:', error); res.status(500).json({ message: '참가자 삭제 중 오류가 발생했습니다.' }); } };