import { CATEGORY_IDS } from '../../config/index.js'; import { addOrUpdateSchedule, syncScheduleById } from '../../services/meilisearch/index.js'; import { badRequest, notFound, serverError } from '../../utils/error.js'; import { logActivity } from '../../utils/log.js'; const VARIETY_CATEGORY_ID = CATEGORY_IDS.VARIETY; /** * 예능 관련 관리자 라우트 */ export default async function varietyRoutes(fastify) { const { db, meilisearch } = fastify; /** * POST /api/admin/variety/schedule * 예능 일정 저장 */ fastify.post('/schedule', { schema: { tags: ['admin/variety'], summary: '예능 일정 저장', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { title, date, time, broadcaster, replayUrl, thumbnailUrl, memberIds } = request.body; if (!title?.trim()) { return badRequest(reply, '프로그램명은 필수입니다.'); } if (!date) { return badRequest(reply, '날짜는 필수입니다.'); } if (!broadcaster?.trim()) { return badRequest(reply, '방송사/플랫폼은 필수입니다.'); } try { // schedules 테이블 const [scheduleResult] = await db.query( 'INSERT INTO schedules (category_id, title, date, time) VALUES (?, ?, ?, ?)', [VARIETY_CATEGORY_ID, title.trim(), date, time || null] ); const scheduleId = scheduleResult.insertId; // schedule_variety 테이블 await db.query( 'INSERT INTO schedule_variety (schedule_id, broadcaster, replay_url, thumbnail_url) VALUES (?, ?, ?, ?)', [scheduleId, broadcaster.trim(), replayUrl?.trim() || null, thumbnailUrl?.trim() || null] ); // schedule_members 테이블 if (memberIds && memberIds.length > 0) { const values = memberIds.map(memberId => [scheduleId, memberId]); await db.query('INSERT INTO schedule_members (schedule_id, member_id) VALUES ?', [values]); } // Meilisearch 동기화 const [categoryRows] = await db.query( 'SELECT name, color FROM schedule_categories WHERE id = ?', [VARIETY_CATEGORY_ID] ); const category = categoryRows[0] || {}; let memberNames = ''; if (memberIds && 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(','); } await addOrUpdateSchedule(meilisearch, { id: scheduleId, title: title.trim(), date, time: time || '', category_id: VARIETY_CATEGORY_ID, category_name: category.name || '', category_color: category.color || '', member_names: memberNames, }); logActivity(db, { actor: 'admin', action: 'create', category: 'schedule', targetType: 'variety_schedule', targetId: scheduleId, summary: `예능 일정 생성: ${title.trim()}`, }); return { success: true, scheduleId }; } catch (err) { fastify.log.error(`예능 일정 저장 오류: ${err.message}`); return serverError(reply, err.message); } }); /** * PUT /api/admin/variety/schedule/:id * 예능 일정 수정 */ fastify.put('/schedule/:id', { schema: { tags: ['admin/variety'], summary: '예능 일정 수정', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; const { title, date, time, broadcaster, replayUrl, thumbnailUrl, memberIds } = request.body; if (!title?.trim()) { return badRequest(reply, '프로그램명은 필수입니다.'); } try { // 존재 확인 const [existing] = await db.query('SELECT id FROM schedules WHERE id = ?', [id]); if (existing.length === 0) { return notFound(reply, '일정을 찾을 수 없습니다.'); } // schedules 업데이트 await db.query( 'UPDATE schedules SET title = ?, date = ?, time = ? WHERE id = ?', [title.trim(), date, time || null, id] ); // schedule_variety 업데이트 (upsert) const [varietyExisting] = await db.query('SELECT schedule_id FROM schedule_variety WHERE schedule_id = ?', [id]); if (varietyExisting.length > 0) { await db.query( 'UPDATE schedule_variety SET broadcaster = ?, replay_url = ?, thumbnail_url = ? WHERE schedule_id = ?', [broadcaster?.trim() || '', replayUrl?.trim() || null, thumbnailUrl?.trim() || null, id] ); } else { await db.query( 'INSERT INTO schedule_variety (schedule_id, broadcaster, replay_url, thumbnail_url) VALUES (?, ?, ?, ?)', [id, broadcaster?.trim() || '', replayUrl?.trim() || null, thumbnailUrl?.trim() || null] ); } // 멤버 업데이트 await db.query('DELETE FROM schedule_members WHERE schedule_id = ?', [id]); if (memberIds && memberIds.length > 0) { const values = memberIds.map(memberId => [id, memberId]); await db.query('INSERT INTO schedule_members (schedule_id, member_id) VALUES ?', [values]); } // Meilisearch 동기화 await syncScheduleById(meilisearch, db, parseInt(id)); logActivity(db, { actor: 'admin', action: 'update', category: 'schedule', targetType: 'variety_schedule', targetId: parseInt(id), summary: `예능 일정 수정: ${title.trim()}`, }); return { success: true }; } catch (err) { fastify.log.error(`예능 일정 수정 오류: ${err.message}`); return serverError(reply, err.message); } }); /** * GET /api/admin/variety/schedule/:id * 예능 일정 상세 조회 (수정 폼용) */ fastify.get('/schedule/:id', { schema: { tags: ['admin/variety'], summary: '예능 일정 상세 조회', security: [{ bearerAuth: [] }], }, preHandler: [fastify.authenticate], }, async (request, reply) => { const { id } = request.params; try { const [rows] = await db.query(` SELECT s.id, s.title, s.date, s.time, sv.broadcaster, sv.replay_url, sv.thumbnail_url FROM schedules s LEFT JOIN schedule_variety sv ON s.id = sv.schedule_id WHERE s.id = ? `, [id]); if (rows.length === 0) { return notFound(reply, '일정을 찾을 수 없습니다.'); } const schedule = rows[0]; // 멤버 조회 const [memberRows] = await db.query( 'SELECT member_id FROM schedule_members WHERE schedule_id = ?', [id] ); return { id: schedule.id, title: schedule.title, date: schedule.date instanceof Date ? schedule.date.toISOString().split('T')[0] : schedule.date?.split('T')[0] || '', time: schedule.time ? schedule.time.substring(0, 5) : '', broadcaster: schedule.broadcaster || '', replayUrl: schedule.replay_url || '', thumbnailUrl: schedule.thumbnail_url || '', memberIds: memberRows.map(r => r.member_id), }; } catch (err) { fastify.log.error(`예능 일정 조회 오류: ${err.message}`); return serverError(reply, err.message); } }); }