165 lines
4.7 KiB
JavaScript
165 lines
4.7 KiB
JavaScript
|
|
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;
|
||
|
|
}
|