feat: 봇 관리 API 추가 및 타임존 수정
- 봇 관리 API 엔드포인트 추가 (routes/admin/bots.js) - GET /api/admin/bots: 봇 목록 조회 - POST /api/admin/bots/:id/start: 봇 시작 - POST /api/admin/bots/:id/stop: 봇 정지 - POST /api/admin/bots/:id/sync-all: 전체 동기화 - GET/DELETE /api/admin/bots/quota-warning: 할당량 경고 - 프론트엔드 API 엔드포인트 경로 수정 - 봇 업데이트 시간 UTC → 로컬 시간 변환 수정 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cd70fb18c9
commit
841c3c8626
4 changed files with 193 additions and 11 deletions
180
backend/src/routes/admin/bots.js
Normal file
180
backend/src/routes/admin/bots.js
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
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 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ import membersRoutes from './members/index.js';
|
||||||
import albumsRoutes from './albums/index.js';
|
import albumsRoutes from './albums/index.js';
|
||||||
import schedulesRoutes from './schedules/index.js';
|
import schedulesRoutes from './schedules/index.js';
|
||||||
import statsRoutes from './stats/index.js';
|
import statsRoutes from './stats/index.js';
|
||||||
|
import botsRoutes from './admin/bots.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 라우트 통합
|
* 라우트 통합
|
||||||
|
|
@ -23,4 +24,7 @@ export default async function routes(fastify) {
|
||||||
|
|
||||||
// 통계 라우트
|
// 통계 라우트
|
||||||
fastify.register(statsRoutes, { prefix: '/stats' });
|
fastify.register(statsRoutes, { prefix: '/stats' });
|
||||||
|
|
||||||
|
// 관리자 - 봇 라우트
|
||||||
|
fastify.register(botsRoutes, { prefix: '/admin/bots' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ export async function syncAllVideos(id) {
|
||||||
|
|
||||||
// 할당량 경고 조회
|
// 할당량 경고 조회
|
||||||
export async function getQuotaWarning() {
|
export async function getQuotaWarning() {
|
||||||
return fetchAdminApi("/api/admin/quota-warning");
|
return fetchAdminApi("/api/admin/bots/quota-warning");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 할당량 경고 해제
|
// 할당량 경고 해제
|
||||||
export async function dismissQuotaWarning() {
|
export async function dismissQuotaWarning() {
|
||||||
return fetchAdminApi("/api/admin/quota-warning", { method: "DELETE" });
|
return fetchAdminApi("/api/admin/bots/quota-warning", { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,17 +180,15 @@ function AdminScheduleBots() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 시간 포맷 (DB에 KST로 저장되어 있으므로 그대로 표시)
|
// 시간 포맷 (UTC → KST 변환)
|
||||||
const formatTime = (dateString) => {
|
const formatTime = (dateString) => {
|
||||||
if (!dateString) return '-';
|
if (!dateString) return '-';
|
||||||
// DB의 KST 시간을 UTC로 재해석하지 않도록 Z 접미사 제거
|
const date = new Date(dateString);
|
||||||
const cleanDateString = dateString.replace('Z', '').replace('T', ' ');
|
return date.toLocaleString('ko-KR', {
|
||||||
const date = new Date(cleanDateString);
|
month: 'short',
|
||||||
return date.toLocaleString('ko-KR', {
|
day: 'numeric',
|
||||||
month: 'short',
|
hour: '2-digit',
|
||||||
day: 'numeric',
|
minute: '2-digit'
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue