2026-01-05 22:16:02 +09:00
|
|
|
import cron from "node-cron";
|
|
|
|
|
import pool from "../lib/db.js";
|
|
|
|
|
import { syncNewVideos } from "./youtube-bot.js";
|
2026-01-10 17:06:23 +09:00
|
|
|
import { syncNewTweets } from "./x-bot.js";
|
2026-01-05 22:16:02 +09:00
|
|
|
|
|
|
|
|
// 봇별 스케줄러 인스턴스 저장
|
|
|
|
|
const schedulers = new Map();
|
|
|
|
|
|
2026-01-10 17:06:23 +09:00
|
|
|
/**
|
|
|
|
|
* 봇 타입에 따라 적절한 동기화 함수 호출
|
|
|
|
|
*/
|
|
|
|
|
async function syncBot(botId) {
|
|
|
|
|
const [bots] = await pool.query("SELECT type FROM bots WHERE id = ?", [
|
|
|
|
|
botId,
|
|
|
|
|
]);
|
|
|
|
|
if (bots.length === 0) throw new Error("봇을 찾을 수 없습니다.");
|
|
|
|
|
|
|
|
|
|
const botType = bots[0].type;
|
|
|
|
|
|
|
|
|
|
if (botType === "youtube") {
|
|
|
|
|
return await syncNewVideos(botId);
|
|
|
|
|
} else if (botType === "x") {
|
|
|
|
|
return await syncNewTweets(botId);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`지원하지 않는 봇 타입: ${botType}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 00:27:35 +09:00
|
|
|
/**
|
|
|
|
|
* 봇이 메모리에서 실행 중인지 확인
|
|
|
|
|
*/
|
|
|
|
|
export function isBotRunning(botId) {
|
2026-01-09 19:31:11 +09:00
|
|
|
const id = parseInt(botId);
|
|
|
|
|
return schedulers.has(id);
|
2026-01-06 00:27:35 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-05 22:16:02 +09:00
|
|
|
/**
|
|
|
|
|
* 개별 봇 스케줄 등록
|
|
|
|
|
*/
|
2026-01-05 22:26:44 +09:00
|
|
|
export function registerBot(botId, intervalMinutes = 2, cronExpression = null) {
|
2026-01-09 19:31:11 +09:00
|
|
|
const id = parseInt(botId);
|
2026-01-05 22:16:02 +09:00
|
|
|
// 기존 스케줄이 있으면 제거
|
2026-01-09 19:31:11 +09:00
|
|
|
unregisterBot(id);
|
2026-01-05 22:16:02 +09:00
|
|
|
|
2026-01-05 22:26:44 +09:00
|
|
|
// cron 표현식: 지정된 표현식 사용, 없으면 기본값 생성
|
|
|
|
|
const expression = cronExpression || `1-59/${intervalMinutes} * * * *`;
|
2026-01-05 22:16:02 +09:00
|
|
|
|
2026-01-05 22:26:44 +09:00
|
|
|
const task = cron.schedule(expression, async () => {
|
2026-01-09 19:31:11 +09:00
|
|
|
console.log(`[Bot ${id}] 동기화 시작...`);
|
2026-01-05 22:16:02 +09:00
|
|
|
try {
|
2026-01-10 17:06:23 +09:00
|
|
|
const result = await syncBot(id);
|
2026-01-09 19:31:11 +09:00
|
|
|
console.log(`[Bot ${id}] 동기화 완료: ${result.addedCount}개 추가`);
|
2026-01-05 22:16:02 +09:00
|
|
|
} catch (error) {
|
2026-01-09 19:31:11 +09:00
|
|
|
console.error(`[Bot ${id}] 동기화 오류:`, error.message);
|
2026-01-05 22:16:02 +09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-09 19:31:11 +09:00
|
|
|
schedulers.set(id, task);
|
|
|
|
|
console.log(`[Bot ${id}] 스케줄 등록됨 (cron: ${expression})`);
|
2026-01-05 22:16:02 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 개별 봇 스케줄 해제
|
|
|
|
|
*/
|
|
|
|
|
export function unregisterBot(botId) {
|
2026-01-09 19:31:11 +09:00
|
|
|
const id = parseInt(botId);
|
|
|
|
|
if (schedulers.has(id)) {
|
|
|
|
|
schedulers.get(id).stop();
|
|
|
|
|
schedulers.delete(id);
|
|
|
|
|
console.log(`[Bot ${id}] 스케줄 해제됨`);
|
2026-01-05 22:16:02 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 00:27:35 +09:00
|
|
|
/**
|
|
|
|
|
* 10초 간격으로 메모리 상태와 DB status 동기화
|
|
|
|
|
*/
|
|
|
|
|
async function syncBotStatuses() {
|
|
|
|
|
try {
|
|
|
|
|
const [bots] = await pool.query("SELECT id, status FROM bots");
|
|
|
|
|
|
|
|
|
|
for (const bot of bots) {
|
2026-01-09 19:37:29 +09:00
|
|
|
const botId = parseInt(bot.id);
|
|
|
|
|
const isRunningInMemory = schedulers.has(botId);
|
2026-01-06 00:27:35 +09:00
|
|
|
const isRunningInDB = bot.status === "running";
|
|
|
|
|
|
2026-01-09 19:37:29 +09:00
|
|
|
// 메모리에 없는데 DB가 running이면 → 서버 크래시 등으로 불일치
|
|
|
|
|
// 이 경우 DB를 stopped로 변경하는 대신, 메모리에 봇을 다시 등록
|
2026-01-06 00:27:35 +09:00
|
|
|
if (!isRunningInMemory && isRunningInDB) {
|
2026-01-09 19:37:29 +09:00
|
|
|
console.log(`[Scheduler] Bot ${botId} 메모리에 없음, 재등록 시도...`);
|
|
|
|
|
try {
|
|
|
|
|
const [botInfo] = await pool.query(
|
|
|
|
|
"SELECT check_interval, cron_expression FROM bots WHERE id = ?",
|
|
|
|
|
[botId]
|
|
|
|
|
);
|
|
|
|
|
if (botInfo.length > 0) {
|
|
|
|
|
const { check_interval, cron_expression } = botInfo[0];
|
|
|
|
|
// 직접 registerBot 함수 호출 (import 순환 방지를 위해 내부 로직 사용)
|
|
|
|
|
const expression =
|
|
|
|
|
cron_expression || `1-59/${check_interval} * * * *`;
|
|
|
|
|
const task = cron.schedule(expression, async () => {
|
|
|
|
|
console.log(`[Bot ${botId}] 동기화 시작...`);
|
|
|
|
|
try {
|
2026-01-10 17:06:23 +09:00
|
|
|
const result = await syncBot(botId);
|
2026-01-09 19:37:29 +09:00
|
|
|
console.log(
|
|
|
|
|
`[Bot ${botId}] 동기화 완료: ${result.addedCount}개 추가`
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[Bot ${botId}] 동기화 오류:`, error.message);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
schedulers.set(botId, task);
|
|
|
|
|
console.log(
|
|
|
|
|
`[Scheduler] Bot ${botId} 재등록 완료 (cron: ${expression})`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[Scheduler] Bot ${botId} 재등록 오류:`, error.message);
|
|
|
|
|
// 재등록 실패 시에만 stopped로 변경
|
|
|
|
|
await pool.query("UPDATE bots SET status = 'stopped' WHERE id = ?", [
|
|
|
|
|
botId,
|
|
|
|
|
]);
|
|
|
|
|
console.log(`[Scheduler] Bot ${botId} 상태 동기화: stopped`);
|
|
|
|
|
}
|
2026-01-06 00:27:35 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[Scheduler] 상태 동기화 오류:", error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 22:16:02 +09:00
|
|
|
/**
|
|
|
|
|
* 서버 시작 시 실행 중인 봇들 스케줄 등록
|
|
|
|
|
*/
|
|
|
|
|
export async function initScheduler() {
|
|
|
|
|
try {
|
|
|
|
|
const [bots] = await pool.query(
|
2026-01-05 22:26:44 +09:00
|
|
|
"SELECT id, check_interval, cron_expression FROM bots WHERE status = 'running'"
|
2026-01-05 22:16:02 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const bot of bots) {
|
2026-01-05 22:26:44 +09:00
|
|
|
registerBot(bot.id, bot.check_interval, bot.cron_expression);
|
2026-01-05 22:16:02 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[Scheduler] ${bots.length}개 봇 스케줄 등록됨`);
|
2026-01-06 00:27:35 +09:00
|
|
|
|
|
|
|
|
// 10초 간격으로 상태 동기화 (DB status와 메모리 상태 일치 유지)
|
|
|
|
|
setInterval(syncBotStatuses, 10000);
|
|
|
|
|
console.log(`[Scheduler] 10초 간격 상태 동기화 시작`);
|
2026-01-05 22:16:02 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[Scheduler] 초기화 오류:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 봇 시작
|
|
|
|
|
*/
|
|
|
|
|
export async function startBot(botId) {
|
|
|
|
|
const [bots] = await pool.query("SELECT * FROM bots WHERE id = ?", [botId]);
|
|
|
|
|
if (bots.length === 0) {
|
|
|
|
|
throw new Error("봇을 찾을 수 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bot = bots[0];
|
|
|
|
|
|
2026-01-05 22:26:44 +09:00
|
|
|
// 스케줄 등록 (cron_expression 우선 사용)
|
|
|
|
|
registerBot(botId, bot.check_interval, bot.cron_expression);
|
2026-01-05 22:16:02 +09:00
|
|
|
|
|
|
|
|
// 상태 업데이트
|
|
|
|
|
await pool.query(
|
|
|
|
|
"UPDATE bots SET status = 'running', error_message = NULL WHERE id = ?",
|
|
|
|
|
[botId]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 즉시 1회 실행
|
|
|
|
|
try {
|
2026-01-10 17:06:23 +09:00
|
|
|
await syncBot(botId);
|
2026-01-05 22:16:02 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`[Bot ${botId}] 초기 동기화 오류:`, error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 봇 정지
|
|
|
|
|
*/
|
|
|
|
|
export async function stopBot(botId) {
|
|
|
|
|
unregisterBot(botId);
|
|
|
|
|
|
|
|
|
|
await pool.query("UPDATE bots SET status = 'stopped' WHERE id = ?", [botId]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
initScheduler,
|
|
|
|
|
registerBot,
|
|
|
|
|
unregisterBot,
|
|
|
|
|
startBot,
|
|
|
|
|
stopBot,
|
2026-01-06 00:27:35 +09:00
|
|
|
isBotRunning,
|
2026-01-05 22:16:02 +09:00
|
|
|
};
|