public 기능 완료

This commit is contained in:
2025-05-07 21:02:50 +09:00
parent 283c8a7f4a
commit 1684fbf5d1
28 changed files with 2522 additions and 194 deletions
+304
View File
@@ -0,0 +1,304 @@
// publicController.js
// 공개 이벤트 관련 API만 담당 (비밀번호 인증, 점수입력 등)
const { Event, EventParticipant, Member, Club, EventScore, ClubMember, EventTeam, EventTeamMember, sequelize } = require('../models');
const { Op } = require('sequelize');
const { signPublicEventToken, verifyPublicEventToken } = require('../utils/publicJwt');
// 공개 이벤트 최초 진입 (토큰 또는 비밀번호 인증)
exports.publicEventEntry = async (req, res) => {
const { publicHash } = req.params;
const { password } = req.body || {};
const auth = req.headers.authorization;
let event;
try {
event = await Event.findOne({
where: { publicHash },
include: [{ model: Club, attributes: ['id', 'name'] }]
});
if (!event) {
return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' });
}
if (event.status === '취소') {
return res.status(403).json({ message: '취소된 이벤트입니다.' });
}
let token = null;
let isAuthenticated = false;
// 1. 토큰 인증 우선
if (auth && auth.startsWith('Bearer ')) {
try {
const payload = verifyPublicEventToken(auth.slice(7));
if (payload.publicHash === event.publicHash) {
isAuthenticated = true;
}
} catch (e) {
// 토큰 인증 실패: isAuthenticated는 false
}
}
// 2. 토큰 인증 실패 시
if (!isAuthenticated) {
// 비밀번호가 필요한 이벤트
if (event.accessPassword) {
if (!password) {
return res.status(401).json({ message: '비밀번호가 필요합니다.' });
}
if (password !== event.accessPassword) {
return res.status(401).json({ message: '비밀번호가 일치하지 않습니다.' });
}
}
// 비밀번호가 없거나(공개 이벤트) or 비밀번호 인증 성공
isAuthenticated = true;
token = signPublicEventToken({ publicHash: event.publicHash });
}
// 3. 인증 성공 시 데이터 반환
if (isAuthenticated) {
const data = await exports.getEventByPublicHash(event, token);
return res.json(data);
}
// 4. 나머지는 모두 인증 실패
return res.status(401).json({ message: '인증이 필요합니다.' });
} catch (e) {
console.error('[publicEventEntry] 공개 이벤트 최초 진입 오류', { publicHash, error: e });
res.status(500).json({ message: '이벤트 정보를 가져오는 중 오류가 발생했습니다.' });
}
};
// 공개 이벤트 조회 (내부 데이터 반환 전용)
exports.getEventByPublicHash = async (event, token = null) => {
try {
const allMembers = await Member.findAll({
where: { clubId: event.clubId },
attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'average']
});
const participants = await EventParticipant.findAll({
where: { eventId: event.id, status: { [Op.ne]: '취소' } },
include: [ { model: Member, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'average'] } ]
});
const allMemberObjs = allMembers.map(m => m.dataValues);
const participantMemberIds = [
...new Set(participants.filter(p => p.dataValues.status !== '취소').map(p => parseInt(p.dataValues.memberId)))
];
// 상태별로 미참가자/게스트 추가 제한
let availableMembers = [];
if (event.status === '준비') {
availableMembers = allMemberObjs.filter(m => !participantMemberIds.includes(parseInt(m.memberId)));
} // 준비 아닐 땐 빈 배열(신청/게스트 추가 불가)
// 팀 정보
const teams = await EventTeam.findAll({
where: { eventId: event.id },
order: [['teamNumber', 'ASC']],
include: [{
model: EventTeamMember,
include: [{
model: EventParticipant,
where: { status: { [Op.ne]: '취소' } },
include: [{ model: Member, attributes: [[sequelize.col('id'), 'memberId'], 'name', 'memberType', 'gender', 'handicap', 'average'] }]
}]
}]
});
const scores = await EventScore.findAll({ where: { eventId: event.id } });
// 참가자 id → 참가자 객체 Map
const participantMap = new Map();
participants.forEach(p => participantMap.set(p.id, p));
// 팀에 소속된 참가자 id Set
const assignedParticipantIds = new Set();
const teamList = teams.map(team => ({
teamId: team.id,
teamNumber: team.teamNumber,
handicap: team.handicap,
members: (team.EventTeamMembers || []).map(tm => {
const participant = tm.EventParticipant;
if (participant?.id) assignedParticipantIds.add(participant.id);
const member = participant?.Member;
// 각 게임별 점수 배열 생성
const scoresArr = Array(event.gameCount).fill(null);
scores.filter(s => s.participantId === participant?.id).forEach(s => {
if (s.gameNumber && s.gameNumber >= 1 && s.gameNumber <= event.gameCount) {
scoresArr[s.gameNumber - 1] = s.score;
}
});
return {
participantId: participant?.id,
memberId: member?.memberId,
name: member?.name || `참가자 ${participant?.id}`,
gender: member?.gender,
memberType: member?.memberType,
average: member?.average || 0,
handicap: member?.handicap || 0,
status: participant?.status,
paymentStatus: participant?.paymentStatus,
comment: participant?.comment,
isGuest: member?.memberType === '게스트',
scores: scoresArr
};
})
}));
// 팀에 소속되지 않은 참가자(미배정)
const unassignedMembers = participants
.filter(p => !assignedParticipantIds.has(p.id))
.map(p => {
const member = p.Member;
// 각 게임별 점수 배열 생성
const scoresArr = Array(event.gameCount).fill(null);
scores.filter(s => s.participantId === p.id).forEach(s => {
if (s.gameNumber && s.gameNumber >= 1 && s.gameNumber <= event.gameCount) {
scoresArr[s.gameNumber - 1] = s.score;
}
});
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,
status: p.status,
paymentStatus: p.paymentStatus,
comment: p.comment,
isGuest: member?.memberType === '게스트',
scores: scoresArr
};
});
let participantList = participants.map(p => {
const member = p.Member;
// 각 게임별 점수 배열 생성
const scoresArr = Array(event.gameCount).fill(null);
scores.filter(s => s.participantId === p.id).forEach(s => {
if (s.gameNumber && s.gameNumber >= 1 && s.gameNumber <= event.gameCount) {
scoresArr[s.gameNumber - 1] = s.score;
}
});
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,
status: p.status,
paymentStatus: p.paymentStatus,
comment: p.comment,
isGuest: member?.memberType === '게스트',
scores: scoresArr
};
});
return {
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,
unassignedMembers,
availableMembers,
needPassword: !!event.accessPassword,
...(token ? { token } : {})
};
} catch (e) {
console.error('공개 이벤트 조회 오류:', e);
throw new Error('이벤트 정보를 가져오는 중 오류가 발생했습니다.');
}
};
// 공개 참가자 등록/수정
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 } });
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, phone, gender, average } = 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: '취소된 이벤트입니다.' });
// publicAuth 미들웨어로 인증됨
const member = await Member.create({
name,
phone,
gender,
clubId: event.clubId,
memberType: '게스트',
average: average || 0
});
const participant = await EventParticipant.create({ eventId: event.id, memberId: member.id });
res.json({
message: '게스트가 추가되었습니다.',
data: {
memberId: member.id,
participantId: participant.id,
name: member.name,
phone: member.phone,
gender: member.gender,
memberType: member.memberType,
handicap: member.handicap,
average: member.average,
group: '미신청'
}
});
} catch (e) {
console.error('공개 게스트 추가 오류:', e);
res.status(500).json({ message: '게스트 추가 중 오류가 발생했습니다.' });
}
};
// 공개 점수 수정
exports.updatePublicScore = async (req, res) => {
const { publicHash } = req.params;
let { participantId, gameNumber, score, handicap, teamNumber } = req.body || {};
score = Number(score);
handicap = Number(handicap) || 0; // undefined/null이면 0
teamNumber = teamNumber ?? null;
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: '점수 수정이 불가능한 상태입니다.' });
// publicAuth 미들웨어로 인증됨
let eventScore = await EventScore.findOne({ where: { eventId: event.id, participantId, gameNumber } });
if (!eventScore) {
eventScore = await EventScore.create({ eventId: event.id, participantId, gameNumber, teamNumber, score, handicap, totalScore: score + handicap });
} else {
await eventScore.update({ teamNumber, score, handicap, totalScore: score + handicap });
}
res.json({ message: '점수가 저장되었습니다.' });
} catch (e) {
console.error('공개 점수 수정 오류:', e);
res.status(500).json({ message: '점수 저장 중 오류가 발생했습니다.' });
}
};