import bots from '../../config/bots.js'; /** * 봇 관리 라우트 * 인증 필요 */ export default async function botsRoutes(fastify) { const { scheduler, redis } = fastify; const QUOTA_WARNING_KEY = 'youtube:quota_warning'; /** * GET /api/admin/bots * 봇 목록 조회 */ fastify.get('/', { schema: { tags: ['admin/bots'], summary: '봇 목록 조회', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const result = []; for (const bot of bots) { const status = await scheduler.getStatus(bot.id); // cron 표현식에서 간격 추출 (분 단위) let checkInterval = 2; // 기본값 const cronMatch = bot.cron.match(/^\*\/(\d+)/); if (cronMatch) { checkInterval = parseInt(cronMatch[1]); } result.push({ id: bot.id, name: bot.channelName || bot.username || bot.id, type: bot.type, status: status.status, last_check_at: status.lastCheckAt, last_added_count: status.lastAddedCount, schedules_added: status.totalAdded, check_interval: checkInterval, error_message: status.errorMessage, enabled: bot.enabled, }); } return result; }); /** * POST /api/admin/bots/:id/start * 봇 시작 */ fastify.post('/:id/start', { schema: { tags: ['admin/bots'], summary: '봇 시작', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; try { await scheduler.startBot(id); return { success: true, message: '봇이 시작되었습니다.' }; } catch (err) { return reply.code(400).send({ error: err.message }); } }); /** * POST /api/admin/bots/:id/stop * 봇 정지 */ fastify.post('/:id/stop', { schema: { tags: ['admin/bots'], summary: '봇 정지', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; try { await scheduler.stopBot(id); return { success: true, message: '봇이 정지되었습니다.' }; } catch (err) { return reply.code(400).send({ error: err.message }); } }); /** * POST /api/admin/bots/:id/sync-all * 전체 동기화 */ fastify.post('/:id/sync-all', { schema: { tags: ['admin/bots'], summary: '봇 전체 동기화', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; const bot = bots.find(b => b.id === id); if (!bot) { return reply.code(404).send({ error: '봇을 찾을 수 없습니다.' }); } try { let result; if (bot.type === 'youtube') { result = await fastify.youtubeBot.syncAllVideos(bot); } else if (bot.type === 'x') { result = await fastify.xBot.syncAllTweets(bot); } else { return reply.code(400).send({ error: '지원하지 않는 봇 타입입니다.' }); } // 상태 업데이트 const status = await scheduler.getStatus(id); await fastify.redis.set(`bot:status:${id}`, JSON.stringify({ ...status, lastCheckAt: new Date().toISOString(), lastAddedCount: result.addedCount, totalAdded: (status.totalAdded || 0) + result.addedCount, updatedAt: new Date().toISOString(), })); return { success: true, addedCount: result.addedCount, total: result.total, }; } catch (err) { fastify.log.error(`[${id}] 전체 동기화 오류:`, err); return reply.code(500).send({ error: err.message }); } }); /** * GET /api/admin/quota-warning * 할당량 경고 조회 */ fastify.get('/quota-warning', { schema: { tags: ['admin/bots'], summary: '할당량 경고 조회', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const data = await redis.get(QUOTA_WARNING_KEY); if (data) { return { active: true, ...JSON.parse(data) }; } return { active: false }; }); /** * DELETE /api/admin/quota-warning * 할당량 경고 해제 */ fastify.delete('/quota-warning', { schema: { tags: ['admin/bots'], summary: '할당량 경고 해제', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { await redis.del(QUOTA_WARNING_KEY); return { success: true }; }); }