타임스탬프 KST 통일 및 Meilisearch 동기화 소요 시간 추가
- date.js: nowKST() 함수 추가 - 모든 타임스탬프를 UTC에서 KST(+09:00)로 변경 - scheduler.js, bots.js, x/index.js, logger.js, app.js - Meilisearch 봇에 동기화 소요 시간(ms) 추적 추가 - BotCard.jsx: 중복된 마지막 동기화 대신 소요 시간 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
95285634e9
commit
85f03cb2d8
7 changed files with 45 additions and 27 deletions
|
|
@ -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() };
|
||||
});
|
||||
|
||||
// 봇 상태 조회 엔드포인트
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,26 +144,20 @@ const BotCard = memo(function BotCard({
|
|||
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-gray-50/50">
|
||||
{bot.type === 'meilisearch' ? (
|
||||
<>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{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,
|
||||
})
|
||||
: '-'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">마지막 동기화</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{bot.last_added_count?.toLocaleString() || '-'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">동기화 수</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-lg font-bold text-gray-900">
|
||||
{bot.last_sync_duration != null
|
||||
? `${(bot.last_sync_duration / 1000).toFixed(1)}초`
|
||||
: '-'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">소요 시간</div>
|
||||
</div>
|
||||
<div className="p-3 text-center">
|
||||
<div className="text-lg font-bold text-gray-900">{bot.version || '-'}</div>
|
||||
<div className="text-xs text-gray-400">버전</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue