774 lines
26 KiB
JavaScript
774 lines
26 KiB
JavaScript
/**
|
|
* 파일 업로드 컨트롤러
|
|
* 이벤트 관련 파일 업로드 및 처리를 담당하는 컨트롤러
|
|
*/
|
|
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 averageService = require('../services/averageService');
|
|
|
|
// 업로드 디렉토리 설정
|
|
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, sheetName } = 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', 'handicap'],
|
|
through: { where: { status: 'active' } }
|
|
});
|
|
|
|
// 각 회원의 동적 에버리지 계산
|
|
const memberMap = new Map();
|
|
for (const member of clubMembers) {
|
|
if (member && member.name) {
|
|
let dynamicAverage = 0;
|
|
try {
|
|
const averageData = await averageService.calculateMemberAverageByClubSetting(
|
|
member.id,
|
|
clubId
|
|
);
|
|
dynamicAverage = averageData.average;
|
|
} catch (error) {
|
|
console.error(`회원 ${member.id}의 에버리지 계산 오류:`, error);
|
|
}
|
|
|
|
memberMap.set(member.name, {
|
|
id: member.id,
|
|
name: member.name,
|
|
gender: member.gender,
|
|
average: dynamicAverage,
|
|
handicap: member.handicap
|
|
});
|
|
}
|
|
};
|
|
|
|
// 파일 형식에 따른 처리
|
|
if (fileExt === '.xlsx' || fileExt === '.xls') {
|
|
// Excel 파일 처리
|
|
parsedData = await parseExcelFile(filePath, memberMap, sheetName);
|
|
} 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: '지원되지 않는 파일 형식입니다.' });
|
|
}
|
|
|
|
// 참가자별 동적 에버리지 계산 호출 (memberId가 있을 때만)
|
|
if (parsedData && parsedData.participants) {
|
|
for (const p of parsedData.participants) {
|
|
if (p.memberId) {
|
|
await averageService.calculateMemberAverageByClubSetting(p.memberId, clubId);
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
message: '파일 파싱 완료',
|
|
data: parsedData
|
|
});
|
|
} catch (error) {
|
|
console.error('파일 파싱 오류:', error);
|
|
res.status(500).json({ message: `파일 파싱 중 오류가 발생했습니다: ${error.message}` });
|
|
}
|
|
};
|
|
|
|
// Excel 파일 파싱 함수
|
|
async function parseExcelFile(filePath, memberMap, requestedSheetName = null) {
|
|
const workbook = xlsx.readFile(filePath, { type: 'binary', cellDates: true, codepage: 949 });
|
|
|
|
// 요청된 시트명이 있고 해당 시트가 존재하면 그것을 사용, 아니면 첫 번째 시트 사용
|
|
const sheetName = (requestedSheetName && workbook.SheetNames.includes(requestedSheetName))
|
|
? requestedSheetName
|
|
: 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}` });
|
|
}
|
|
};
|