diff --git a/backend/src/app.js b/backend/src/app.js index 9479267..ed97774 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -9,6 +9,7 @@ import multipart from '@fastify/multipart'; import rateLimit from '@fastify/rate-limit'; import config from './config/index.js'; import * as schemas from './schemas/index.js'; +import { nowKST } from './utils/date.js'; // 플러그인 import dbPlugin from './plugins/db.js'; @@ -116,7 +117,7 @@ export async function buildApp(opts = {}) { // 헬스 체크 엔드포인트 fastify.get('/api/health', async () => { - return { status: 'ok', timestamp: new Date().toISOString() }; + return { status: 'ok', timestamp: nowKST() }; }); // 봇 상태 조회 엔드포인트 diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index 0e2a1be..f7ed742 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -2,6 +2,7 @@ import fp from 'fastify-plugin'; import cron from 'node-cron'; import bots from '../config/bots.js'; import { syncWithRetry, getVersion } from '../services/meilisearch/index.js'; +import { nowKST } from '../utils/date.js'; const REDIS_PREFIX = 'bot:status:'; const TIMEZONE = 'Asia/Seoul'; @@ -14,7 +15,7 @@ async function schedulerPlugin(fastify, opts) { */ async function updateStatus(botId, status) { const current = await getStatus(botId); - const updated = { ...current, ...status, updatedAt: new Date().toISOString() }; + const updated = { ...current, ...status, updatedAt: nowKST() }; await fastify.redis.set(`${REDIS_PREFIX}${botId}`, JSON.stringify(updated)); return updated; } @@ -32,6 +33,7 @@ async function schedulerPlugin(fastify, opts) { lastCheckAt: null, lastAddedCount: 0, totalAdded: 0, + lastSyncDuration: null, errorMessage: null, }; } @@ -99,7 +101,7 @@ async function schedulerPlugin(fastify, opts) { fastify.log.info(`[${botId}] 버전 변경 없음, 체크 종료`); await updateStatus(botId, { status: 'running', - lastCheckAt: new Date().toISOString(), + lastCheckAt: nowKST(), }); } return; @@ -136,20 +138,25 @@ async function schedulerPlugin(fastify, opts) { * 동기화 실행 및 상태 업데이트 */ async function performSync(botId, newVersion, versionKey) { + const startTime = Date.now(); try { const count = await syncWithRetry(fastify.meilisearch, fastify.db); + const duration = Date.now() - startTime; await fastify.redis.set(versionKey, newVersion); await updateStatus(botId, { status: 'running', - lastCheckAt: new Date().toISOString(), + lastCheckAt: nowKST(), lastAddedCount: count, + lastSyncDuration: duration, errorMessage: null, }); - fastify.log.info(`[${botId}] 동기화 완료: ${count}개, 새 버전: ${newVersion}`); + fastify.log.info(`[${botId}] 동기화 완료: ${count}개, ${duration}ms, 새 버전: ${newVersion}`); } catch (err) { + const duration = Date.now() - startTime; await updateStatus(botId, { status: 'error', - lastCheckAt: new Date().toISOString(), + lastCheckAt: nowKST(), + lastSyncDuration: duration, errorMessage: err.message, }); fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`); @@ -163,7 +170,7 @@ async function schedulerPlugin(fastify, opts) { const { setRunningStatus = false, setErrorOnFail = false } = options; const status = await getStatus(botId); const updateData = { - lastCheckAt: new Date().toISOString(), + lastCheckAt: nowKST(), totalAdded: (status.totalAdded || 0) + result.addedCount, }; if (setRunningStatus) { @@ -213,7 +220,7 @@ async function schedulerPlugin(fastify, opts) { } catch (err) { await updateStatus(botId, { status: 'error', - lastCheckAt: new Date().toISOString(), + lastCheckAt: nowKST(), errorMessage: err.message, }); fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`); diff --git a/backend/src/routes/admin/bots.js b/backend/src/routes/admin/bots.js index 5bd1e2c..bc87438 100644 --- a/backend/src/routes/admin/bots.js +++ b/backend/src/routes/admin/bots.js @@ -2,6 +2,7 @@ import bots from '../../config/bots.js'; import { errorResponse } from '../../schemas/index.js'; import { syncAllSchedules } from '../../services/meilisearch/index.js'; import { badRequest, notFound, serverError } from '../../utils/error.js'; +import { nowKST } from '../../utils/date.js'; // 봇 관련 스키마 const botResponse = { @@ -13,6 +14,7 @@ const botResponse = { status: { type: 'string', enum: ['running', 'stopped', 'error'] }, last_check_at: { type: 'string', format: 'date-time' }, last_added_count: { type: 'integer' }, + last_sync_duration: { type: 'integer', description: '마지막 동기화 소요 시간 (ms)' }, schedules_added: { type: 'integer' }, check_interval: { type: 'integer' }, error_message: { type: 'string' }, @@ -74,6 +76,7 @@ export default async function botsRoutes(fastify) { status: status.status, last_check_at: status.lastCheckAt, last_added_count: status.lastAddedCount, + last_sync_duration: status.lastSyncDuration, schedules_added: status.totalAdded, check_interval: checkInterval, error_message: status.errorMessage, @@ -194,6 +197,7 @@ export default async function botsRoutes(fastify) { return notFound(reply, '봇을 찾을 수 없습니다.'); } + const startTime = Date.now(); try { let result; if (bot.type === 'youtube') { @@ -207,14 +211,17 @@ export default async function botsRoutes(fastify) { return badRequest(reply, '지원하지 않는 봇 타입입니다.'); } + const duration = Date.now() - startTime; + // 상태 업데이트 const status = await scheduler.getStatus(id); await fastify.redis.set(`bot:status:${id}`, JSON.stringify({ ...status, - lastCheckAt: new Date().toISOString(), + lastCheckAt: nowKST(), lastAddedCount: result.addedCount, + lastSyncDuration: duration, totalAdded: (status.totalAdded || 0) + result.addedCount, - updatedAt: new Date().toISOString(), + updatedAt: nowKST(), })); return { diff --git a/backend/src/services/x/index.js b/backend/src/services/x/index.js index 7a786fa..cbc6cbe 100644 --- a/backend/src/services/x/index.js +++ b/backend/src/services/x/index.js @@ -1,7 +1,7 @@ import fp from 'fastify-plugin'; import { fetchTweets, fetchAllTweets, extractTitle, extractYoutubeVideoIds, extractProfile } from './scraper.js'; import { fetchVideoInfo } from '../youtube/api.js'; -import { formatDate, formatTime } from '../../utils/date.js'; +import { formatDate, formatTime, nowKST } from '../../utils/date.js'; import bots from '../../config/bots.js'; import { withTransaction } from '../../utils/transaction.js'; @@ -41,7 +41,7 @@ async function xBotPlugin(fastify, opts) { username, displayName: profile.displayName, avatarUrl: profile.avatarUrl, - updatedAt: new Date().toISOString(), + updatedAt: nowKST(), }; await fastify.redis.setex( `${PROFILE_CACHE_PREFIX}${username}`, diff --git a/backend/src/utils/date.js b/backend/src/utils/date.js index 6028288..da86c6e 100644 --- a/backend/src/utils/date.js +++ b/backend/src/utils/date.js @@ -28,6 +28,14 @@ export function formatTime(date) { return dayjs(date).tz(KST).format('HH:mm:ss'); } +/** + * 현재 KST 시간을 ISO 형식으로 반환 + * 예: "2025-01-23T13:05:00+09:00" + */ +export function nowKST() { + return dayjs().tz(KST).format(); +} + /** * Nitter 날짜 문자열 파싱 * 예: "Jan 15, 2026 · 10:30 PM UTC" diff --git a/backend/src/utils/logger.js b/backend/src/utils/logger.js index 1e75bcd..90ba42e 100644 --- a/backend/src/utils/logger.js +++ b/backend/src/utils/logger.js @@ -2,6 +2,7 @@ * 로거 유틸리티 * 서비스 레이어에서 사용할 수 있는 간단한 로깅 유틸리티 */ +import { nowKST } from './date.js'; const PREFIX = { info: '[INFO]', @@ -11,7 +12,7 @@ const PREFIX = { }; function formatMessage(level, context, message) { - const timestamp = new Date().toISOString(); + const timestamp = nowKST(); return `${timestamp} ${PREFIX[level]} [${context}] ${message}`; } diff --git a/frontend/src/components/pc/admin/bot/BotCard.jsx b/frontend/src/components/pc/admin/bot/BotCard.jsx index 2cb80a2..5df1c23 100644 --- a/frontend/src/components/pc/admin/bot/BotCard.jsx +++ b/frontend/src/components/pc/admin/bot/BotCard.jsx @@ -144,26 +144,20 @@ const BotCard = memo(function BotCard({
{bot.type === 'meilisearch' ? ( <> -
-
- {bot.last_check_at - ? new Date(bot.last_check_at).toLocaleString('ko-KR', { - month: 'numeric', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false, - }) - : '-'} -
-
마지막 동기화
-
{bot.last_added_count?.toLocaleString() || '-'}
동기화 수
+
+
+ {bot.last_sync_duration != null + ? `${(bot.last_sync_duration / 1000).toFixed(1)}초` + : '-'} +
+
소요 시간
+
{bot.version || '-'}
버전