323 lines
8.9 KiB
JavaScript
323 lines
8.9 KiB
JavaScript
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)
|
|
};
|