2026-01-16 21:11:02 +09:00
|
|
|
import fp from 'fastify-plugin';
|
|
|
|
|
import cron from 'node-cron';
|
|
|
|
|
import bots from '../config/bots.js';
|
2026-01-27 11:59:18 +09:00
|
|
|
import { syncAllSchedules } from '../services/meilisearch/index.js';
|
2026-01-23 22:00:58 +09:00
|
|
|
import { nowKST } from '../utils/date.js';
|
2026-01-16 21:11:02 +09:00
|
|
|
|
|
|
|
|
const REDIS_PREFIX = 'bot:status:';
|
2026-01-23 21:52:01 +09:00
|
|
|
const TIMEZONE = 'Asia/Seoul';
|
2026-01-16 21:11:02 +09:00
|
|
|
|
|
|
|
|
async function schedulerPlugin(fastify, opts) {
|
|
|
|
|
const tasks = new Map();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 봇 상태 Redis에 저장
|
|
|
|
|
*/
|
|
|
|
|
async function updateStatus(botId, status) {
|
|
|
|
|
const current = await getStatus(botId);
|
2026-01-23 22:00:58 +09:00
|
|
|
const updated = { ...current, ...status, updatedAt: nowKST() };
|
2026-01-16 21:11:02 +09:00
|
|
|
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,
|
2026-01-23 22:00:58 +09:00
|
|
|
lastSyncDuration: null,
|
2026-01-16 21:11:02 +09:00
|
|
|
errorMessage: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 봇 동기화 함수 가져오기
|
|
|
|
|
*/
|
|
|
|
|
function getSyncFunction(bot) {
|
|
|
|
|
if (bot.type === 'youtube') {
|
|
|
|
|
return fastify.youtubeBot.syncNewVideos;
|
|
|
|
|
} else if (bot.type === 'x') {
|
|
|
|
|
return fastify.xBot.syncNewTweets;
|
2026-01-23 11:14:17 +09:00
|
|
|
} else if (bot.type === 'meilisearch') {
|
|
|
|
|
return async () => {
|
2026-01-27 11:59:18 +09:00
|
|
|
const count = await syncAllSchedules(fastify.meilisearch, fastify.db);
|
2026-01-23 11:14:17 +09:00
|
|
|
return { addedCount: count, total: count };
|
|
|
|
|
};
|
2026-01-16 21:11:02 +09:00
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 13:45:08 +09:00
|
|
|
/**
|
|
|
|
|
* 동기화 결과 처리 (중복 코드 제거)
|
|
|
|
|
*/
|
|
|
|
|
async function handleSyncResult(botId, result, options = {}) {
|
|
|
|
|
const { setRunningStatus = false, setErrorOnFail = false } = options;
|
|
|
|
|
const status = await getStatus(botId);
|
|
|
|
|
const updateData = {
|
2026-01-23 22:00:58 +09:00
|
|
|
lastCheckAt: nowKST(),
|
2026-01-21 13:45:08 +09:00
|
|
|
totalAdded: (status.totalAdded || 0) + result.addedCount,
|
|
|
|
|
};
|
|
|
|
|
if (setRunningStatus) {
|
|
|
|
|
updateData.status = 'running';
|
|
|
|
|
updateData.errorMessage = null;
|
|
|
|
|
}
|
|
|
|
|
if (result.addedCount > 0) {
|
|
|
|
|
updateData.lastAddedCount = result.addedCount;
|
|
|
|
|
}
|
|
|
|
|
await updateStatus(botId, updateData);
|
|
|
|
|
return result.addedCount;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 21:11:02 +09:00
|
|
|
/**
|
|
|
|
|
* 봇 시작
|
|
|
|
|
*/
|
|
|
|
|
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}`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 21:52:01 +09:00
|
|
|
// cron 태스크 등록 (한국 시간 기준)
|
2026-01-16 21:11:02 +09:00
|
|
|
const task = cron.schedule(bot.cron, async () => {
|
|
|
|
|
fastify.log.info(`[${botId}] 동기화 시작`);
|
|
|
|
|
try {
|
|
|
|
|
const result = await syncFn(bot);
|
2026-01-21 13:45:08 +09:00
|
|
|
const addedCount = await handleSyncResult(botId, result, { setRunningStatus: true });
|
|
|
|
|
fastify.log.info(`[${botId}] 동기화 완료: ${addedCount}개 추가`);
|
2026-01-16 21:11:02 +09:00
|
|
|
} catch (err) {
|
|
|
|
|
await updateStatus(botId, {
|
|
|
|
|
status: 'error',
|
2026-01-23 22:00:58 +09:00
|
|
|
lastCheckAt: nowKST(),
|
2026-01-16 21:11:02 +09:00
|
|
|
errorMessage: err.message,
|
|
|
|
|
});
|
|
|
|
|
fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`);
|
|
|
|
|
}
|
2026-01-23 21:52:01 +09:00
|
|
|
}, { timezone: TIMEZONE });
|
2026-01-16 21:11:02 +09:00
|
|
|
|
|
|
|
|
tasks.set(botId, task);
|
|
|
|
|
await updateStatus(botId, { status: 'running' });
|
|
|
|
|
fastify.log.info(`[${botId}] 스케줄 시작 (cron: ${bot.cron})`);
|
|
|
|
|
|
2026-01-23 11:14:17 +09:00
|
|
|
// 즉시 1회 실행 (meilisearch는 스케줄 시간에만 실행)
|
|
|
|
|
if (bot.type !== 'meilisearch') {
|
|
|
|
|
try {
|
|
|
|
|
const result = await syncFn(bot);
|
|
|
|
|
const addedCount = await handleSyncResult(botId, result);
|
|
|
|
|
fastify.log.info(`[${botId}] 초기 동기화 완료: ${addedCount}개 추가`);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
fastify.log.error(`[${botId}] 초기 동기화 오류: ${err.message}`);
|
|
|
|
|
}
|
2026-01-16 21:11:02 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 봇 정지
|
|
|
|
|
*/
|
|
|
|
|
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',
|
2026-01-23 11:14:17 +09:00
|
|
|
dependencies: ['db', 'redis', 'meilisearch', 'youtubeBot', 'xBot'],
|
2026-01-16 21:11:02 +09:00
|
|
|
});
|