From fa37891ab3b2b0c470c0bc63a5f8ecf35613d376 Mon Sep 17 00:00:00 2001 From: caadiq Date: Sat, 6 Jun 2026 22:52:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(bots):=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=EC=97=90=20=ED=99=9C=EC=84=B1=20=EC=9B=94=20=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 봇 매퍼에 activeMonths 파싱 추가. cron 콜백·즉시 실행 시 현재 KST 월이 활성 월에 포함될 때만 동기화 실행(매 실행 시점 재평가). NULL/ 빈배열/12개 전체는 항상 실행. date.js에 monthKST() 헬퍼 추가. Co-Authored-By: Claude Opus 4.7 --- backend/src/plugins/scheduler.js | 38 +++++++++++++++++++++++++++++--- backend/src/utils/date.js | 7 ++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index caa03a9..092733b 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -2,13 +2,37 @@ import fp from 'fastify-plugin'; import cron from 'node-cron'; import staticBots from '../config/bots.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'; const REDIS_PREFIX = 'bot:status:'; const TIMEZONE = 'Asia/Seoul'; 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 구성 * - err.cause(Node fetch failed의 진짜 원인 등), err.code를 함께 포함 @@ -73,6 +97,7 @@ async function schedulerPlugin(fastify, opts) { : [], extractMembersFromDesc: row.extract_members_from_desc === 1, extractMembersFromTitle: row.extract_members_from_title === 1, + activeMonths: parseActiveMonths(row.active_months), autoScheduleNext: row.auto_schedule_config ? (typeof row.auto_schedule_config === 'string' ? JSON.parse(row.auto_schedule_config) @@ -108,6 +133,7 @@ async function schedulerPlugin(fastify, opts) { includeRetweets: row.include_retweets === 1, extractYoutube: row.extract_youtube === 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), cronInterval: row.cron_interval, enabled: row.enabled === 1, + activeMonths: parseActiveMonths(row.active_months), })); } @@ -374,6 +401,11 @@ async function schedulerPlugin(fastify, opts) { // cron 태스크 등록 (한국 시간 기준) const task = cron.schedule(bot.cron, async () => { + // 활성 월이 아니면 스킵 (매 실행 시점의 현재 월로 재평가) + if (!isActiveMonth(bot)) { + fastify.log.info(`[${botId}] 비활성 월(${monthKST()}월) - 동기화 스킵`); + return; + } fastify.log.info(`[${botId}] 동기화 시작`); if (bot.weeklySchedule) { await startWeeklyBurst(botId, bot, syncFn); @@ -386,8 +418,8 @@ async function schedulerPlugin(fastify, opts) { await updateStatus(botId, { status: 'running' }); fastify.log.info(`[${botId}] 스케줄 시작 (cron: ${bot.cron})`); - // 즉시 1회 실행: meilisearch와 weekly 모드는 제외 (weekly는 지정 시각에만) - if (bot.type !== 'meilisearch' && !bot.weeklySchedule) { + // 즉시 1회 실행: meilisearch와 weekly 모드는 제외 (weekly는 지정 시각에만), 비활성 월도 제외 + if (bot.type !== 'meilisearch' && !bot.weeklySchedule && isActiveMonth(bot)) { await runSync(botId, bot, syncFn, { setRunningStatus: false }); } } diff --git a/backend/src/utils/date.js b/backend/src/utils/date.js index da86c6e..21db4ae 100644 --- a/backend/src/utils/date.js +++ b/backend/src/utils/date.js @@ -36,6 +36,13 @@ export function nowKST() { return dayjs().tz(KST).format(); } +/** + * 현재 KST 기준 월 반환 (1~12) + */ +export function monthKST() { + return dayjs().tz(KST).month() + 1; +} + /** * Nitter 날짜 문자열 파싱 * 예: "Jan 15, 2026 · 10:30 PM UTC"