diff --git a/backend/src/app.js b/backend/src/app.js index a8861cf..0702a96 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -61,10 +61,10 @@ export async function buildApp(opts = {}) { ], tags: [ { name: 'auth', description: '인증 API' }, - { name: 'members', description: '멤버 관리 API' }, - { name: 'albums', description: '앨범 관리 API' }, + { name: 'members', description: '멤버 API' }, + { name: 'albums', description: '앨범 API' }, + { name: 'schedules', description: '일정 API' }, { name: 'stats', description: '통계 API' }, - { name: 'public', description: '공개 API' }, ], components: { securitySchemes: { diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index ca45be9..ba0ef65 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -1,13 +1,14 @@ import authRoutes from './auth.js'; import membersRoutes from './members/index.js'; import albumsRoutes from './albums/index.js'; +import schedulesRoutes from './schedules/index.js'; import statsRoutes from './stats/index.js'; /** * 라우트 통합 * /api/* */ -export default async function routes(fastify, opts) { +export default async function routes(fastify) { // 인증 라우트 fastify.register(authRoutes, { prefix: '/auth' }); @@ -17,6 +18,9 @@ export default async function routes(fastify, opts) { // 앨범 라우트 fastify.register(albumsRoutes, { prefix: '/albums' }); + // 일정 라우트 + fastify.register(schedulesRoutes, { prefix: '/schedules' }); + // 통계 라우트 fastify.register(statsRoutes, { prefix: '/stats' }); } diff --git a/backend/src/routes/schedules/index.js b/backend/src/routes/schedules/index.js new file mode 100644 index 0000000..6e21b5d --- /dev/null +++ b/backend/src/routes/schedules/index.js @@ -0,0 +1,147 @@ +/** + * 일정 라우트 + * GET: 공개, POST/PUT/DELETE: 인증 필요 + */ +export default async function schedulesRoutes(fastify) { + const { db } = fastify; + + /** + * GET /api/schedules + * 월별 일정 목록 조회 + * @query year - 년도 (필수) + * @query month - 월 (필수) + */ + fastify.get('/', { + schema: { + tags: ['schedules'], + summary: '월별 일정 목록 조회', + querystring: { + type: 'object', + required: ['year', 'month'], + properties: { + year: { type: 'integer', description: '년도' }, + month: { type: 'integer', minimum: 1, maximum: 12, description: '월' }, + }, + }, + }, + }, async (request, reply) => { + const { year, month } = request.query; + + if (!year || !month) { + return reply.code(400).send({ error: 'year와 month는 필수입니다.' }); + } + + const startDate = `${year}-${String(month).padStart(2, '0')}-01`; + const endDate = new Date(year, month, 0).toISOString().split('T')[0]; + + const [schedules] = await db.query(` + SELECT + s.id, + s.title, + s.date, + s.time, + s.category_id, + c.name as category_name, + c.color as category_color, + sy.channel_name as source_name + FROM schedules s + LEFT JOIN schedule_categories c ON s.category_id = c.id + LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id + WHERE s.date BETWEEN ? AND ? + ORDER BY s.date ASC, s.time ASC + `, [startDate, endDate]); + + // 날짜별로 그룹화 + const grouped = {}; + + for (const s of schedules) { + const dateKey = s.date.toISOString().split('T')[0]; + + if (!grouped[dateKey]) { + grouped[dateKey] = { + categories: [], + schedules: [], + }; + } + + // 일정 추가 + const schedule = { + id: s.id, + title: s.title, + time: s.time, + category: { + id: s.category_id, + name: s.category_name, + color: s.category_color, + }, + }; + if (s.source_name) { + schedule.source_name = s.source_name; + } + grouped[dateKey].schedules.push(schedule); + + // 카테고리 카운트 + const existingCategory = grouped[dateKey].categories.find(c => c.id === s.category_id); + if (existingCategory) { + existingCategory.count++; + } else { + grouped[dateKey].categories.push({ + id: s.category_id, + name: s.category_name, + color: s.category_color, + count: 1, + }); + } + } + + return grouped; + }); + + /** + * GET /api/schedules/:id + * 일정 상세 조회 + */ + fastify.get('/:id', { + schema: { + tags: ['schedules'], + summary: '일정 상세 조회', + }, + }, async (request, reply) => { + const { id } = request.params; + + const [schedules] = await db.query(` + SELECT + s.*, + c.name as category_name, + c.color as category_color, + sy.channel_name as source_name + FROM schedules s + LEFT JOIN schedule_categories c ON s.category_id = c.id + LEFT JOIN schedule_youtube sy ON s.id = sy.schedule_id + WHERE s.id = ? + `, [id]); + + if (schedules.length === 0) { + return reply.code(404).send({ error: '일정을 찾을 수 없습니다.' }); + } + + const s = schedules[0]; + const result = { + id: s.id, + title: s.title, + date: s.date, + time: s.time, + category: { + id: s.category_id, + name: s.category_name, + color: s.category_color, + }, + created_at: s.created_at, + updated_at: s.updated_at, + }; + if (s.source_name) { + result.source_name = s.source_name; + } + return result; + }); +}