fromis_9/backend/services/youtube-scheduler.js
caadiq 59e5a1d47b feat: X 봇 구현 및 봇 관리 기능 개선
- X 봇 서비스 추가 (x-bot.js)
  - Nitter를 통한 @realfromis_9 트윗 수집
  - 트윗을 일정으로 자동 저장 (카테고리 12)
  - 관리 채널 외 유튜브 링크 감지 시 별도 일정 추가
  - 1분 간격 동기화 지원

- DB 스키마 변경
  - bots.type enum 수정 (vlive, weverse 제거, x 추가)
  - bot_x_config 테이블 추가

- 봇 스케줄러 수정 (youtube-scheduler.js)
  - 봇 타입별 동기화 함수 분기 (syncBot)
  - X 봇 지원 추가

- 관리자 페이지 개선 (AdminScheduleBots.jsx)
  - 봇 타입별 아이콘 표시 (YouTube/X)
  - X 아이콘 SVG 컴포넌트 추가

- last_added_count 로직 수정
  - 추가 항목 없으면 이전 값 유지 (0으로 초기화 방지)

- 기존 X 일정에서 유튜브 영상 추출 스크립트 추가
2026-01-10 17:06:23 +09:00

198 lines
5.8 KiB
JavaScript

import cron from "node-cron";
import pool from "../lib/db.js";
import { syncNewVideos } from "./youtube-bot.js";
import { syncNewTweets } from "./x-bot.js";
// 봇별 스케줄러 인스턴스 저장
const schedulers = new Map();
/**
* 봇 타입에 따라 적절한 동기화 함수 호출
*/
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}`);
}
}
/**
* 봇이 메모리에서 실행 중인지 확인
*/
export function isBotRunning(botId) {
const id = parseInt(botId);
return schedulers.has(id);
}
/**
* 개별 봇 스케줄 등록
*/
export function registerBot(botId, intervalMinutes = 2, cronExpression = null) {
const id = parseInt(botId);
// 기존 스케줄이 있으면 제거
unregisterBot(id);
// cron 표현식: 지정된 표현식 사용, 없으면 기본값 생성
const expression = cronExpression || `1-59/${intervalMinutes} * * * *`;
const task = cron.schedule(expression, async () => {
console.log(`[Bot ${id}] 동기화 시작...`);
try {
const result = await syncBot(id);
console.log(`[Bot ${id}] 동기화 완료: ${result.addedCount}개 추가`);
} catch (error) {
console.error(`[Bot ${id}] 동기화 오류:`, error.message);
}
});
schedulers.set(id, task);
console.log(`[Bot ${id}] 스케줄 등록됨 (cron: ${expression})`);
}
/**
* 개별 봇 스케줄 해제
*/
export function unregisterBot(botId) {
const id = parseInt(botId);
if (schedulers.has(id)) {
schedulers.get(id).stop();
schedulers.delete(id);
console.log(`[Bot ${id}] 스케줄 해제됨`);
}
}
/**
* 10초 간격으로 메모리 상태와 DB status 동기화
*/
async function syncBotStatuses() {
try {
const [bots] = await pool.query("SELECT id, status FROM bots");
for (const bot of bots) {
const botId = parseInt(bot.id);
const isRunningInMemory = schedulers.has(botId);
const isRunningInDB = bot.status === "running";
// 메모리에 없는데 DB가 running이면 → 서버 크래시 등으로 불일치
// 이 경우 DB를 stopped로 변경하는 대신, 메모리에 봇을 다시 등록
if (!isRunningInMemory && isRunningInDB) {
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 {
const result = await syncBot(botId);
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`);
}
}
}
} catch (error) {
console.error("[Scheduler] 상태 동기화 오류:", error.message);
}
}
/**
* 서버 시작 시 실행 중인 봇들 스케줄 등록
*/
export async function initScheduler() {
try {
const [bots] = await pool.query(
"SELECT id, check_interval, cron_expression FROM bots WHERE status = 'running'"
);
for (const bot of bots) {
registerBot(bot.id, bot.check_interval, bot.cron_expression);
}
console.log(`[Scheduler] ${bots.length}개 봇 스케줄 등록됨`);
// 10초 간격으로 상태 동기화 (DB status와 메모리 상태 일치 유지)
setInterval(syncBotStatuses, 10000);
console.log(`[Scheduler] 10초 간격 상태 동기화 시작`);
} 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];
// 스케줄 등록 (cron_expression 우선 사용)
registerBot(botId, bot.check_interval, bot.cron_expression);
// 상태 업데이트
await pool.query(
"UPDATE bots SET status = 'running', error_message = NULL WHERE id = ?",
[botId]
);
// 즉시 1회 실행
try {
await syncBot(botId);
} 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,
isBotRunning,
};