fromis_9/backend/src/routes/admin/youtube.js

270 lines
7.9 KiB
JavaScript
Raw Normal View History

import { fetchVideoInfo } from '../../services/youtube/api.js';
import { addOrUpdateSchedule } from '../../services/meilisearch/index.js';
import { CATEGORY_IDS } from '../../config/index.js';
const YOUTUBE_CATEGORY_ID = CATEGORY_IDS.YOUTUBE;
/**
* 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 });
}
});
/**
* PUT /api/admin/youtube/schedule/:id
* YouTube 일정 수정 (멤버, 영상 유형 수정 가능)
*/
fastify.put('/schedule/:id', {
schema: {
tags: ['admin/youtube'],
summary: 'YouTube 일정 수정',
security: [{ bearerAuth: [] }],
params: {
type: 'object',
properties: {
id: { type: 'integer' },
},
required: ['id'],
},
body: {
type: 'object',
properties: {
memberIds: { type: 'array', items: { type: 'integer' } },
videoType: { type: 'string', enum: ['video', 'shorts'] },
},
},
},
preHandler: [fastify.authenticate],
}, async (request, reply) => {
const { id } = request.params;
const { memberIds = [], videoType } = request.body;
try {
// 일정 존재 확인
const [schedules] = await db.query(
'SELECT id, title, date, time FROM schedules WHERE id = ? AND category_id = ?',
[id, YOUTUBE_CATEGORY_ID]
);
if (schedules.length === 0) {
return reply.code(404).send({ error: 'YouTube 일정을 찾을 수 없습니다.' });
}
// 영상 유형 수정
if (videoType) {
await db.query(
'UPDATE schedule_youtube SET video_type = ? WHERE schedule_id = ?',
[videoType, id]
);
}
// 기존 멤버 삭제
await db.query('DELETE FROM schedule_members WHERE schedule_id = ?', [id]);
// 새 멤버 추가
if (memberIds.length > 0) {
const values = memberIds.map(memberId => [id, memberId]);
await db.query(
'INSERT INTO schedule_members (schedule_id, member_id) VALUES ?',
[values]
);
}
// 멤버 이름 조회 (Meilisearch 동기화용)
let memberNames = '';
if (memberIds.length > 0) {
const [members] = await db.query(
'SELECT name FROM members WHERE id IN (?) ORDER BY id',
[memberIds]
);
memberNames = members.map(m => m.name).join(',');
}
// YouTube 채널 정보 조회
const [youtubeInfo] = await db.query(
'SELECT channel_name FROM schedule_youtube WHERE schedule_id = ?',
[id]
);
const channelName = youtubeInfo[0]?.channel_name || '';
// 카테고리 정보 조회
const [categoryRows] = await db.query(
'SELECT name, color FROM schedule_categories WHERE id = ?',
[YOUTUBE_CATEGORY_ID]
);
const category = categoryRows[0] || {};
// Meilisearch 동기화
const schedule = schedules[0];
await addOrUpdateSchedule(meilisearch, {
id: schedule.id,
title: schedule.title,
date: schedule.date,
time: schedule.time || '',
category_id: YOUTUBE_CATEGORY_ID,
category_name: category.name || '',
category_color: category.color || '',
member_names: memberNames,
source_name: channelName,
});
return { success: true };
} 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;
}