import { fetchVideoInfo } from '../../services/youtube/api.js'; import { addOrUpdateSchedule } from '../../services/meilisearch/index.js'; const YOUTUBE_CATEGORY_ID = 2; /** * YouTube 관련 관리자 라우트 */ export default async function youtubeRoutes(fastify) { const { db, meilisearch } = fastify; /** * GET /api/admin/youtube/video-info * YouTube 영상 정보 조회 */ fastify.get('/video-info', { schema: { tags: ['admin/youtube'], summary: 'YouTube 영상 정보 조회', security: [{ bearerAuth: [] }], querystring: { type: 'object', properties: { url: { type: 'string', description: 'YouTube URL' }, }, required: ['url'], }, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { url } = request.query; // YouTube URL에서 video ID 추출 const videoId = extractVideoId(url); if (!videoId) { return reply.code(400).send({ error: '유효하지 않은 YouTube URL입니다.' }); } try { const video = await fetchVideoInfo(videoId); if (!video) { return reply.code(404).send({ error: '영상을 찾을 수 없습니다.' }); } return { videoId: video.videoId, title: video.title, channelId: video.channelId, channelName: video.channelTitle, publishedAt: video.publishedAt, date: video.date, time: video.time, videoType: video.videoType, videoUrl: video.videoUrl, }; } catch (err) { fastify.log.error(`YouTube 영상 조회 오류: ${err.message}`); return reply.code(500).send({ error: err.message }); } }); /** * POST /api/admin/youtube/schedule * YouTube 일정 저장 */ fastify.post('/schedule', { schema: { tags: ['admin/youtube'], summary: 'YouTube 일정 저장', security: [{ bearerAuth: [] }], body: { type: 'object', properties: { videoId: { type: 'string' }, title: { type: 'string' }, channelId: { type: 'string' }, channelName: { type: 'string' }, date: { type: 'string' }, time: { type: 'string' }, videoType: { type: 'string' }, }, required: ['videoId', 'title', 'date'], }, }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { videoId, title, channelId, channelName, date, time, videoType } = request.body; try { // 중복 체크 const [existing] = await db.query( 'SELECT id FROM schedule_youtube WHERE video_id = ?', [videoId] ); if (existing.length > 0) { return reply.code(409).send({ error: '이미 등록된 영상입니다.' }); } // schedules 테이블에 저장 const [result] = await db.query( 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', [YOUTUBE_CATEGORY_ID, title, date, time || null] ); const scheduleId = result.insertId; // schedule_youtube 테이블에 저장 await db.query( 'INSERT INTO schedule_youtube (schedule_id, video_id, video_type, channel_id, channel_name) VALUES (?, ?, ?, ?, ?)', [scheduleId, videoId, videoType || 'video', channelId, channelName] ); // Meilisearch 동기화 const [categoryRows] = await db.query( 'SELECT name, color FROM schedule_categories WHERE id = ?', [YOUTUBE_CATEGORY_ID] ); const category = categoryRows[0] || {}; await addOrUpdateSchedule(meilisearch, { id: scheduleId, title, date, time: time || '', category_id: YOUTUBE_CATEGORY_ID, category_name: category.name || '', category_color: category.color || '', source_name: channelName || '', }); return { success: true, scheduleId }; } catch (err) { fastify.log.error(`YouTube 일정 저장 오류: ${err.message}`); return reply.code(500).send({ error: err.message }); } }); } /** * YouTube URL에서 video ID 추출 */ function extractVideoId(url) { if (!url) return null; const patterns = [ // https://www.youtube.com/watch?v=VIDEO_ID /(?:youtube\.com\/watch\?v=)([a-zA-Z0-9_-]{11})/, // https://youtu.be/VIDEO_ID /(?:youtu\.be\/)([a-zA-Z0-9_-]{11})/, // https://www.youtube.com/shorts/VIDEO_ID /(?:youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/, // https://www.youtube.com/embed/VIDEO_ID /(?:youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/, // https://www.youtube.com/v/VIDEO_ID /(?:youtube\.com\/v\/)([a-zA-Z0-9_-]{11})/, ]; for (const pattern of patterns) { const match = url.match(pattern); if (match) { return match[1]; } } return null; }