feat(bots): 스케줄러에 활성 월 게이트 추가
봇 매퍼에 activeMonths 파싱 추가. cron 콜백·즉시 실행 시 현재 KST 월이 활성 월에 포함될 때만 동기화 실행(매 실행 시점 재평가). NULL/ 빈배열/12개 전체는 항상 실행. date.js에 monthKST() 헬퍼 추가. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
604142672e
commit
fa37891ab3
2 changed files with 42 additions and 3 deletions
|
|
@ -2,13 +2,37 @@ import fp from 'fastify-plugin';
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import staticBots from '../config/bots.js';
|
import staticBots from '../config/bots.js';
|
||||||
import { syncAllSchedules } from '../services/meilisearch/index.js';
|
import { syncAllSchedules } from '../services/meilisearch/index.js';
|
||||||
import { nowKST } from '../utils/date.js';
|
import { nowKST, monthKST } from '../utils/date.js';
|
||||||
import { logActivity } from '../utils/log.js';
|
import { logActivity } from '../utils/log.js';
|
||||||
|
|
||||||
const REDIS_PREFIX = 'bot:status:';
|
const REDIS_PREFIX = 'bot:status:';
|
||||||
const TIMEZONE = 'Asia/Seoul';
|
const TIMEZONE = 'Asia/Seoul';
|
||||||
const MAX_CONSECUTIVE_ERRORS = 10;
|
const MAX_CONSECUTIVE_ERRORS = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* active_months 컬럼 파싱 (JSON 정수 배열 또는 null)
|
||||||
|
*/
|
||||||
|
function parseActiveMonths(raw) {
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||||
|
if (!Array.isArray(arr) || arr.length === 0) return null;
|
||||||
|
return arr.map(Number).filter((m) => m >= 1 && m <= 12);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 KST 월이 봇의 활성 월에 포함되는지
|
||||||
|
* - activeMonths가 null/빈배열/12개 전체 → 항상 실행
|
||||||
|
*/
|
||||||
|
function isActiveMonth(bot) {
|
||||||
|
const months = bot.activeMonths;
|
||||||
|
if (!months || months.length === 0 || months.length >= 12) return true;
|
||||||
|
return months.includes(monthKST());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 에러 객체에서 활동 로그용 details 구성
|
* 에러 객체에서 활동 로그용 details 구성
|
||||||
* - err.cause(Node fetch failed의 진짜 원인 등), err.code를 함께 포함
|
* - err.cause(Node fetch failed의 진짜 원인 등), err.code를 함께 포함
|
||||||
|
|
@ -73,6 +97,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
: [],
|
: [],
|
||||||
extractMembersFromDesc: row.extract_members_from_desc === 1,
|
extractMembersFromDesc: row.extract_members_from_desc === 1,
|
||||||
extractMembersFromTitle: row.extract_members_from_title === 1,
|
extractMembersFromTitle: row.extract_members_from_title === 1,
|
||||||
|
activeMonths: parseActiveMonths(row.active_months),
|
||||||
autoScheduleNext: row.auto_schedule_config
|
autoScheduleNext: row.auto_schedule_config
|
||||||
? (typeof row.auto_schedule_config === 'string'
|
? (typeof row.auto_schedule_config === 'string'
|
||||||
? JSON.parse(row.auto_schedule_config)
|
? JSON.parse(row.auto_schedule_config)
|
||||||
|
|
@ -108,6 +133,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
includeRetweets: row.include_retweets === 1,
|
includeRetweets: row.include_retweets === 1,
|
||||||
extractYoutube: row.extract_youtube === 1,
|
extractYoutube: row.extract_youtube === 1,
|
||||||
excludeManagedChannels: row.exclude_managed_channels === 1,
|
excludeManagedChannels: row.exclude_managed_channels === 1,
|
||||||
|
activeMonths: parseActiveMonths(row.active_months),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +167,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
cron: intervalToCron(row.cron_interval),
|
cron: intervalToCron(row.cron_interval),
|
||||||
cronInterval: row.cron_interval,
|
cronInterval: row.cron_interval,
|
||||||
enabled: row.enabled === 1,
|
enabled: row.enabled === 1,
|
||||||
|
activeMonths: parseActiveMonths(row.active_months),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -374,6 +401,11 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
|
|
||||||
// cron 태스크 등록 (한국 시간 기준)
|
// cron 태스크 등록 (한국 시간 기준)
|
||||||
const task = cron.schedule(bot.cron, async () => {
|
const task = cron.schedule(bot.cron, async () => {
|
||||||
|
// 활성 월이 아니면 스킵 (매 실행 시점의 현재 월로 재평가)
|
||||||
|
if (!isActiveMonth(bot)) {
|
||||||
|
fastify.log.info(`[${botId}] 비활성 월(${monthKST()}월) - 동기화 스킵`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
fastify.log.info(`[${botId}] 동기화 시작`);
|
fastify.log.info(`[${botId}] 동기화 시작`);
|
||||||
if (bot.weeklySchedule) {
|
if (bot.weeklySchedule) {
|
||||||
await startWeeklyBurst(botId, bot, syncFn);
|
await startWeeklyBurst(botId, bot, syncFn);
|
||||||
|
|
@ -386,8 +418,8 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
await updateStatus(botId, { status: 'running' });
|
await updateStatus(botId, { status: 'running' });
|
||||||
fastify.log.info(`[${botId}] 스케줄 시작 (cron: ${bot.cron})`);
|
fastify.log.info(`[${botId}] 스케줄 시작 (cron: ${bot.cron})`);
|
||||||
|
|
||||||
// 즉시 1회 실행: meilisearch와 weekly 모드는 제외 (weekly는 지정 시각에만)
|
// 즉시 1회 실행: meilisearch와 weekly 모드는 제외 (weekly는 지정 시각에만), 비활성 월도 제외
|
||||||
if (bot.type !== 'meilisearch' && !bot.weeklySchedule) {
|
if (bot.type !== 'meilisearch' && !bot.weeklySchedule && isActiveMonth(bot)) {
|
||||||
await runSync(botId, bot, syncFn, { setRunningStatus: false });
|
await runSync(botId, bot, syncFn, { setRunningStatus: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,13 @@ export function nowKST() {
|
||||||
return dayjs().tz(KST).format();
|
return dayjs().tz(KST).format();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 KST 기준 월 반환 (1~12)
|
||||||
|
*/
|
||||||
|
export function monthKST() {
|
||||||
|
return dayjs().tz(KST).month() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nitter 날짜 문자열 파싱
|
* Nitter 날짜 문자열 파싱
|
||||||
* 예: "Jan 15, 2026 · 10:30 PM UTC"
|
* 예: "Jan 15, 2026 · 10:30 PM UTC"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue