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

221 lines
7.2 KiB
JavaScript
Raw Normal View History

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);
}
});
}