feat: Meilisearch 동기화 봇 추가 및 시간대 관련 수정
봇 시스템: - Meilisearch 동기화 봇 추가 (meilisearch-bot.js) - bots 테이블 type enum에 meilisearch 추가 - youtube-scheduler.js에 meilisearch 봇 분기 추가 - admin.js API에서 meilisearch 봇 지원 봇 관리 페이지 개선 (AdminScheduleBots.jsx): - Meilisearch 공식 로고 아이콘 추가 - Meilisearch 봇 통계: 동기화 수/소요 시간 표시 - 봇 타입별 배경색 (X: 검정, Meilisearch: #ddf1fd, YouTube: 빨강) 시간대 정리: - MariaDB KST 설정으로 DATE_ADD(NOW(), INTERVAL 9 HOUR) → NOW() 변경 - youtube-bot.js, x-bot.js에서 10곳 수정
This commit is contained in:
parent
8780384164
commit
622839b0e8
6 changed files with 153 additions and 21 deletions
|
|
@ -11,6 +11,7 @@ import {
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { syncNewVideos, syncAllVideos } from "../services/youtube-bot.js";
|
import { syncNewVideos, syncAllVideos } from "../services/youtube-bot.js";
|
||||||
import { syncAllTweets } from "../services/x-bot.js";
|
import { syncAllTweets } from "../services/x-bot.js";
|
||||||
|
import { syncAllSchedules } from "../services/meilisearch-bot.js";
|
||||||
import { startBot, stopBot } from "../services/youtube-scheduler.js";
|
import { startBot, stopBot } from "../services/youtube-scheduler.js";
|
||||||
import {
|
import {
|
||||||
addOrUpdateSchedule,
|
addOrUpdateSchedule,
|
||||||
|
|
@ -1810,6 +1811,8 @@ router.post("/bots/:id/sync-all", authenticateToken, async (req, res) => {
|
||||||
result = await syncAllVideos(id);
|
result = await syncAllVideos(id);
|
||||||
} else if (botType === "x") {
|
} else if (botType === "x") {
|
||||||
result = await syncAllTweets(id);
|
result = await syncAllTweets(id);
|
||||||
|
} else if (botType === "meilisearch") {
|
||||||
|
result = await syncAllSchedules(id);
|
||||||
} else {
|
} else {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
|
|
|
||||||
84
backend/services/meilisearch-bot.js
Normal file
84
backend/services/meilisearch-bot.js
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* Meilisearch 동기화 봇 서비스
|
||||||
|
* 모든 일정을 Meilisearch에 동기화
|
||||||
|
*/
|
||||||
|
|
||||||
|
import pool from "../lib/db.js";
|
||||||
|
import { addOrUpdateSchedule } from "./meilisearch.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 일정 Meilisearch 동기화
|
||||||
|
*/
|
||||||
|
export async function syncAllSchedules(botId) {
|
||||||
|
try {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// 모든 일정 조회
|
||||||
|
const [schedules] = await pool.query(`
|
||||||
|
SELECT s.id, s.title, s.description, s.date, s.time,
|
||||||
|
s.category_id, s.source_url, s.source_name,
|
||||||
|
c.name as category_name, c.color as category_color
|
||||||
|
FROM schedules s
|
||||||
|
LEFT JOIN schedule_categories c ON s.category_id = c.id
|
||||||
|
`);
|
||||||
|
|
||||||
|
let synced = 0;
|
||||||
|
|
||||||
|
for (const s of schedules) {
|
||||||
|
// 멤버 조회
|
||||||
|
const [members] = await pool.query(
|
||||||
|
"SELECT m.id, m.name FROM schedule_members sm JOIN members m ON sm.member_id = m.id WHERE sm.schedule_id = ?",
|
||||||
|
[s.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Meilisearch 동기화
|
||||||
|
await addOrUpdateSchedule({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title,
|
||||||
|
description: s.description || "",
|
||||||
|
date: s.date,
|
||||||
|
time: s.time,
|
||||||
|
category_id: s.category_id,
|
||||||
|
category_name: s.category_name || "",
|
||||||
|
category_color: s.category_color || "",
|
||||||
|
source_name: s.source_name,
|
||||||
|
source_url: s.source_url,
|
||||||
|
members: members,
|
||||||
|
});
|
||||||
|
|
||||||
|
synced++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsedMs = Date.now() - startTime;
|
||||||
|
const elapsedSec = (elapsedMs / 1000).toFixed(2);
|
||||||
|
|
||||||
|
// 봇 상태 업데이트 (schedules_added = 동기화 수, last_added_count = 소요시간 ms)
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE bots SET
|
||||||
|
last_check_at = NOW(),
|
||||||
|
schedules_added = ?,
|
||||||
|
last_added_count = ?,
|
||||||
|
error_message = NULL
|
||||||
|
WHERE id = ?`,
|
||||||
|
[synced, elapsedMs, botId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[Meilisearch Bot] ${synced}개 동기화 완료 (${elapsedSec}초)`);
|
||||||
|
return { synced, elapsed: elapsedSec };
|
||||||
|
} catch (error) {
|
||||||
|
// 오류 상태 업데이트
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE bots SET
|
||||||
|
last_check_at = NOW(),
|
||||||
|
status = 'error',
|
||||||
|
error_message = ?
|
||||||
|
WHERE id = ?`,
|
||||||
|
[error.message, botId]
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
syncAllSchedules,
|
||||||
|
};
|
||||||
|
|
@ -497,7 +497,7 @@ export async function syncNewTweets(botId) {
|
||||||
if (totalAdded > 0) {
|
if (totalAdded > 0) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
schedules_added = schedules_added + ?,
|
schedules_added = schedules_added + ?,
|
||||||
last_added_count = ?,
|
last_added_count = ?,
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
|
|
@ -507,7 +507,7 @@ export async function syncNewTweets(botId) {
|
||||||
} else {
|
} else {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[botId]
|
[botId]
|
||||||
|
|
@ -519,7 +519,7 @@ export async function syncNewTweets(botId) {
|
||||||
// 오류 상태 업데이트
|
// 오류 상태 업데이트
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
status = 'error',
|
status = 'error',
|
||||||
error_message = ?
|
error_message = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
|
|
@ -590,7 +590,7 @@ export async function syncAllTweets(botId) {
|
||||||
if (totalAdded > 0) {
|
if (totalAdded > 0) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
schedules_added = schedules_added + ?,
|
schedules_added = schedules_added + ?,
|
||||||
last_added_count = ?,
|
last_added_count = ?,
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
|
|
@ -600,7 +600,7 @@ export async function syncAllTweets(botId) {
|
||||||
} else {
|
} else {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[botId]
|
[botId]
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ export async function syncNewVideos(botId) {
|
||||||
if (addedCount > 0) {
|
if (addedCount > 0) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
schedules_added = schedules_added + ?,
|
schedules_added = schedules_added + ?,
|
||||||
last_added_count = ?,
|
last_added_count = ?,
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
|
|
@ -525,7 +525,7 @@ export async function syncNewVideos(botId) {
|
||||||
} else {
|
} else {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[botId]
|
[botId]
|
||||||
|
|
@ -537,7 +537,7 @@ export async function syncNewVideos(botId) {
|
||||||
// 오류 상태 업데이트
|
// 오류 상태 업데이트
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
status = 'error',
|
status = 'error',
|
||||||
error_message = ?
|
error_message = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
|
|
@ -630,7 +630,7 @@ export async function syncAllVideos(botId) {
|
||||||
if (addedCount > 0) {
|
if (addedCount > 0) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
schedules_added = schedules_added + ?,
|
schedules_added = schedules_added + ?,
|
||||||
last_added_count = ?,
|
last_added_count = ?,
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
|
|
@ -640,7 +640,7 @@ export async function syncAllVideos(botId) {
|
||||||
} else {
|
} else {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE bots SET
|
`UPDATE bots SET
|
||||||
last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR),
|
last_check_at = NOW(),
|
||||||
error_message = NULL
|
error_message = NULL
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[botId]
|
[botId]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import cron from "node-cron";
|
||||||
import pool from "../lib/db.js";
|
import pool from "../lib/db.js";
|
||||||
import { syncNewVideos } from "./youtube-bot.js";
|
import { syncNewVideos } from "./youtube-bot.js";
|
||||||
import { syncNewTweets } from "./x-bot.js";
|
import { syncNewTweets } from "./x-bot.js";
|
||||||
|
import { syncAllSchedules } from "./meilisearch-bot.js";
|
||||||
|
|
||||||
// 봇별 스케줄러 인스턴스 저장
|
// 봇별 스케줄러 인스턴스 저장
|
||||||
const schedulers = new Map();
|
const schedulers = new Map();
|
||||||
|
|
@ -21,6 +22,8 @@ async function syncBot(botId) {
|
||||||
return await syncNewVideos(botId);
|
return await syncNewVideos(botId);
|
||||||
} else if (botType === "x") {
|
} else if (botType === "x") {
|
||||||
return await syncNewTweets(botId);
|
return await syncNewTweets(botId);
|
||||||
|
} else if (botType === "meilisearch") {
|
||||||
|
return await syncAllSchedules(botId);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`지원하지 않는 봇 타입: ${botType}`);
|
throw new Error(`지원하지 않는 봇 타입: ${botType}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,29 @@ const XIcon = ({ size = 20, fill = "currentColor" }) => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Meilisearch 아이콘 컴포넌트
|
||||||
|
const MeilisearchIcon = ({ size = 20 }) => (
|
||||||
|
<svg width={size} height={size} viewBox="0 108.4 512 295.2">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="meili-a" x1="488.157" x2="-21.055" y1="469.917" y2="179.001" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stopColor="#ff5caa"/>
|
||||||
|
<stop offset="1" stopColor="#ff4e62"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="meili-b" x1="522.305" x2="13.094" y1="410.144" y2="119.228" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stopColor="#ff5caa"/>
|
||||||
|
<stop offset="1" stopColor="#ff4e62"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="meili-c" x1="556.456" x2="47.244" y1="350.368" y2="59.452" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stopColor="#ff5caa"/>
|
||||||
|
<stop offset="1" stopColor="#ff4e62"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path d="m0 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z" fill="url(#meili-a)"/>
|
||||||
|
<path d="m138.8 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z" fill="url(#meili-b)"/>
|
||||||
|
<path d="m277.6 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z" fill="url(#meili-c)"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
function AdminScheduleBots() {
|
function AdminScheduleBots() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
|
|
@ -311,10 +334,12 @@ function AdminScheduleBots() {
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
|
||||||
bot.type === 'x' ? 'bg-black' : 'bg-red-50'
|
bot.type === 'x' ? 'bg-black' : bot.type === 'meilisearch' ? 'bg-[#ddf1fd]' : 'bg-red-50'
|
||||||
}`}>
|
}`}>
|
||||||
{bot.type === 'x' ? (
|
{bot.type === 'x' ? (
|
||||||
<XIcon size={20} fill="white" />
|
<XIcon size={20} fill="white" />
|
||||||
|
) : bot.type === 'meilisearch' ? (
|
||||||
|
<MeilisearchIcon size={20} />
|
||||||
) : (
|
) : (
|
||||||
<Youtube size={20} className="text-red-500" />
|
<Youtube size={20} className="text-red-500" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -334,16 +359,33 @@ function AdminScheduleBots() {
|
||||||
|
|
||||||
{/* 통계 정보 */}
|
{/* 통계 정보 */}
|
||||||
<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">
|
||||||
<div className="p-3 text-center">
|
{bot.type === 'meilisearch' ? (
|
||||||
<div className="text-lg font-bold text-gray-900">{bot.schedules_added}</div>
|
<>
|
||||||
<div className="text-xs text-gray-400">총 추가</div>
|
<div className="p-3 text-center">
|
||||||
</div>
|
<div className="text-lg font-bold text-gray-900">{bot.schedules_added || 0}</div>
|
||||||
<div className="p-3 text-center">
|
<div className="text-xs text-gray-400">동기화 수</div>
|
||||||
<div className={`text-lg font-bold ${bot.last_added_count > 0 ? 'text-green-500' : 'text-gray-400'}`}>
|
</div>
|
||||||
+{bot.last_added_count || 0}
|
<div className="p-3 text-center">
|
||||||
</div>
|
<div className="text-lg font-bold text-gray-900">
|
||||||
<div className="text-xs text-gray-400">마지막</div>
|
{bot.last_added_count ? `${((bot.last_added_count / 1000) || 0).toFixed(1)}초` : '-'}
|
||||||
</div>
|
</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.schedules_added}</div>
|
||||||
|
<div className="text-xs text-gray-400">총 추가</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 text-center">
|
||||||
|
<div className={`text-lg font-bold ${bot.last_added_count > 0 ? 'text-green-500' : 'text-gray-400'}`}>
|
||||||
|
+{bot.last_added_count || 0}
|
||||||
|
</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">{formatInterval(bot.check_interval)}</div>
|
<div className="text-lg font-bold text-gray-900">{formatInterval(bot.check_interval)}</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