오류 수정, 통계 보강

This commit is contained in:
2025-06-08 20:21:37 +09:00
parent 1a3706d7a5
commit 141a0de6d0
42 changed files with 2645 additions and 799 deletions
+322
View File
@@ -0,0 +1,322 @@
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)
};