fromis_9/backend/src/services/youtube/index.js
caadiq 2fec6c552d feat: 봇 일정 추가 시 Meilisearch 실시간 동기화
- syncScheduleById 함수 추가: 개별 일정 동기화
- YouTube 봇: 영상 추가 시 Meilisearch 동기화
- X 봇: 트윗/유튜브 링크 추가 시 Meilisearch 동기화
- description 컬럼 제거 (schedules 테이블에 없음)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:26:32 +09:00

178 lines
5 KiB
JavaScript

import fp from 'fastify-plugin';
import { fetchRecentVideos, fetchAllVideos, getUploadsPlaylistId } from './api.js';
import bots from '../../config/bots.js';
import { CATEGORY_IDS } from '../../config/index.js';
import { withTransaction } from '../../utils/transaction.js';
import { syncScheduleById } from '../meilisearch/index.js';
const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE;
const PLAYLIST_CACHE_PREFIX = 'yt_uploads:';
async function youtubeBotPlugin(fastify, opts) {
/**
* uploads playlist ID 조회 (Redis 캐싱)
*/
async function getCachedUploadsPlaylistId(channelId) {
const cacheKey = `${PLAYLIST_CACHE_PREFIX}${channelId}`;
// Redis 캐시 확인
const cached = await fastify.redis.get(cacheKey);
if (cached) {
return cached;
}
// API 호출 후 캐싱 (영구 저장 - 값이 변하지 않음)
const playlistId = await getUploadsPlaylistId(channelId);
await fastify.redis.set(cacheKey, playlistId);
return playlistId;
}
/**
* 멤버 이름 맵 조회
*/
async function getMemberNameMap() {
const [rows] = await fastify.db.query('SELECT id, name FROM members');
const map = {};
for (const r of rows) {
map[r.name] = r.id;
}
return map;
}
/**
* description에서 멤버 추출
*/
function extractMemberIds(description, memberNameMap) {
if (!description) return [];
const ids = [];
for (const [name, id] of Object.entries(memberNameMap)) {
if (description.includes(name)) {
ids.push(id);
}
}
return ids;
}
/**
* 영상을 DB에 저장
*/
async function saveVideo(video, bot) {
// 중복 체크 (video_id로) - 트랜잭션 전에 수행
const [existing] = await fastify.db.query(
'SELECT id FROM schedule_youtube WHERE video_id = ?',
[video.videoId]
);
if (existing.length > 0) {
return null;
}
// 커스텀 설정 적용
if (bot.titleFilter && !video.title.includes(bot.titleFilter)) {
return null;
}
// 멤버 이름 맵 미리 조회 (트랜잭션 전에)
let nameMap = null;
if (bot.extractMembersFromDesc) {
nameMap = await getMemberNameMap();
}
// 트랜잭션으로 INSERT 작업 수행
return withTransaction(fastify.db, async (connection) => {
// schedules 테이블에 저장
const [result] = await connection.query(
'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)',
[YOUTUBE_CATEGORY_ID, video.title, video.date, video.time]
);
const scheduleId = result.insertId;
// schedule_youtube 테이블에 저장
await connection.query(
'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)',
[scheduleId, video.videoId, video.videoType, video.channelId, bot.channelName]
);
// 멤버 연결 (커스텀 설정)
if (bot.defaultMemberId || bot.extractMembersFromDesc) {
const memberIds = [];
if (bot.defaultMemberId) {
memberIds.push(bot.defaultMemberId);
}
if (nameMap) {
memberIds.push(...extractMemberIds(video.description, nameMap));
}
if (memberIds.length > 0) {
const uniqueIds = [...new Set(memberIds)];
const values = uniqueIds.map(id => [scheduleId, id]);
await connection.query(
'INSERT INTO schedule_members (schedule_id, member_id) VALUES ?',
[values]
);
}
}
return scheduleId;
});
}
/**
* 최근 영상 동기화 (정기 실행)
*/
async function syncNewVideos(bot) {
const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId);
const videos = await fetchRecentVideos(bot.channelId, 10, uploadsPlaylistId);
let addedCount = 0;
for (const video of videos) {
const scheduleId = await saveVideo(video, bot);
if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++;
}
}
return { addedCount, total: videos.length };
}
/**
* 전체 영상 동기화 (초기화)
*/
async function syncAllVideos(bot) {
const uploadsPlaylistId = await getCachedUploadsPlaylistId(bot.channelId);
const videos = await fetchAllVideos(bot.channelId, uploadsPlaylistId);
let addedCount = 0;
for (const video of videos) {
const scheduleId = await saveVideo(video, bot);
if (scheduleId) {
// Meilisearch 동기화
await syncScheduleById(fastify.meilisearch, fastify.db, scheduleId);
addedCount++;
}
}
return { addedCount, total: videos.length };
}
/**
* 관리 중인 채널 ID 목록
*/
function getManagedChannelIds() {
return bots
.filter(b => b.type === 'youtube')
.map(b => b.channelId);
}
fastify.decorate('youtubeBot', {
syncNewVideos,
syncAllVideos,
getManagedChannelIds,
});
}
export default fp(youtubeBotPlugin, {
name: 'youtubeBot',
dependencies: ['db', 'redis'],
});