feat(admin-schedule): 일반 일정 생성/수정 라우트 추가 (컴백·팬사인회·기타)
전용 폼이 없는 단순 카테고리용 POST/PUT /admin/schedules 신규. 제목·날짜·시간·카테고리·멤버 + date_precision(월만=날짜 미정) 처리, month이면 날짜를 해당 월 1일로 정규화. schedules + schedule_members 트랜잭션 + meili 동기화 + 월별 캐시 무효화. (앨범→컴백 카테고리 리네임 동반) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ad20149c88
commit
a11a027682
2 changed files with 128 additions and 0 deletions
124
backend/src/routes/admin/schedules.js
Normal file
124
backend/src/routes/admin/schedules.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* 일반 일정 생성/수정 라우트 (전용 폼이 없는 단순 카테고리용 — 컴백·팬사인회·기타)
|
||||
* 제목·날짜·시간·멤버 + date_precision(컴백의 "날짜 미정"). 이미지/장소/설명은 미지원.
|
||||
*/
|
||||
import { errorResponse } from '../../schemas/index.js';
|
||||
import { badRequest, notFound } from '../../utils/error.js';
|
||||
import { logActivity } from '../../utils/log.js';
|
||||
import { withTransaction } from '../../utils/transaction.js';
|
||||
import { syncScheduleById } from '../../services/meilisearch/index.js';
|
||||
|
||||
const scheduleBody = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
date: { type: 'string' }, // YYYY-MM-DD
|
||||
time: { type: ['string', 'null'] },
|
||||
category: { type: 'integer' },
|
||||
datePrecision: { type: 'string', enum: ['day', 'month'], default: 'day' },
|
||||
members: { type: 'array', items: { type: 'integer' }, default: [] },
|
||||
},
|
||||
required: ['title', 'date', 'category'],
|
||||
};
|
||||
|
||||
/**
|
||||
* date_precision이 month면 날짜를 해당 월 1일로 정규화
|
||||
*/
|
||||
function normalizeDate(date, precision) {
|
||||
if (precision === 'month' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
||||
return `${date.slice(0, 7)}-01`;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
export default async function schedulesAdminRoutes(fastify) {
|
||||
const { db, meilisearch, redis } = fastify;
|
||||
|
||||
/**
|
||||
* POST /api/admin/schedules — 일정 생성
|
||||
*/
|
||||
fastify.post('/', {
|
||||
schema: {
|
||||
tags: ['admin/schedules'],
|
||||
summary: '일반 일정 생성',
|
||||
security: [{ bearerAuth: [] }],
|
||||
body: scheduleBody,
|
||||
response: { 201: { type: 'object', additionalProperties: true }, 400: errorResponse },
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { title, date, time = null, category, datePrecision = 'day', members = [] } = request.body;
|
||||
if (!title?.trim()) return badRequest(reply, '제목은 필수입니다.');
|
||||
if (!date) return badRequest(reply, '날짜는 필수입니다.');
|
||||
|
||||
const finalDate = normalizeDate(date, datePrecision);
|
||||
|
||||
const scheduleId = await withTransaction(db, async (conn) => {
|
||||
const [result] = await conn.query(
|
||||
'INSERT INTO schedules (category_id, title, date, time, date_precision) VALUES (?, ?, ?, ?, ?)',
|
||||
[category, title.trim(), finalDate, time || null, datePrecision]
|
||||
);
|
||||
const sid = result.insertId;
|
||||
if (members.length > 0) {
|
||||
await conn.query('INSERT INTO schedule_members (schedule_id, member_id) VALUES ?',
|
||||
[members.map((m) => [sid, m])]);
|
||||
}
|
||||
return sid;
|
||||
});
|
||||
|
||||
await syncScheduleById(meilisearch, db, scheduleId, redis);
|
||||
logActivity(db, {
|
||||
actor: 'admin', action: 'create', category: 'schedule',
|
||||
targetType: 'schedule', targetId: scheduleId,
|
||||
summary: `일정 생성: ${title.trim()}`,
|
||||
});
|
||||
|
||||
reply.code(201);
|
||||
return { success: true, scheduleId };
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /api/admin/schedules/:id — 일정 수정
|
||||
*/
|
||||
fastify.put('/:id', {
|
||||
schema: {
|
||||
tags: ['admin/schedules'],
|
||||
summary: '일반 일정 수정',
|
||||
security: [{ bearerAuth: [] }],
|
||||
params: { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] },
|
||||
body: scheduleBody,
|
||||
response: { 200: { type: 'object', additionalProperties: true }, 404: errorResponse },
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params;
|
||||
const { title, date, time = null, category, datePrecision = 'day', members = [] } = request.body;
|
||||
if (!title?.trim()) return badRequest(reply, '제목은 필수입니다.');
|
||||
|
||||
const [existing] = await db.query('SELECT id FROM schedules WHERE id = ?', [id]);
|
||||
if (existing.length === 0) return notFound(reply, '일정을 찾을 수 없습니다.');
|
||||
|
||||
const finalDate = normalizeDate(date, datePrecision);
|
||||
|
||||
await withTransaction(db, async (conn) => {
|
||||
await conn.query(
|
||||
'UPDATE schedules SET category_id = ?, title = ?, date = ?, time = ?, date_precision = ? WHERE id = ?',
|
||||
[category, title.trim(), finalDate, time || null, datePrecision, id]
|
||||
);
|
||||
await conn.query('DELETE FROM schedule_members WHERE schedule_id = ?', [id]);
|
||||
if (members.length > 0) {
|
||||
await conn.query('INSERT INTO schedule_members (schedule_id, member_id) VALUES ?',
|
||||
[members.map((m) => [id, m])]);
|
||||
}
|
||||
});
|
||||
|
||||
await syncScheduleById(meilisearch, db, parseInt(id), redis);
|
||||
logActivity(db, {
|
||||
actor: 'admin', action: 'update', category: 'schedule',
|
||||
targetType: 'schedule', targetId: parseInt(id),
|
||||
summary: `일정 수정: ${title.trim()}`,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import xAdminRoutes from './admin/x.js';
|
|||
import concertAdminRoutes from './admin/concert.js';
|
||||
import eventsAdminRoutes from './admin/events.js';
|
||||
import varietyAdminRoutes from './admin/variety.js';
|
||||
import schedulesAdminRoutes from './admin/schedules.js';
|
||||
import placesAdminRoutes from './admin/places.js';
|
||||
import logsAdminRoutes from './admin/logs.js';
|
||||
|
||||
|
|
@ -62,6 +63,9 @@ export default async function routes(fastify) {
|
|||
// 관리자 - 예능 라우트
|
||||
fastify.register(varietyAdminRoutes, { prefix: '/admin/variety' });
|
||||
|
||||
// 관리자 - 일반 일정 라우트 (컴백·팬사인회·기타)
|
||||
fastify.register(schedulesAdminRoutes, { prefix: '/admin/schedules' });
|
||||
|
||||
// 관리자 - 장소 검색 라우트
|
||||
fastify.register(placesAdminRoutes, { prefix: '/admin' });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue