feat: 봇 일정 추가 시 Meilisearch 실시간 동기화

- syncScheduleById 함수 추가: 개별 일정 동기화
- YouTube 봇: 영상 추가 시 Meilisearch 동기화
- X 봇: 트윗/유튜브 링크 추가 시 Meilisearch 동기화
- description 컬럼 제거 (schedules 테이블에 없음)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-25 11:26:32 +09:00
parent ea9922de00
commit 2fec6c552d
3 changed files with 71 additions and 7 deletions

View file

@ -178,7 +178,7 @@ function formatScheduleResponse(hit) {
} }
/** /**
* 일정 추가/업데이트 * 일정 추가/업데이트 (데이터 직접 전달)
*/ */
export async function addOrUpdateSchedule(meilisearch, schedule) { export async function addOrUpdateSchedule(meilisearch, schedule) {
try { try {
@ -187,7 +187,6 @@ export async function addOrUpdateSchedule(meilisearch, schedule) {
const document = { const document = {
id: schedule.id, id: schedule.id,
title: schedule.title, title: schedule.title,
description: schedule.description || '',
date: schedule.date, date: schedule.date,
time: schedule.time || '', time: schedule.time || '',
category_id: schedule.category_id, category_id: schedule.category_id,
@ -204,6 +203,59 @@ export async function addOrUpdateSchedule(meilisearch, schedule) {
} }
} }
/**
* 일정 ID로 DB에서 조회 Meilisearch에 동기화
*/
export async function syncScheduleById(meilisearch, db, scheduleId) {
try {
const [rows] = await db.query(`
SELECT
s.id,
s.title,
s.date,
s.time,
s.category_id,
c.name as category_name,
c.color as category_color,
sy.channel_name as source_name,
GROUP_CONCAT(DISTINCT m.name ORDER BY m.id SEPARATOR ',') as member_names
FROM schedules s
LEFT JOIN schedule_categories c ON s.category_id = c.id
LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id
LEFT JOIN schedule_members sm ON s.id = sm.schedule_id
LEFT JOIN members m ON sm.member_id = m.id AND m.is_former = 0
WHERE s.id = ?
GROUP BY s.id
`, [scheduleId]);
if (rows.length === 0) {
logger.warn(`일정을 찾을 수 없음: ${scheduleId}`);
return false;
}
const s = rows[0];
const document = {
id: s.id,
title: s.title,
date: s.date instanceof Date ? s.date.toISOString().split('T')[0] : 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 || '',
member_names: s.member_names || '',
};
const index = meilisearch.index(INDEX_NAME);
await index.addDocuments([document]);
logger.info(`일정 동기화: ${scheduleId}`);
return true;
} catch (err) {
logger.error(`일정 동기화 오류 (${scheduleId}): ${err.message}`);
return false;
}
}
/** /**
* 일정 삭제 * 일정 삭제
*/ */
@ -233,7 +285,6 @@ export async function syncAllSchedules(meilisearch, db) {
SELECT SELECT
s.id, s.id,
s.title, s.title,
s.description,
s.date, s.date,
s.time, s.time,
s.category_id, s.category_id,
@ -255,7 +306,6 @@ export async function syncAllSchedules(meilisearch, db) {
const documents = schedules.map(s => ({ const documents = schedules.map(s => ({
id: s.id, id: s.id,
title: s.title, title: s.title,
description: s.description || '',
date: s.date instanceof Date ? s.date.toISOString().split('T')[0] : s.date, date: s.date instanceof Date ? s.date.toISOString().split('T')[0] : s.date,
time: s.time || '', time: s.time || '',
category_id: s.category_id, category_id: s.category_id,
@ -310,7 +360,7 @@ async function recreateIndex(meilisearch) {
// 설정 복원 // 설정 복원
await index.updateSearchableAttributes([ await index.updateSearchableAttributes([
'title', 'member_names', 'description', 'source_name', 'category_name', 'title', 'member_names', 'source_name', 'category_name',
]); ]);
await index.updateFilterableAttributes(['category_id', 'date']); await index.updateFilterableAttributes(['category_id', 'date']);
await index.updateSortableAttributes(['date', 'time']); await index.updateSortableAttributes(['date', 'time']);

View file

@ -4,6 +4,7 @@ import { fetchVideoInfo } from '../youtube/api.js';
import { formatDate, formatTime, nowKST } 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';
import { syncScheduleById } from '../meilisearch/index.js';
const X_CATEGORY_ID = 3; const X_CATEGORY_ID = 3;
const YOUTUBE_CATEGORY_ID = 2; const YOUTUBE_CATEGORY_ID = 2;
@ -141,8 +142,12 @@ async function xBotPlugin(fastify, opts) {
// 관리 중인 채널이면 스킵 // 관리 중인 채널이면 스킵
if (managedChannels.includes(video.channelId)) continue; if (managedChannels.includes(video.channelId)) continue;
const saved = await saveYoutubeFromTweet(video); const scheduleId = await saveYoutubeFromTweet(video);
if (saved) addedCount++; if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++;
}
} catch (err) { } catch (err) {
fastify.log.error(`YouTube 영상 처리 오류 (${videoId}): ${err.message}`); fastify.log.error(`YouTube 영상 처리 오류 (${videoId}): ${err.message}`);
} }
@ -166,6 +171,8 @@ async function xBotPlugin(fastify, opts) {
for (const tweet of tweets) { for (const tweet of tweets) {
const scheduleId = await saveTweet(tweet); const scheduleId = await saveTweet(tweet);
if (scheduleId) { if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++; addedCount++;
// YouTube 링크 처리 // YouTube 링크 처리
ytAddedCount += await processYoutubeLinks(tweet); ytAddedCount += await processYoutubeLinks(tweet);
@ -187,6 +194,8 @@ async function xBotPlugin(fastify, opts) {
for (const tweet of tweets) { for (const tweet of tweets) {
const scheduleId = await saveTweet(tweet); const scheduleId = await saveTweet(tweet);
if (scheduleId) { if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++; addedCount++;
ytAddedCount += await processYoutubeLinks(tweet); ytAddedCount += await processYoutubeLinks(tweet);
} }

View file

@ -3,6 +3,7 @@ import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId } from './api.j
import bots from '../../config/bots.js'; import bots from '../../config/bots.js';
import { CATEGORY_IDS } from '../../config/index.js'; import { CATEGORY_IDS } from '../../config/index.js';
import { withTransaction } from '../../utils/transaction.js'; import { withTransaction } from '../../utils/transaction.js';
import { syncScheduleById } from '../meilisearch/index.js';
const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE; const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE;
const PLAYLIST_CACHE_PREFIX = 'yt_uploads:'; const PLAYLIST_CACHE_PREFIX = 'yt_uploads:';
@ -126,6 +127,8 @@ async function youtubeBotPlugin(fastify, opts) {
for (const video of videos) { for (const video of videos) {
const scheduleId = await saveVideo(video, bot); const scheduleId = await saveVideo(video, bot);
if (scheduleId) { if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++; addedCount++;
} }
} }
@ -144,6 +147,8 @@ async function youtubeBotPlugin(fastify, opts) {
for (const video of videos) { for (const video of videos) {
const scheduleId = await saveVideo(video, bot); const scheduleId = await saveVideo(video, bot);
if (scheduleId) { if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++; addedCount++;
} }
} }