파일 업로드기능 완료
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
/**
|
||||
* 파일 업로드 컨트롤러
|
||||
* 이벤트 관련 파일 업로드 및 처리를 담당하는 컨트롤러
|
||||
*/
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const xlsx = require('xlsx');
|
||||
const csv = require('csv-parser');
|
||||
const Tesseract = require('tesseract.js');
|
||||
const sharp = require('sharp');
|
||||
const models = require('../models');
|
||||
const { Event, EventParticipant, Member, EventScore, Club, EventTeam, ClubMember, EventTeamMember } = models;
|
||||
const { Op, Sequelize } = require('sequelize');
|
||||
const { sequelize } = require('../models');
|
||||
|
||||
// 업로드 디렉토리 설정
|
||||
const uploadDir = path.join(__dirname, '../uploads');
|
||||
// 업로드 디렉토리가 없으면 생성
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 파일 업로드를 위한 multer 설정
|
||||
const storage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, uploadDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
cb(null, uniqueSuffix + path.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
|
||||
// 파일 필터 (허용된 파일 형식만 업로드)
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowedTypes = [
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'text/csv',
|
||||
'image/jpeg',
|
||||
'image/png'
|
||||
];
|
||||
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('지원되지 않는 파일 형식입니다. Excel, CSV, JPG, PNG 파일만 업로드 가능합니다.'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
fileFilter: fileFilter,
|
||||
limits: { fileSize: 10 * 1024 * 1024 } // 10MB 제한
|
||||
});
|
||||
|
||||
// 파일 업로드 처리
|
||||
exports.uploadEventFile = (req, res) => {
|
||||
upload.single('file')(req, res, async function (err) {
|
||||
if (err instanceof multer.MulterError) {
|
||||
// Multer 에러 처리
|
||||
return res.status(400).json({ message: `파일 업로드 오류: ${err.message}` });
|
||||
} else if (err) {
|
||||
// 기타 에러 처리
|
||||
return res.status(400).json({ message: err.message });
|
||||
}
|
||||
|
||||
// 파일이 없는 경우
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ message: '업로드할 파일이 없습니다.' });
|
||||
}
|
||||
|
||||
// 클럽 ID 확인
|
||||
const clubId = req.body.clubId;
|
||||
if (!clubId) {
|
||||
// 파일 삭제
|
||||
if (fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 클럽 존재 여부 확인
|
||||
const club = await Club.findOne({
|
||||
where: { id: clubId, status: 'active' }
|
||||
});
|
||||
|
||||
if (!club) {
|
||||
// 파일 삭제
|
||||
if (fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 이미지 파일 전처리 (OCR용)
|
||||
let ocrImagePath = req.file.path;
|
||||
if (req.file.mimetype.startsWith('image/')) {
|
||||
const preprocessedPath = req.file.path.replace(/(\.[^.]+)$/, '_preprocessed$1');
|
||||
await sharp(req.file.path)
|
||||
.grayscale()
|
||||
.sharpen()
|
||||
.toFile(preprocessedPath);
|
||||
ocrImagePath = preprocessedPath;
|
||||
}
|
||||
|
||||
// 파일 정보 반환
|
||||
res.json({
|
||||
message: '파일이 업로드되었습니다.',
|
||||
file: {
|
||||
filename: req.file.filename,
|
||||
originalname: req.file.originalname,
|
||||
mimetype: req.file.mimetype,
|
||||
size: req.file.size,
|
||||
path: req.file.path,
|
||||
clubId: clubId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// 오류 발생 시 파일 삭제
|
||||
if (fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
console.error('파일 업로드 오류:', error);
|
||||
res.status(500).json({ message: `파일 업로드 중 오류가 발생했습니다: ${error.message}` });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 업로드된 파일 파싱 및 미리보기 데이터 제공
|
||||
exports.parseEventFile = async (req, res) => {
|
||||
try {
|
||||
const { filename, clubId } = req.body;
|
||||
|
||||
if (!filename) {
|
||||
return res.status(400).json({ message: '파일명이 필요합니다.' });
|
||||
}
|
||||
|
||||
if (!clubId) {
|
||||
return res.status(400).json({ message: '클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
const filePath = path.join(uploadDir, filename);
|
||||
|
||||
// 파일 존재 여부 확인
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({ message: '파일을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
// 파일 확장자 확인
|
||||
const fileExt = path.extname(filename).toLowerCase();
|
||||
let parsedData = [];
|
||||
|
||||
// 클럽 회원 목록 조회 (참가자 매핑용)
|
||||
const club = await Club.findOne({
|
||||
where: { id: clubId, status: 'active' }
|
||||
});
|
||||
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' });
|
||||
}
|
||||
|
||||
const clubMembers = await club.getMembers({
|
||||
attributes: ['id', 'name', 'gender', 'average', 'handicap'],
|
||||
through: { where: { status: 'active' } }
|
||||
});
|
||||
|
||||
const memberMap = new Map();
|
||||
clubMembers.forEach(member => {
|
||||
if (member && member.name) {
|
||||
memberMap.set(member.name, {
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
gender: member.gender,
|
||||
average: member.average,
|
||||
handicap: member.handicap
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 파일 형식에 따른 처리
|
||||
if (fileExt === '.xlsx' || fileExt === '.xls') {
|
||||
// Excel 파일 처리
|
||||
parsedData = await parseExcelFile(filePath, memberMap);
|
||||
} else if (fileExt === '.csv') {
|
||||
// CSV 파일 처리
|
||||
parsedData = await parseCsvFile(filePath, memberMap);
|
||||
} else if (fileExt === '.jpg' || fileExt === '.jpeg' || fileExt === '.png') {
|
||||
// 이미지 파일 OCR 처리
|
||||
parsedData = await parseImageFile(filePath, memberMap);
|
||||
} else {
|
||||
return res.status(400).json({ message: '지원되지 않는 파일 형식입니다.' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: '파일 파싱 완료',
|
||||
data: parsedData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('파일 파싱 오류:', error);
|
||||
res.status(500).json({ message: `파일 파싱 중 오류가 발생했습니다: ${error.message}` });
|
||||
}
|
||||
};
|
||||
|
||||
// Excel 파일 파싱 함수
|
||||
async function parseExcelFile(filePath, memberMap) {
|
||||
const workbook = xlsx.readFile(filePath, { type: 'binary', cellDates: true, codepage: 949 });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const jsonData = xlsx.utils.sheet_to_json(worksheet, { header: 1, defval: '' });
|
||||
|
||||
return processRawData(jsonData, memberMap);
|
||||
}
|
||||
|
||||
// CSV 파일 파싱 함수
|
||||
async function parseCsvFile(filePath, memberMap) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const results = [];
|
||||
fs.createReadStream(filePath, { encoding: 'utf8' })
|
||||
.pipe(csv({ headers: false, skipEmptyLines: true }))
|
||||
.on('data', (data) => {
|
||||
// 각 행을 배열로 변환
|
||||
results.push(Object.values(data).map(cell => (cell || '').trim()));
|
||||
})
|
||||
.on('end', () => {
|
||||
try {
|
||||
const processedData = processRawData(results, memberMap);
|
||||
resolve(processedData);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
})
|
||||
.on('error', (error) => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
// 이미지 파일 OCR 처리 함수
|
||||
async function parseImageFile(filePath, memberMap) {
|
||||
try {
|
||||
// 한글 인식을 위한 Tesseract 설정
|
||||
const { data } = await Tesseract.recognize(filePath, 'kor+eng', {
|
||||
logger: m => console.log(m),
|
||||
tessedit_char_whitelist: '0123456789가-힣이름팀핸디총핀에버Gg.,:()[]- ',
|
||||
preserve_interword_spaces: 1,
|
||||
psm: 6 // 표/블록: 6, 한줄: 7, 단어: 8. 상황에 따라 실험 가능
|
||||
});
|
||||
|
||||
// OCR 원본 결과 로그
|
||||
console.log('[OCR 원본 결과]', data.text);
|
||||
|
||||
// OCR 결과 텍스트 처리
|
||||
const lines = data.text.split('\n')
|
||||
.filter(line => line.trim() !== '')
|
||||
.map(line => line.split(/\s+/));
|
||||
|
||||
return processRawData(lines, memberMap);
|
||||
} catch (error) {
|
||||
console.error('OCR 처리 오류:', error);
|
||||
throw new Error('이미지 OCR 처리 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 원시 데이터 처리 및 정규화 함수
|
||||
function processRawData(rawData, memberMap) {
|
||||
// 이벤트 정보 추론
|
||||
const inferredEventInfo = inferEventInfo(rawData);
|
||||
|
||||
// 다양한 표 헤더 키워드 정의
|
||||
const headerKeywords = {
|
||||
name: ['이름', '성명', '참가자'],
|
||||
team: ['팀', '팀명'],
|
||||
handicap: ['핸디', '핸디캡'],
|
||||
total: ['총핀', '총점', '합계'],
|
||||
avg: ['에버', '평균'],
|
||||
// 점수 컬럼: 1게임, 2게임, 1G, 2G, 16, 26 등 다양한 형태
|
||||
score: [/^(\d+)[Gg]$/, /^(\d{2,})$/, /^\d+게임$/]
|
||||
};
|
||||
|
||||
// 헤더 행 찾기 및 컬럼 인덱스 매핑
|
||||
let headerRow = -1;
|
||||
let colMap = {};
|
||||
for (let i = 0; i < Math.min(10, rawData.length); i++) {
|
||||
const row = rawData[i];
|
||||
row.forEach((cell, idx) => {
|
||||
if (typeof cell !== 'string') return;
|
||||
for (const key in headerKeywords) {
|
||||
const patterns = headerKeywords[key];
|
||||
for (const pattern of patterns) {
|
||||
if ((typeof pattern === 'string' && cell.replace(/\s/g, '').includes(pattern)) ||
|
||||
(pattern instanceof RegExp && pattern.test(cell.replace(/\s/g, '')))) {
|
||||
if (key === 'score') {
|
||||
if (!colMap.scores) colMap.scores = [];
|
||||
colMap.scores.push(idx);
|
||||
} else {
|
||||
colMap[key] = idx;
|
||||
}
|
||||
headerRow = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// 헤더가 최소 이름, 점수, 팀 중 하나라도 있으면 헤더로 간주
|
||||
if (Object.keys(colMap).length > 1) break;
|
||||
}
|
||||
if (headerRow === -1) return { participants: [], gameCount: 0, inferredEventInfo };
|
||||
|
||||
// 데이터 행 파싱
|
||||
const participants = [];
|
||||
for (let i = headerRow + 1; i < rawData.length; i++) {
|
||||
const row = rawData[i];
|
||||
if (row.length < 2) continue;
|
||||
// 이름: 한글 2자 이상
|
||||
const name = colMap.name !== undefined ? row[colMap.name] : row.find(cell => /^[가-힣a-z]{2,}$/i.test(cell));
|
||||
if (!name) continue;
|
||||
const teamNumber = colMap.team !== undefined ? row[colMap.team] : '';
|
||||
const scores = (colMap.scores || []).map(idx => parseInt(row[idx], 10)).filter(n => !isNaN(n));
|
||||
const handicap = colMap.handicap !== undefined ? parseInt(row[colMap.handicap], 10) : null;
|
||||
const total = colMap.total !== undefined ? parseInt(row[colMap.total], 10) : null;
|
||||
const avg = colMap.avg !== undefined ? parseFloat(row[colMap.avg]) : null;
|
||||
// 회원 매핑 (이름 기준)
|
||||
const memberMatch = memberMap && memberMap.get && name ? memberMap.get(name) : null;
|
||||
participants.push({
|
||||
name,
|
||||
teamNumber,
|
||||
scores,
|
||||
handicap,
|
||||
total,
|
||||
avg,
|
||||
memberId: memberMatch ? memberMatch.id : null,
|
||||
memberInfo: memberMatch || null
|
||||
});
|
||||
}
|
||||
return {
|
||||
participants,
|
||||
gameCount: Math.max(...participants.map(p => p.scores.length), 0),
|
||||
inferredEventInfo
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 파일에서 이벤트 정보 추론
|
||||
function inferEventInfo(rawData) {
|
||||
const result = {
|
||||
title: null,
|
||||
titleCandidates: [],
|
||||
startDate: null,
|
||||
location: null
|
||||
};
|
||||
|
||||
// 파일의 처음 10줄을 검색하여 이벤트 정보 추론
|
||||
const maxRows = Math.min(10, rawData.length);
|
||||
|
||||
// 제목 추론 패턴 (키워드 포함 전체 구문 추출)
|
||||
const titlePatterns = [
|
||||
/(.+?정기전)/i,
|
||||
/(.+?대회)/i,
|
||||
/(.+?이벤트)/i,
|
||||
/(.+?토너먼트)/i,
|
||||
/(.+?경기)/i,
|
||||
/(.+?오픈)/i,
|
||||
/제\s*\d+\s*회\s*.+/i,
|
||||
/제\s*\d+\s*차\s*.+/i,
|
||||
/\d+\s*회\s*.+/i,
|
||||
/\d+\s*차\s*.+/i,
|
||||
/정기전\s*[:\-]?\s*.+/i,
|
||||
/대회\s*[:\-]?\s*.+/i,
|
||||
/이벤트\s*[:\-]?\s*.+/i
|
||||
];
|
||||
|
||||
// 날짜 추론 패턴
|
||||
const datePatterns = [
|
||||
// YYYY-MM-DD 형식
|
||||
/(\d{4})[-\/\.]\s*(\d{1,2})[-\/\.]\s*(\d{1,2})/,
|
||||
// YYYY년 MM월 DD일 형식
|
||||
/(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})\s*일/,
|
||||
// MM월 DD일 형식 (현재 연도 가정)
|
||||
/(\d{1,2})\s*월\s*(\d{1,2})\s*일/,
|
||||
// 날짜: YYYY-MM-DD 형식
|
||||
/날짜\s*[:\-]?\s*(\d{4})[-\/\.]\s*(\d{1,2})[-\/\.]\s*(\d{1,2})/,
|
||||
// 일시: YYYY-MM-DD 형식
|
||||
/일시\s*[:\-]?\s*(\d{4})[-\/\.]\s*(\d{1,2})[-\/\.]\s*(\d{1,2})/,
|
||||
// 시작일: YYYY-MM-DD 형식
|
||||
/시작일\s*[:\-]?\s*(\d{4})[-\/\.]\s*(\d{1,2})[-\/\.]\s*(\d{1,2})/,
|
||||
// YYYYMMDD 형식
|
||||
/\s*(\d{4})\s*(\d{2})\s*(\d{2})\s*/,
|
||||
// YYMMDD, YY.MM.DD, YY-MM-DD 형식 (25.02.04, 25-02-04, 250204)
|
||||
/(\d{2})[.\-\/\s]?(\d{2})[.\-\/\s]?(\d{2})/
|
||||
];
|
||||
|
||||
// 파일명에서 정보 추론 (originalname이 있는 경우)
|
||||
if (rawData.originalname) {
|
||||
const filename = rawData.originalname;
|
||||
|
||||
// 제목 추론 (모든 후보 수집)
|
||||
for (const pattern of titlePatterns) {
|
||||
const match = filename.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const candidate = match[1].trim();
|
||||
if (!result.titleCandidates.includes(candidate)) {
|
||||
result.titleCandidates.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.titleCandidates.length > 0) {
|
||||
result.title = result.titleCandidates[0];
|
||||
}
|
||||
|
||||
// 날짜 추론
|
||||
for (const pattern of datePatterns) {
|
||||
const match = filename.match(pattern);
|
||||
if (match) {
|
||||
// 매칭 그룹의 수에 따라 다르게 처리
|
||||
if (match.length >= 4) { // YYYY-MM-DD 형식
|
||||
const year = parseInt(match[1]);
|
||||
const month = parseInt(match[2]) - 1; // JavaScript의 월은 0부터 시작
|
||||
const day = parseInt(match[3]);
|
||||
result.startDate = new Date(year, month, day);
|
||||
} else if (match.length >= 3) { // MM월 DD일 형식
|
||||
const currentYear = new Date().getFullYear();
|
||||
const month = parseInt(match[1]) - 1;
|
||||
const day = parseInt(match[2]);
|
||||
result.startDate = new Date(currentYear, month, day);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 파일 내용에서 정보 추론
|
||||
for (let i = 0; i < maxRows; i++) {
|
||||
const row = rawData[i];
|
||||
if (!row || !Array.isArray(row)) continue;
|
||||
|
||||
// 행을 문자열로 변환하여 패턴 매칭
|
||||
const rowText = row.join(' ');
|
||||
|
||||
// 제목 후보 수집
|
||||
for (const pattern of titlePatterns) {
|
||||
const match = rowText.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const candidate = match[1].trim();
|
||||
if (!result.titleCandidates.includes(candidate)) {
|
||||
result.titleCandidates.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!result.title && result.titleCandidates.length > 0) {
|
||||
result.title = result.titleCandidates[0];
|
||||
}
|
||||
|
||||
// 날짜가 아직 추론되지 않은 경우
|
||||
if (!result.startDate) {
|
||||
for (const pattern of datePatterns) {
|
||||
const match = rowText.match(pattern);
|
||||
if (match) {
|
||||
// 매칭 그룹의 수에 따라 다르게 처리
|
||||
if (match.length >= 4) {
|
||||
// YYYY-MM-DD, YYYYMMDD, YYMMDD, YY.MM.DD, YY-MM-DD 모두 처리
|
||||
let year = parseInt(match[1], 10);
|
||||
const month = parseInt(match[2], 10) - 1;
|
||||
const day = parseInt(match[3], 10);
|
||||
// 2자리 연도라면 보정
|
||||
if (match[1].length === 2) {
|
||||
year += 2000;
|
||||
}
|
||||
result.startDate = new Date(year, month, day);
|
||||
} else if (match.length === 3) {
|
||||
// MM월 DD일 형식 (현재 연도 가정)
|
||||
const currentYear = new Date().getFullYear();
|
||||
const month = parseInt(match[1]) - 1;
|
||||
const day = parseInt(match[2]);
|
||||
result.startDate = new Date(currentYear, month, day);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 정보가 추론되었다면 중단
|
||||
if (result.title && result.startDate) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 이벤트에 파일 데이터 저장
|
||||
exports.saveEventFileData = async (req, res) => {
|
||||
const { eventData, participants, clubId } = req.body;
|
||||
|
||||
if (!eventData || !participants || !clubId) {
|
||||
return res.status(400).json({ message: '이벤트 정보, 참가자 정보, 클럽 ID가 필요합니다.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 트랜잭션 시작
|
||||
const result = await sequelize.transaction(async (t) => {
|
||||
// 1. 이벤트 생성 또는 업데이트
|
||||
let event;
|
||||
if (eventData.id) {
|
||||
// 기존 이벤트 업데이트
|
||||
event = await Event.findOne({
|
||||
where: { id: eventData.id, clubId },
|
||||
transaction: t
|
||||
});
|
||||
|
||||
if (!event) {
|
||||
throw new Error('이벤트를 찾을 수 없습니다.');
|
||||
}
|
||||
|
||||
await event.update({
|
||||
title: eventData.title,
|
||||
description: eventData.description || '',
|
||||
eventType: eventData.eventType,
|
||||
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: eventData.status || '완료'
|
||||
}, { transaction: t });
|
||||
} else {
|
||||
// 새 이벤트 생성
|
||||
event = await Event.create({
|
||||
clubId,
|
||||
title: eventData.title,
|
||||
description: eventData.description || '',
|
||||
eventType: eventData.eventType,
|
||||
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: eventData.status || '완료',
|
||||
publicHash: generateRandomHash()
|
||||
}, { transaction: t });
|
||||
}
|
||||
|
||||
// 1.5 팀 정보 처리 - 참가자 처리 전에 팀 생성
|
||||
// 팀 번호별로 그룹화
|
||||
const teamGroups = {};
|
||||
participants.forEach(p => {
|
||||
if (p.teamNumber) {
|
||||
if (!teamGroups[p.teamNumber]) {
|
||||
teamGroups[p.teamNumber] = [];
|
||||
}
|
||||
teamGroups[p.teamNumber].push(p);
|
||||
}
|
||||
});
|
||||
|
||||
// 팀 생성
|
||||
const createdTeams = {};
|
||||
for (const teamNumber in teamGroups) {
|
||||
// 기존 팀 확인
|
||||
let eventTeam = await EventTeam.findOne({
|
||||
where: {
|
||||
eventId: event.id,
|
||||
teamNumber: parseInt(teamNumber)
|
||||
},
|
||||
transaction: t
|
||||
});
|
||||
|
||||
// 팀이 없으면 생성
|
||||
if (!eventTeam) {
|
||||
eventTeam = await EventTeam.create({
|
||||
eventId: event.id,
|
||||
teamNumber: parseInt(teamNumber),
|
||||
name: `팀 ${teamNumber}`,
|
||||
handicap: 0 // 기본 핸디캡 값
|
||||
}, { transaction: t });
|
||||
}
|
||||
|
||||
createdTeams[teamNumber] = eventTeam;
|
||||
}
|
||||
|
||||
// 2. 참가자 및 점수 처리
|
||||
for (const participant of participants) {
|
||||
let eventParticipant;
|
||||
|
||||
// 참가자 생성 또는 업데이트
|
||||
if (participant.memberId) {
|
||||
// 기존 회원인 경우
|
||||
const existingParticipant = await EventParticipant.findOne({
|
||||
where: {
|
||||
eventId: event.id,
|
||||
memberId: participant.memberId
|
||||
},
|
||||
transaction: t
|
||||
});
|
||||
|
||||
// 팀 ID 가져오기
|
||||
let teamId = null;
|
||||
if (participant.teamNumber && createdTeams[participant.teamNumber]) {
|
||||
teamId = createdTeams[participant.teamNumber].id;
|
||||
}
|
||||
|
||||
if (existingParticipant) {
|
||||
// 기존 참가자 업데이트 (팀 정보 포함)
|
||||
await existingParticipant.update({
|
||||
teamId: teamId
|
||||
}, { transaction: t });
|
||||
eventParticipant = existingParticipant;
|
||||
} else {
|
||||
eventParticipant = await EventParticipant.create({
|
||||
eventId: event.id,
|
||||
memberId: participant.memberId,
|
||||
status: '참가확정',
|
||||
paymentStatus: '납부완료',
|
||||
teamId: teamId,
|
||||
comment: ''
|
||||
}, { transaction: t });
|
||||
}
|
||||
} else {
|
||||
// 새 회원 생성 및 참가자 등록
|
||||
const newMember = await Member.create({
|
||||
name: participant.name,
|
||||
gender: participant.gender || '남성',
|
||||
memberType: '정회원',
|
||||
status: '활성'
|
||||
}, { transaction: t });
|
||||
|
||||
// 클럽 회원으로 등록
|
||||
await ClubMember.create({
|
||||
clubId,
|
||||
memberId: newMember.id,
|
||||
joinDate: new Date(),
|
||||
status: 'active'
|
||||
}, { transaction: t });
|
||||
|
||||
// 팀 ID 가져오기
|
||||
let teamId = null;
|
||||
if (participant.teamNumber && createdTeams[participant.teamNumber]) {
|
||||
teamId = createdTeams[participant.teamNumber].id;
|
||||
}
|
||||
|
||||
eventParticipant = await EventParticipant.create({
|
||||
eventId: event.id,
|
||||
memberId: newMember.id,
|
||||
status: '참가확정',
|
||||
teamId: teamId,
|
||||
comment: ''
|
||||
}, { transaction: t });
|
||||
}
|
||||
|
||||
// 팀에 속한 참가자인 경우 팀 멤버로 등록
|
||||
if (participant.teamNumber && createdTeams[participant.teamNumber]) {
|
||||
// 기존 팀 멤버 확인
|
||||
const existingTeamMember = await EventTeamMember.findOne({
|
||||
where: {
|
||||
eventTeamId: createdTeams[participant.teamNumber].id,
|
||||
eventParticipantId: eventParticipant.id
|
||||
},
|
||||
transaction: t
|
||||
});
|
||||
|
||||
// 팀 멤버가 없으면 생성
|
||||
if (!existingTeamMember) {
|
||||
await EventTeamMember.create({
|
||||
eventTeamId: createdTeams[participant.teamNumber].id,
|
||||
eventParticipantId: eventParticipant.id,
|
||||
order: 1
|
||||
}, { transaction: t });
|
||||
}
|
||||
}
|
||||
|
||||
// 점수 저장
|
||||
if (participant.scores && participant.scores.length > 0) {
|
||||
// 기존 점수 삭제
|
||||
await EventScore.destroy({
|
||||
where: {
|
||||
eventId: event.id,
|
||||
participantId: eventParticipant.id
|
||||
},
|
||||
transaction: t
|
||||
});
|
||||
|
||||
// 새 점수 등록
|
||||
for (const scoreData of participant.scores) {
|
||||
// 핵디캡 값이 없는 경우 참가자의 핵디캡 값 사용
|
||||
const handicap = scoreData.handicap !== undefined ? scoreData.handicap : (participant.handicap || 0);
|
||||
// 총점수가 없는 경우 원점수 사용
|
||||
const totalScore = scoreData.totalScore !== undefined ? scoreData.totalScore : scoreData.score;
|
||||
|
||||
await EventScore.create({
|
||||
eventId: event.id,
|
||||
participantId: eventParticipant.id,
|
||||
gameNumber: scoreData.gameNumber,
|
||||
score: scoreData.score,
|
||||
handicap: handicap,
|
||||
totalScore: totalScore,
|
||||
teamNumber: participant.teamNumber || null
|
||||
}, { transaction: t });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: '이벤트 데이터가 성공적으로 저장되었습니다.',
|
||||
event: {
|
||||
id: result.id,
|
||||
title: result.title,
|
||||
eventType: result.eventType,
|
||||
startDate: result.startDate
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('이벤트 데이터 저장 오류:', error);
|
||||
res.status(500).json({ message: `이벤트 데이터 저장 중 오류가 발생했습니다: ${error.message}` });
|
||||
}
|
||||
};
|
||||
|
||||
// 랜덤 해시 생성 함수
|
||||
function generateRandomHash() {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
return Array.from({ length: 16 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
||||
}
|
||||
|
||||
// 임시 파일 삭제
|
||||
exports.deleteUploadedFile = async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.body;
|
||||
|
||||
if (!filename) {
|
||||
return res.status(400).json({ message: '파일명이 필요합니다.' });
|
||||
}
|
||||
|
||||
const filePath = path.join(uploadDir, filename);
|
||||
|
||||
// 파일 존재 여부 확인
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
res.json({ message: '파일이 삭제되었습니다.' });
|
||||
} catch (error) {
|
||||
console.error('파일 삭제 오류:', error);
|
||||
res.status(500).json({ message: `파일 삭제 중 오류가 발생했습니다: ${error.message}` });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user