const { AverageCache } = require('../models'); const { EventScore, EventParticipant, Member, Event, Club, sequelize } = require('../models'); const { Op } = require('sequelize'); const memoryCache = new Map(); // 캐시 키 생성 함수 const generateAverageKey = (memberId, startDate, endDate) => { return `average:${memberId}:${startDate.getTime()}:${endDate.getTime()}`; }; // 클럽 설정 기반 캐시 키 생성 const generateClubSettingAverageKey = (memberId, clubId, period) => { return `average:club:${clubId}:${memberId}:${period}`; }; /** * 회원의 기간별 에버리지 계산 (캐싱 적용) * @param {number} memberId - 회원 ID * @param {Date} startDate - 시작 날짜 * @param {Date} endDate - 종료 날짜 * @returns {Object} - 에버리지 정보 */ const calculateMemberAverage = async (memberId, clubId, startDate, endDate) => { try { // 캐시 키 생성 const cacheKey = generateAverageKey(memberId, startDate, endDate); // 메모리 캐시에서 데이터 조회 if (memoryCache.has(cacheKey)) { return memoryCache.get(cacheKey); } // DB 캐시에서 데이터 조회 const cachedData = await AverageCache.findOne({ where: { memberId, startDate, endDate, expiredAt: { [Op.gt]: new Date() } } }); if (cachedData) { memoryCache.set(cacheKey, { average: cachedData.average, games: cachedData.games, totalScore: cachedData.totalScore }); return memoryCache.get(cacheKey); } // 캐시에 없으면 DB에서 계산 // 1. games(경기수)를 동적으로 count로 계산 const games = await EventScore.count({ include: [ { model: EventParticipant, where: { memberId }, include: [ { model: Event, where: { startDate: { [Op.between]: [startDate, endDate] } } } ] } ] }); if (games === 0) { const result = { average: 0, games: 0, totalScore: 0 }; // DB 캐시에 저장 (1시간 TTL) await AverageCache.upsert({ memberId, clubId: clubId, // 필요시 clubId 인자 추가 startDate, endDate, average: 0, games: 0, totalScore: 0, expiredAt: new Date(Date.now() + 60 * 60 * 1000) }); memoryCache.set(cacheKey, result); return result; } // 2. 점수 총합 계산 (동적) const scores = await EventScore.findAll({ include: [ { model: EventParticipant, where: { memberId }, include: [ { model: Event, where: { startDate: { [Op.between]: [startDate, endDate] } } } ] } ] }); const totalScore = scores.reduce((sum, score) => sum + score.score, 0); const average = parseFloat((totalScore / games).toFixed(1)); const result = { average, games, totalScore }; if (!clubId) { console.warn('[AverageCache] clubId가 null 또는 undefined로 전달됨:', { memberId, startDate, endDate }); } await AverageCache.upsert({ memberId, clubId, // 반드시 값 전달 startDate, endDate, average, games, totalScore, expiredAt: new Date(Date.now() + 60 * 60 * 1000) }); memoryCache.set(cacheKey, result); return result; } catch (error) { console.error('에버리지 계산 중 오류 발생:', error); throw error; } }; /** * 클럽 설정에 따른 회원 에버리지 계산 (캐싱 적용) * @param {number} memberId - 회원 ID * @param {number} clubId - 클럽 ID * @returns {Object} - 에버리지 정보 */ const calculateMemberAverageByClubSetting = async (memberId, clubId, deps = module.exports) => { try { const member = await Member.findByPk(memberId); if (!member) { throw new Error('회원을 찾을 수 없습니다.'); } const club = await Club.findByPk(clubId); if (!club) { throw new Error('클럽을 찾을 수 없습니다.'); } // 캐시 키 생성 // 기간 계산 const now = new Date(); const currentYear = now.getFullYear(); const currentMonth = now.getMonth(); let startDate; let endDate = new Date(); switch (club.averageCalculationPeriod) { case '3month': startDate = new Date(now); startDate.setMonth(now.getMonth() - 3); break; case '6month': startDate = new Date(now); startDate.setMonth(now.getMonth() - 6); break; case '1year': startDate = new Date(now); startDate.setFullYear(now.getFullYear() - 1); break; case 'firsthalf': startDate = new Date(currentYear, 0, 1); // 1월 1일 endDate = new Date(currentYear, 5, 30); // 6월 30일 break; case 'secondhalf': startDate = new Date(currentYear, 6, 1); // 7월 1일 endDate = new Date(currentYear, 11, 31); // 12월 31일 break; case 'quarterly': // 현재 분기 계산 const currentQuarter = Math.floor(currentMonth / 3) + 1; const quarterStartMonth = (currentQuarter - 1) * 3; startDate = new Date(currentYear, quarterStartMonth, 1); // 분기 시작일 endDate = new Date(currentYear, quarterStartMonth + 3, 0); // 분기 마지막일 break; case 'total': default: startDate = new Date(0); // 1970년 1월 1일 endDate = now; break; } // 캐시 키 생성 const cacheKey = generateClubSettingAverageKey(memberId, clubId, club.averageCalculationPeriod); // 메모리 캐시에서 데이터 조회 if (memoryCache.has(cacheKey)) { return memoryCache.get(cacheKey); } // DB 캐시에서 데이터 조회 const cachedData = await AverageCache.findOne({ where: { memberId, clubId, startDate, endDate, expiredAt: { [Op.gt]: new Date() } } }); if (cachedData) { memoryCache.set(cacheKey, { average: cachedData.average, games: cachedData.games, totalScore: cachedData.totalScore }); return memoryCache.get(cacheKey); } // DB에서 계산 const result = await deps.calculateMemberAverage(memberId, clubId, startDate, endDate); // 결과를 캐시에 저장 (TTL: 1시간) await AverageCache.upsert({ memberId, clubId, startDate, endDate, average: result.average, games: result.games, totalScore: result.totalScore, expiredAt: new Date(Date.now() + 60 * 60 * 1000) }); memoryCache.set(cacheKey, result); return result; } catch (error) { console.error('클럽 설정에 따른 에버리지 계산 중 오류 발생:', error); throw error; } }; /** * 새로운 점수가 추가되었을 때 캐시 무효화 * @param {number} memberId - 회원 ID */ // 특정 회원의 모든 club/기간별 average 캐시도 무효화 const invalidateClubAveragesForMember = async (memberId, deps = module.exports) => { // 메모리 캐시에서 삭제 for (const key of memoryCache.keys()) { if (key.startsWith('average:club:') && key.split(':')[3] == String(memberId)) { memoryCache.delete(key); } } // DB 캐시에서 삭제 await AverageCache.destroy({ where: { memberId } }); }; const invalidateMemberAverageCache = async (memberId, deps = module.exports) => { // 메모리 캐시에서 삭제 for (const key of memoryCache.keys()) { if (key.startsWith('average:') && key.split(':')[1] == String(memberId)) { memoryCache.delete(key); } } // DB 캐시에서 삭제 await AverageCache.destroy({ where: { memberId } }); await invalidateClubAveragesForMember(memberId, deps); }; /** * 만료된 캐시 정리 (주기적으로 실행) */ const cleanExpiredCache = async () => { try { const now = new Date(); // 메모리 캐시 정리 for (const [key, value] of memoryCache.entries()) { if (value.expiry && new Date(value.expiry) < now) { memoryCache.delete(key); } } // DB 캐시 정리 await AverageCache.destroy({ where: { expiredAt: { [Op.lt]: now } } }); } catch (error) { console.error('만료된 캐시 정리 중 오류 발생:', error); } }; // 서버 시작 시 캐시 초기화 // 주기적으로 만료된 캐시 정리 (1시간마다) setInterval(cleanExpiredCache, 60 * 60 * 1000); module.exports = { calculateMemberAverage, calculateMemberAverageByClubSetting, invalidateMemberAverageCache, cleanExpiredCache, // 테스트를 위해 내부 함수 노출 getFromCache: (key) => memoryCache.get(key), setToCache: (key, value) => memoryCache.set(key, value), invalidateCache: (key) => memoryCache.delete(key) };