From 622839b0e83d5bcfebf429fdfa315e57f671f45e Mon Sep 17 00:00:00 2001 From: caadiq Date: Sat, 10 Jan 2026 18:59:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Meilisearch=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=B4=87=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EB=8C=80=20=EA=B4=80=EB=A0=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 봇 시스템: - 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곳 수정 --- backend/routes/admin.js | 3 + backend/services/meilisearch-bot.js | 84 +++++++++++++++++++ backend/services/x-bot.js | 10 +-- backend/services/youtube-bot.js | 10 +-- backend/services/youtube-scheduler.js | 3 + .../src/pages/pc/admin/AdminScheduleBots.jsx | 64 +++++++++++--- 6 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 backend/services/meilisearch-bot.js diff --git a/backend/routes/admin.js b/backend/routes/admin.js index 2908782..cb3cba6 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -11,6 +11,7 @@ import { import pool from "../lib/db.js"; import { syncNewVideos, syncAllVideos } from "../services/youtube-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 { addOrUpdateSchedule, @@ -1810,6 +1811,8 @@ router.post("/bots/:id/sync-all", authenticateToken, async (req, res) => { result = await syncAllVideos(id); } else if (botType === "x") { result = await syncAllTweets(id); + } else if (botType === "meilisearch") { + result = await syncAllSchedules(id); } else { return res .status(400) diff --git a/backend/services/meilisearch-bot.js b/backend/services/meilisearch-bot.js new file mode 100644 index 0000000..0bec668 --- /dev/null +++ b/backend/services/meilisearch-bot.js @@ -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, +}; diff --git a/backend/services/x-bot.js b/backend/services/x-bot.js index 5b9edba..9c1e851 100644 --- a/backend/services/x-bot.js +++ b/backend/services/x-bot.js @@ -497,7 +497,7 @@ export async function syncNewTweets(botId) { if (totalAdded > 0) { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), schedules_added = schedules_added + ?, last_added_count = ?, error_message = NULL @@ -507,7 +507,7 @@ export async function syncNewTweets(botId) { } else { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), error_message = NULL WHERE id = ?`, [botId] @@ -519,7 +519,7 @@ export async function syncNewTweets(botId) { // 오류 상태 업데이트 await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), status = 'error', error_message = ? WHERE id = ?`, @@ -590,7 +590,7 @@ export async function syncAllTweets(botId) { if (totalAdded > 0) { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), schedules_added = schedules_added + ?, last_added_count = ?, error_message = NULL @@ -600,7 +600,7 @@ export async function syncAllTweets(botId) { } else { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), error_message = NULL WHERE id = ?`, [botId] diff --git a/backend/services/youtube-bot.js b/backend/services/youtube-bot.js index 2d40fd9..43db427 100644 --- a/backend/services/youtube-bot.js +++ b/backend/services/youtube-bot.js @@ -515,7 +515,7 @@ export async function syncNewVideos(botId) { if (addedCount > 0) { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), schedules_added = schedules_added + ?, last_added_count = ?, error_message = NULL @@ -525,7 +525,7 @@ export async function syncNewVideos(botId) { } else { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), error_message = NULL WHERE id = ?`, [botId] @@ -537,7 +537,7 @@ export async function syncNewVideos(botId) { // 오류 상태 업데이트 await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), status = 'error', error_message = ? WHERE id = ?`, @@ -630,7 +630,7 @@ export async function syncAllVideos(botId) { if (addedCount > 0) { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), schedules_added = schedules_added + ?, last_added_count = ?, error_message = NULL @@ -640,7 +640,7 @@ export async function syncAllVideos(botId) { } else { await pool.query( `UPDATE bots SET - last_check_at = DATE_ADD(NOW(), INTERVAL 9 HOUR), + last_check_at = NOW(), error_message = NULL WHERE id = ?`, [botId] diff --git a/backend/services/youtube-scheduler.js b/backend/services/youtube-scheduler.js index d92ed50..3b6db07 100644 --- a/backend/services/youtube-scheduler.js +++ b/backend/services/youtube-scheduler.js @@ -2,6 +2,7 @@ import cron from "node-cron"; import pool from "../lib/db.js"; import { syncNewVideos } from "./youtube-bot.js"; import { syncNewTweets } from "./x-bot.js"; +import { syncAllSchedules } from "./meilisearch-bot.js"; // 봇별 스케줄러 인스턴스 저장 const schedulers = new Map(); @@ -21,6 +22,8 @@ async function syncBot(botId) { return await syncNewVideos(botId); } else if (botType === "x") { return await syncNewTweets(botId); + } else if (botType === "meilisearch") { + return await syncAllSchedules(botId); } else { throw new Error(`지원하지 않는 봇 타입: ${botType}`); } diff --git a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx index 5b092ca..9bc1b63 100644 --- a/frontend/src/pages/pc/admin/AdminScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/AdminScheduleBots.jsx @@ -18,6 +18,29 @@ const XIcon = ({ size = 20, fill = "currentColor" }) => ( ); +// Meilisearch 아이콘 컴포넌트 +const MeilisearchIcon = ({ size = 20 }) => ( + + + + + + + + + + + + + + + + + + + +); + function AdminScheduleBots() { const navigate = useNavigate(); const [user, setUser] = useState(null); @@ -311,10 +334,12 @@ function AdminScheduleBots() {
{bot.type === 'x' ? ( + ) : bot.type === 'meilisearch' ? ( + ) : ( )} @@ -334,16 +359,33 @@ function AdminScheduleBots() { {/* 통계 정보 */}
-
-
{bot.schedules_added}
-
총 추가
-
-
-
0 ? 'text-green-500' : 'text-gray-400'}`}> - +{bot.last_added_count || 0} -
-
마지막
-
+ {bot.type === 'meilisearch' ? ( + <> +
+
{bot.schedules_added || 0}
+
동기화 수
+
+
+
+ {bot.last_added_count ? `${((bot.last_added_count / 1000) || 0).toFixed(1)}초` : '-'} +
+
소요 시간
+
+ + ) : ( + <> +
+
{bot.schedules_added}
+
총 추가
+
+
+
0 ? 'text-green-500' : 'text-gray-400'}`}> + +{bot.last_added_count || 0} +
+
마지막
+
+ + )}
{formatInterval(bot.check_interval)}
업데이트 간격