import fp from 'fastify-plugin'; import cron from 'node-cron'; import bots from '../config/bots.js'; const REDIS_PREFIX = 'bot:status:'; async function schedulerPlugin(fastify, opts) { const tasks = new Map(); /** * 봇 상태 Redis에 저장 */ async function updateStatus(botId, status) { const current = await getStatus(botId); const updated = { ...current, ...status, updatedAt: new Date().toISOString() }; await fastify.redis.set(`${REDIS_PREFIX}${botId}`, JSON.stringify(updated)); return updated; } /** * 봇 상태 Redis에서 조회 */ async function getStatus(botId) { const data = await fastify.redis.get(`${REDIS_PREFIX}${botId}`); if (data) { return JSON.parse(data); } return { status: 'stopped', lastCheckAt: null, lastAddedCount: 0, totalAdded: 0, errorMessage: null, }; } /** * 봇 동기화 함수 가져오기 */ function getSyncFunction(bot) { if (bot.type === 'youtube') { return fastify.youtubeBot.syncNewVideos; } else if (bot.type === 'x') { return fastify.xBot.syncNewTweets; } return null; } /** * 봇 시작 */ async function startBot(botId) { const bot = bots.find(b => b.id === botId); if (!bot) { throw new Error(`봇을 찾을 수 없습니다: ${botId}`); } // 기존 태스크가 있으면 정지 if (tasks.has(botId)) { tasks.get(botId).stop(); tasks.delete(botId); } const syncFn = getSyncFunction(bot); if (!syncFn) { throw new Error(`지원하지 않는 봇 타입: ${bot.type}`); } // cron 태스크 등록 const task = cron.schedule(bot.cron, async () => { fastify.log.info(`[${botId}] 동기화 시작`); try { const result = await syncFn(bot); const status = await getStatus(botId); await updateStatus(botId, { status: 'running', lastCheckAt: new Date().toISOString(), lastAddedCount: result.addedCount, totalAdded: (status.totalAdded || 0) + result.addedCount, errorMessage: null, }); fastify.log.info(`[${botId}] 동기화 완료: ${result.addedCount}개 추가`); } catch (err) { await updateStatus(botId, { status: 'error', lastCheckAt: new Date().toISOString(), errorMessage: err.message, }); fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`); } }); tasks.set(botId, task); await updateStatus(botId, { status: 'running' }); fastify.log.info(`[${botId}] 스케줄 시작 (cron: ${bot.cron})`); // 즉시 1회 실행 try { const result = await syncFn(bot); const status = await getStatus(botId); await updateStatus(botId, { lastCheckAt: new Date().toISOString(), lastAddedCount: result.addedCount, totalAdded: (status.totalAdded || 0) + result.addedCount, }); fastify.log.info(`[${botId}] 초기 동기화 완료: ${result.addedCount}개 추가`); } catch (err) { fastify.log.error(`[${botId}] 초기 동기화 오류: ${err.message}`); } } /** * 봇 정지 */ async function stopBot(botId) { if (tasks.has(botId)) { tasks.get(botId).stop(); tasks.delete(botId); } await updateStatus(botId, { status: 'stopped' }); fastify.log.info(`[${botId}] 스케줄 정지`); } /** * 모든 활성 봇 시작 */ async function startAll() { for (const bot of bots) { if (bot.enabled) { try { await startBot(bot.id); } catch (err) { fastify.log.error(`[${bot.id}] 시작 실패: ${err.message}`); } } } } /** * 모든 봇 정지 */ async function stopAll() { for (const [botId, task] of tasks) { task.stop(); await updateStatus(botId, { status: 'stopped' }); } tasks.clear(); } // 데코레이터 등록 fastify.decorate('scheduler', { startBot, stopBot, startAll, stopAll, getStatus, getBots: () => bots, }); // 앱 종료 시 모든 봇 정지 fastify.addHook('onClose', async () => { await stopAll(); fastify.log.info('모든 봇 스케줄 정지'); }); } export default fp(schedulerPlugin, { name: 'scheduler', dependencies: ['db', 'redis', 'youtubeBot', 'xBot'], });