타임스탬프 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 rateLimit from '@fastify/rate-limit';
|
||||||
import config from './config/index.js';
|
import config from './config/index.js';
|
||||||
import * as schemas from './schemas/index.js';
|
import * as schemas from './schemas/index.js';
|
||||||
|
import { nowKST } from './utils/date.js';
|
||||||
|
|
||||||
// 플러그인
|
// 플러그인
|
||||||
import dbPlugin from './plugins/db.js';
|
import dbPlugin from './plugins/db.js';
|
||||||
|
|
@ -116,7 +117,7 @@ export async function buildApp(opts = {}) {
|
||||||
|
|
||||||
// 헬스 체크 엔드포인트
|
// 헬스 체크 엔드포인트
|
||||||
fastify.get('/api/health', async () => {
|
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 cron from 'node-cron';
|
||||||
import bots from '../config/bots.js';
|
import bots from '../config/bots.js';
|
||||||
import { syncWithRetry, getVersion } from '../services/meilisearch/index.js';
|
import { syncWithRetry, getVersion } from '../services/meilisearch/index.js';
|
||||||
|
import { nowKST } from '../utils/date.js';
|
||||||
|
|
||||||
const REDIS_PREFIX = 'bot:status:';
|
const REDIS_PREFIX = 'bot:status:';
|
||||||
const TIMEZONE = 'Asia/Seoul';
|
const TIMEZONE = 'Asia/Seoul';
|
||||||
|
|
@ -14,7 +15,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
*/
|
*/
|
||||||
async function updateStatus(botId, status) {
|
async function updateStatus(botId, status) {
|
||||||
const current = await getStatus(botId);
|
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));
|
await fastify.redis.set(`${REDIS_PREFIX}${botId}`, JSON.stringify(updated));
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +33,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
lastCheckAt: null,
|
lastCheckAt: null,
|
||||||
lastAddedCount: 0,
|
lastAddedCount: 0,
|
||||||
totalAdded: 0,
|
totalAdded: 0,
|
||||||
|
lastSyncDuration: null,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +101,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
fastify.log.info(`[${botId}] 버전 변경 없음, 체크 종료`);
|
fastify.log.info(`[${botId}] 버전 변경 없음, 체크 종료`);
|
||||||
await updateStatus(botId, {
|
await updateStatus(botId, {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
lastCheckAt: new Date().toISOString(),
|
lastCheckAt: nowKST(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -136,20 +138,25 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
* 동기화 실행 및 상태 업데이트
|
* 동기화 실행 및 상태 업데이트
|
||||||
*/
|
*/
|
||||||
async function performSync(botId, newVersion, versionKey) {
|
async function performSync(botId, newVersion, versionKey) {
|
||||||
|
const startTime = Date.now();
|
||||||
try {
|
try {
|
||||||
const count = await syncWithRetry(fastify.meilisearch, fastify.db);
|
const count = await syncWithRetry(fastify.meilisearch, fastify.db);
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
await fastify.redis.set(versionKey, newVersion);
|
await fastify.redis.set(versionKey, newVersion);
|
||||||
await updateStatus(botId, {
|
await updateStatus(botId, {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
lastCheckAt: new Date().toISOString(),
|
lastCheckAt: nowKST(),
|
||||||
lastAddedCount: count,
|
lastAddedCount: count,
|
||||||
|
lastSyncDuration: duration,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
});
|
});
|
||||||
fastify.log.info(`[${botId}] 동기화 완료: ${count}개, 새 버전: ${newVersion}`);
|
fastify.log.info(`[${botId}] 동기화 완료: ${count}개, ${duration}ms, 새 버전: ${newVersion}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
await updateStatus(botId, {
|
await updateStatus(botId, {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
lastCheckAt: new Date().toISOString(),
|
lastCheckAt: nowKST(),
|
||||||
|
lastSyncDuration: duration,
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
});
|
});
|
||||||
fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`);
|
fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`);
|
||||||
|
|
@ -163,7 +170,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
const { setRunningStatus = false, setErrorOnFail = false } = options;
|
const { setRunningStatus = false, setErrorOnFail = false } = options;
|
||||||
const status = await getStatus(botId);
|
const status = await getStatus(botId);
|
||||||
const updateData = {
|
const updateData = {
|
||||||
lastCheckAt: new Date().toISOString(),
|
lastCheckAt: nowKST(),
|
||||||
totalAdded: (status.totalAdded || 0) + result.addedCount,
|
totalAdded: (status.totalAdded || 0) + result.addedCount,
|
||||||
};
|
};
|
||||||
if (setRunningStatus) {
|
if (setRunningStatus) {
|
||||||
|
|
@ -213,7 +220,7 @@ async function schedulerPlugin(fastify, opts) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await updateStatus(botId, {
|
await updateStatus(botId, {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
lastCheckAt: new Date().toISOString(),
|
lastCheckAt: nowKST(),
|
||||||
errorMessage: err.message,
|
errorMessage: err.message,
|
||||||
});
|
});
|
||||||
fastify.log.error(`[${botId}] 동기화 오류: ${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 { errorResponse } from '../../schemas/index.js';
|
||||||
import { syncAllSchedules } from '../../services/meilisearch/index.js';
|
import { syncAllSchedules } from '../../services/meilisearch/index.js';
|
||||||
import { badRequest, notFound, serverError } from '../../utils/error.js';
|
import { badRequest, notFound, serverError } from '../../utils/error.js';
|
||||||
|
import { nowKST } from '../../utils/date.js';
|
||||||
|
|
||||||
// 봇 관련 스키마
|
// 봇 관련 스키마
|
||||||
const botResponse = {
|
const botResponse = {
|
||||||
|
|
@ -13,6 +14,7 @@ const botResponse = {
|
||||||
status: { type: 'string', enum: ['running', 'stopped', 'error'] },
|
status: { type: 'string', enum: ['running', 'stopped', 'error'] },
|
||||||
last_check_at: { type: 'string', format: 'date-time' },
|
last_check_at: { type: 'string', format: 'date-time' },
|
||||||
last_added_count: { type: 'integer' },
|
last_added_count: { type: 'integer' },
|
||||||
|
last_sync_duration: { type: 'integer', description: '마지막 동기화 소요 시간 (ms)' },
|
||||||
schedules_added: { type: 'integer' },
|
schedules_added: { type: 'integer' },
|
||||||
check_interval: { type: 'integer' },
|
check_interval: { type: 'integer' },
|
||||||
error_message: { type: 'string' },
|
error_message: { type: 'string' },
|
||||||
|
|
@ -74,6 +76,7 @@ export default async function botsRoutes(fastify) {
|
||||||
status: status.status,
|
status: status.status,
|
||||||
last_check_at: status.lastCheckAt,
|
last_check_at: status.lastCheckAt,
|
||||||
last_added_count: status.lastAddedCount,
|
last_added_count: status.lastAddedCount,
|
||||||
|
last_sync_duration: status.lastSyncDuration,
|
||||||
schedules_added: status.totalAdded,
|
schedules_added: status.totalAdded,
|
||||||
check_interval: checkInterval,
|
check_interval: checkInterval,
|
||||||
error_message: status.errorMessage,
|
error_message: status.errorMessage,
|
||||||
|
|
@ -194,6 +197,7 @@ export default async function botsRoutes(fastify) {
|
||||||
return notFound(reply, '봇을 찾을 수 없습니다.');
|
return notFound(reply, '봇을 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
if (bot.type === 'youtube') {
|
if (bot.type === 'youtube') {
|
||||||
|
|
@ -207,14 +211,17 @@ export default async function botsRoutes(fastify) {
|
||||||
return badRequest(reply, '지원하지 않는 봇 타입입니다.');
|
return badRequest(reply, '지원하지 않는 봇 타입입니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
// 상태 업데이트
|
// 상태 업데이트
|
||||||
const status = await scheduler.getStatus(id);
|
const status = await scheduler.getStatus(id);
|
||||||
await fastify.redis.set(`bot:status:${id}`, JSON.stringify({
|
await fastify.redis.set(`bot:status:${id}`, JSON.stringify({
|
||||||
...status,
|
...status,
|
||||||
lastCheckAt: new Date().toISOString(),
|
lastCheckAt: nowKST(),
|
||||||
lastAddedCount: result.addedCount,
|
lastAddedCount: result.addedCount,
|
||||||
|
lastSyncDuration: duration,
|
||||||
totalAdded: (status.totalAdded || 0) + result.addedCount,
|
totalAdded: (status.totalAdded || 0) + result.addedCount,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: nowKST(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fp from 'fastify-plugin';
|
import fp from 'fastify-plugin';
|
||||||
import { fetchTweets, fetchAllTweets, extractTitle, extractYoutubeVideoIds, extractProfile } from './scraper.js';
|
import { fetchTweets, fetchAllTweets, extractTitle, extractYoutubeVideoIds, extractProfile } from './scraper.js';
|
||||||
import { fetchVideoInfo } from '../youtube/api.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 bots from '../../config/bots.js';
|
||||||
import { withTransaction } from '../../utils/transaction.js';
|
import { withTransaction } from '../../utils/transaction.js';
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ async function xBotPlugin(fastify, opts) {
|
||||||
username,
|
username,
|
||||||
displayName: profile.displayName,
|
displayName: profile.displayName,
|
||||||
avatarUrl: profile.avatarUrl,
|
avatarUrl: profile.avatarUrl,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: nowKST(),
|
||||||
};
|
};
|
||||||
await fastify.redis.setex(
|
await fastify.redis.setex(
|
||||||
`${PROFILE_CACHE_PREFIX}${username}`,
|
`${PROFILE_CACHE_PREFIX}${username}`,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,14 @@ export function formatTime(date) {
|
||||||
return dayjs(date).tz(KST).format('HH:mm:ss');
|
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 날짜 문자열 파싱
|
* Nitter 날짜 문자열 파싱
|
||||||
* 예: "Jan 15, 2026 · 10:30 PM UTC"
|
* 예: "Jan 15, 2026 · 10:30 PM UTC"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
* 로거 유틸리티
|
* 로거 유틸리티
|
||||||
* 서비스 레이어에서 사용할 수 있는 간단한 로깅 유틸리티
|
* 서비스 레이어에서 사용할 수 있는 간단한 로깅 유틸리티
|
||||||
*/
|
*/
|
||||||
|
import { nowKST } from './date.js';
|
||||||
|
|
||||||
const PREFIX = {
|
const PREFIX = {
|
||||||
info: '[INFO]',
|
info: '[INFO]',
|
||||||
|
|
@ -11,7 +12,7 @@ const PREFIX = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatMessage(level, context, message) {
|
function formatMessage(level, context, message) {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = nowKST();
|
||||||
return `${timestamp} ${PREFIX[level]} [${context}] ${message}`;
|
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">
|
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-gray-50/50">
|
||||||
{bot.type === 'meilisearch' ? (
|
{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="p-3 text-center">
|
||||||
<div className="text-lg font-bold text-gray-900">
|
<div className="text-lg font-bold text-gray-900">
|
||||||
{bot.last_added_count?.toLocaleString() || '-'}
|
{bot.last_added_count?.toLocaleString() || '-'}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400">동기화 수</div>
|
<div className="text-xs text-gray-400">동기화 수</div>
|
||||||
</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="p-3 text-center">
|
||||||
<div className="text-lg font-bold text-gray-900">{bot.version || '-'}</div>
|
<div className="text-lg font-bold text-gray-900">{bot.version || '-'}</div>
|
||||||
<div className="text-xs text-gray-400">버전</div>
|
<div className="text-xs text-gray-400">버전</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue