diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js index cd28c2c..45ea92f 100644 --- a/backend/controllers/eventController.js +++ b/backend/controllers/eventController.js @@ -687,7 +687,7 @@ exports.updateEvent = async (req, res) => { } }; -// 이벤트 삭제 (소프트 삭제) +// 이벤트 삭제 (소프트 삭제: 숨김 처리) exports.deleteEvent = async (req, res) => { try { const eventId = req.params.id; @@ -716,9 +716,9 @@ exports.deleteEvent = async (req, res) => { return res.status(404).json({ message: '이벤트를 찾을 수 없습니다.' }); } - // 참가자 정보도 함께 삭제 처리 + // 참가자 소프트 삭제(취소 상태로 표시) await EventParticipant.update( - { status: '취소' }, + { status: 'canceled' }, { where: { eventId, @@ -727,10 +727,11 @@ exports.deleteEvent = async (req, res) => { } ); - await event.update({ status: '삭제' }); + // 이벤트 숨김 처리(표준 상태값 사용) + await event.update({ status: 'deleted' }); res.json({ - message: '이벤트가 삭제되었습니다.', + message: '이벤트가 숨김(소프트 삭제)되었습니다.', event: { id: event.id, title: event.title, @@ -744,6 +745,50 @@ exports.deleteEvent = async (req, res) => { } }; +// 이벤트 완전 삭제 (하드 삭제): 팀배정, 점수, 참가자, 팀, 이벤트 순서로 물리 삭제 +exports.deleteEventPermanently = async (req, res) => { + try { + const eventId = Number(req.params.id); + const { clubId } = req.body || {}; + const cid = Number(clubId); + + if (!eventId || !cid) { + return res.status(400).json({ message: '이벤트 ID와 클럽 ID가 필요합니다.' }); + } + + await sequelize.transaction(async (t) => { + const event = await Event.findOne({ where: { id: eventId, clubId: cid }, transaction: t }); + if (!event) { + throw new Error('이벤트를 찾을 수 없습니다.'); + } + + // 팀멤버 삭제 + const teams = await EventTeam.findAll({ where: { eventId }, transaction: t }); + const teamIds = teams.map((tm) => tm.id); + if (teamIds.length > 0) { + await EventTeamMember.destroy({ where: { eventTeamId: teamIds }, transaction: t }); + } + + // 점수 삭제 + await EventScore.destroy({ where: { eventId }, transaction: t }); + + // 참가자 삭제 + await EventParticipant.destroy({ where: { eventId }, transaction: t }); + + // 팀 삭제 + await EventTeam.destroy({ where: { eventId }, transaction: t }); + + // 이벤트 삭제 + await Event.destroy({ where: { id: eventId, clubId: cid }, transaction: t }); + }); + + res.json({ message: '이벤트 및 관련 데이터가 완전히 삭제되었습니다.' }); + } catch (error) { + console.error('이벤트 완전 삭제 오류:', error); + res.status(500).json({ message: '이벤트 완전 삭제 중 오류가 발생했습니다.' }); + } +}; + // 이벤트 점수 조회 exports.getEventScores = async (req, res) => { try { diff --git a/backend/controllers/fileUploadController.js b/backend/controllers/fileUploadController.js index dbc53b9..af4a151 100644 --- a/backend/controllers/fileUploadController.js +++ b/backend/controllers/fileUploadController.js @@ -50,6 +50,79 @@ const fileFilter = (req, file, cb) => { } }; +// 업로드 저장 롤백: 이벤트 및 관련 데이터 삭제 (옵션: 게스트 정리) +exports.rollbackEventImport = async (req, res) => { + try { + const { eventId, clubId, includeGuests } = req.body || {}; + const eid = Number(eventId); + const cid = Number(clubId); + if (!eid || !cid) { + return res.status(400).json({ message: '유효한 eventId, clubId가 필요합니다.' }); + } + + await sequelize.transaction(async (t) => { + // 이벤트 검증 (동일 클럽) + const event = await Event.findOne({ where: { id: eid, clubId: cid }, transaction: t }); + if (!event) { + throw new Error('대상 이벤트를 찾을 수 없습니다.'); + } + + // 관련 팀멤버 삭제 + const teams = await EventTeam.findAll({ where: { eventId: eid }, transaction: t }); + const teamIds = teams.map((tm) => tm.id); + if (teamIds.length > 0) { + await EventTeamMember.destroy({ where: { eventTeamId: teamIds }, transaction: t }); + } + + // 점수 삭제 + await EventScore.destroy({ where: { eventId: eid }, transaction: t }); + + // 참가자 삭제 전, 게스트 후보 수집 + let guestMemberIds = []; + if (includeGuests) { + const participants = await EventParticipant.findAll({ where: { eventId: eid }, transaction: t }); + const memberIds = participants.map((p) => p.memberId).filter(Boolean); + if (memberIds.length > 0) { + const members = await Member.findAll({ where: { id: memberIds, clubId: cid, memberType: 'guest' }, transaction: t }); + guestMemberIds = members.map((m) => m.id); + } + } + + // 참가자 삭제 + await EventParticipant.destroy({ where: { eventId: eid }, transaction: t }); + + // 팀 삭제 + await EventTeam.destroy({ where: { eventId: eid }, transaction: t }); + + // 이벤트 삭제 + await Event.destroy({ where: { id: eid, clubId: cid }, transaction: t }); + + // 게스트 정리: 다른 참가 기록이 없고, 동일 클럽의 조인만 있는 경우 제거 + if (includeGuests && guestMemberIds.length > 0) { + // 다른 참가 기록이 없는 멤버만 필터 + const others = await EventParticipant.findAll({ where: { memberId: guestMemberIds }, transaction: t }); + const stillUsed = new Set(others.map((p) => p.memberId)); + const deletable = guestMemberIds.filter((id) => !stillUsed.has(id)); + if (deletable.length > 0) { + // 클럽 조인 제거 + const club = await Club.findOne({ where: { id: cid }, transaction: t }); + if (club) { + // removeMember 지원 여부에 따라 destroy 직접 수행 + await sequelize.models.ClubMembers?.destroy?.({ where: { clubId: cid, memberId: deletable }, transaction: t }).catch(() => {}); + } + // 멤버 삭제 + await Member.destroy({ where: { id: deletable, clubId: cid, memberType: 'guest' }, transaction: t }); + } + } + }); + + res.json({ message: '롤백이 완료되었습니다.' }); + } catch (error) { + console.error('롤백 오류:', error); + res.status(500).json({ message: `롤백 중 오류가 발생했습니다: ${error.message}` }); + } +}; + const upload = multer({ storage: storage, fileFilter: fileFilter, @@ -531,6 +604,12 @@ exports.saveEventFileData = async (req, res) => { try { // 트랜잭션 시작 const result = await sequelize.transaction(async (t) => { + // 프런트에서 전달할 수 있는 핸디 포함 여부 플래그 (기본: true) + const scoresIncludeHandicap = ( + (typeof req.body.scoresIncludeHandicap === 'boolean') ? req.body.scoresIncludeHandicap + : (eventData && typeof eventData.scoresIncludeHandicap === 'boolean') ? eventData.scoresIncludeHandicap + : true + ); // 이벤트 타입/상태 정규화 (모델 enum 호환) const normalizeEventType = (v) => { if (!v) return 'regular'; @@ -569,13 +648,14 @@ exports.saveEventFileData = async (req, res) => { await event.update({ title: eventData.title, description: eventData.description || '', - eventType: normalizeEventType(eventData.eventType), + // 비즈니스 규칙: 이벤트 유형은 정기전, 상태는 완료로 고정 + eventType: 'regular', startDate: new Date(eventData.startDate), endDate: eventData.endDate ? new Date(eventData.endDate) : null, location: eventData.location || '', gameCount: eventData.gameCount || 3, maxParticipants: eventData.maxParticipants || 0, - status: normalizeStatus(eventData.status) + status: 'completed' }, { transaction: t }); } else { // 새 이벤트 생성 @@ -583,13 +663,15 @@ exports.saveEventFileData = async (req, res) => { clubId: cid, title: eventData.title, description: eventData.description || '', - eventType: normalizeEventType(eventData.eventType), + // 비즈니스 규칙: 이벤트 유형은 정기전으로 저장 + eventType: 'regular', startDate: new Date(eventData.startDate), endDate: eventData.endDate ? new Date(eventData.endDate) : null, location: eventData.location || '', gameCount: eventData.gameCount || 3, maxParticipants: eventData.maxParticipants || 0, - status: normalizeStatus(eventData.status), + // 비즈니스 규칙: 상태는 완료로 저장 + status: 'completed', publicHash: generateRandomHash() }, { transaction: t }); } @@ -702,6 +784,7 @@ exports.saveEventFileData = async (req, res) => { eventId: event.id, memberId: newMember.id, status: 'confirmed', + paymentStatus: 'paid', teamId: teamId, comment: '' }, { transaction: t }); @@ -750,9 +833,15 @@ exports.saveEventFileData = async (req, res) => { for (const s of scoreItems) { const gameNumber = Number(s.gameNumber ?? 0) || 0; - const baseScore = Number(s.score ?? 0) || 0; const handicap = s.handicap !== undefined ? Number(s.handicap) : Number(participant.handicap || 0); - const totalScore = s.totalScore !== undefined ? Number(s.totalScore) : baseScore; + // scoresIncludeHandicap 플래그에 따라 저장 방식 결정 + const provided = s.totalScore !== undefined ? Number(s.totalScore) : Number(s.score ?? 0) || 0; + const totalScore = scoresIncludeHandicap + ? provided + : Math.max(0, provided + (Number.isFinite(handicap) ? handicap : 0)); + const baseScore = scoresIncludeHandicap + ? Math.max(0, provided - (Number.isFinite(handicap) ? handicap : 0)) + : provided; if (gameNumber <= 0) continue; await EventScore.create({ @@ -778,6 +867,10 @@ exports.saveEventFileData = async (req, res) => { title: result.title, eventType: result.eventType, startDate: result.startDate + }, + rollbackToken: { + eventId: result.id, + clubId: Number(result.clubId) } }); } catch (error) { diff --git a/backend/routes/eventRoutes.js b/backend/routes/eventRoutes.js index 805e725..ce17902 100644 --- a/backend/routes/eventRoutes.js +++ b/backend/routes/eventRoutes.js @@ -11,6 +11,8 @@ router.get('/:id', authenticateJWT, requireFeature('event_management'), eventCon router.post('/create', authenticateJWT, requireFeature('event_management'), eventController.createEvent); router.put('/:id', authenticateJWT, requireFeature('event_management'), eventController.updateEvent); router.delete('/:id', authenticateJWT, requireFeature('event_management'), eventController.deleteEvent); +// 이벤트 완전 삭제 (하드 삭제) +router.delete('/:id/permanent', authenticateJWT, requireFeature('event_management'), eventController.deleteEventPermanently); // 이벤트 참가자 관리 router.post('/:id/participants/list', authenticateJWT, requireFeature('event_management'), eventController.getEventParticipants); @@ -42,5 +44,7 @@ router.post('/upload', authenticateJWT, requireFeature('event_management'), file router.post('/parse-preview', authenticateJWT, requireFeature('event_management'), fileUploadController.parseEventFile); router.post('/save-file-data', authenticateJWT, requireFeature('event_management'), fileUploadController.saveEventFileData); router.post('/delete-file', authenticateJWT, requireFeature('event_management'), fileUploadController.deleteUploadedFile); +// 업로드 저장 롤백 +router.post('/rollback-import', authenticateJWT, requireFeature('event_management'), fileUploadController.rollbackEventImport); module.exports = router; \ No newline at end of file