From a11a027682baf4e103ada58f180830836507934c Mon Sep 17 00:00:00 2001 From: caadiq Date: Tue, 16 Jun 2026 21:58:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin-schedule):=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20=EC=83=9D=EC=84=B1/=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(=EC=BB=B4?= =?UTF-8?q?=EB=B0=B1=C2=B7=ED=8C=AC=EC=82=AC=EC=9D=B8=ED=9A=8C=C2=B7?= =?UTF-8?q?=EA=B8=B0=ED=83=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전용 폼이 없는 단순 카테고리용 POST/PUT /admin/schedules 신규. 제목·날짜·시간·카테고리·멤버 + date_precision(월만=날짜 미정) 처리, month이면 날짜를 해당 월 1일로 정규화. schedules + schedule_members 트랜잭션 + meili 동기화 + 월별 캐시 무효화. (앨범→컴백 카테고리 리네임 동반) Co-Authored-By: Claude Opus 4.7 --- backend/src/routes/admin/schedules.js | 124 ++++++++++++++++++++++++++ backend/src/routes/index.js | 4 + 2 files changed, 128 insertions(+) create mode 100644 backend/src/routes/admin/schedules.js diff --git a/backend/src/routes/admin/schedules.js b/backend/src/routes/admin/schedules.js new file mode 100644 index 0000000..7bf6c61 --- /dev/null +++ b/backend/src/routes/admin/schedules.js @@ -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 }; + }); +} diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index 6a90bb8..b6af5bc 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -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' });