diff --git a/backend/controllers/publicController.js b/backend/controllers/publicController.js index 1bbb9ae..94dddbd 100644 --- a/backend/controllers/publicController.js +++ b/backend/controllers/publicController.js @@ -280,18 +280,57 @@ exports.registerPublicParticipant = async (req, res) => { 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 (event.status === 'canceled') return res.status(403).json({ message: '취소된 이벤트입니다.' }); + + // 입력값 정규화 및 검증 + const toInt = (v) => { + if (v === undefined || v === null || v === '') return null; + const n = Number(v); + return Number.isFinite(n) ? n : null; + }; + const pid = toInt(participantId); + const mid = toInt(memberId); + if (!mid) { + return res.status(400).json({ message: '유효한 회원 ID가 필요합니다.' }); + } + + // 상태/납부 검증 (프런트가 백엔드 값에 맞춰 보냄) + const allowedStatus = ['pending', 'confirmed', 'canceled']; + const allowedPayment = ['unpaid', 'paid']; + const normStatus = status || 'pending'; + const normPayment = paymentStatus || 'unpaid'; + if (!allowedStatus.includes(normStatus)) { + return res.status(400).json({ message: `유효하지 않은 참가 상태입니다. 허용값: ${allowedStatus.join(', ')}` }); + } + if (!allowedPayment.includes(normPayment)) { + return res.status(400).json({ message: `유효하지 않은 납부 상태입니다. 허용값: ${allowedPayment.join(', ')}` }); + } + + // 멤버 소속 검증 (이벤트 클럽 소속) + const member = await Member.findOne({ where: { id: mid, clubId: event.clubId } }); + if (!member) { + return res.status(404).json({ message: '클럽 소속 회원을 찾을 수 없습니다.' }); + } + + let participant = null; + if (pid) { + participant = await EventParticipant.findOne({ where: { id: pid, memberId: member.id } }); + } if (!participant) { - participant = await EventParticipant.create({ eventId: event.id, memberId: member.id, comment, status, paymentStatus }); + // 중복 참가 방지: 동일 eventId/memberId의 취소되지 않은 레코드가 있으면 업데이트로 처리 + const existing = await EventParticipant.findOne({ where: { eventId: event.id, memberId: member.id, status: { [Op.ne]: 'canceled' } } }); + if (existing) { + await existing.update({ comment: comment ?? existing.comment, status: normStatus || existing.status, paymentStatus: normPayment || existing.paymentStatus }); + } else { + await EventParticipant.create({ eventId: event.id, memberId: member.id, comment: comment || null, status: normStatus, paymentStatus: normPayment }); + } } else { - await participant.update({ comment, status, paymentStatus }); + await participant.update({ comment, status: normStatus, paymentStatus: normPayment }); } res.json({ message: '참가 신청/수정이 완료되었습니다.' }); } catch (e) { - console.error('공개 참가자 등록 오류:', e); - res.status(500).json({ message: '참가 신청/수정 중 오류가 발생했습니다.' }); + console.error('공개 참가자 등록 오류:', { error: e?.message, stack: e?.stack }); + res.status(500).json({ message: '참가 신청/수정 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.' }); } };