221 lines
7.2 KiB
JavaScript
221 lines
7.2 KiB
JavaScript
|
|
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);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|