import { and, asc, eq, gte, lte } from 'drizzle-orm' import { z } from 'zod' import { db } from '../db/client.js' import { plannerEntries } from '../db/schema.js' import { findAuthenticatedUser } from '../lib/authSession.js' const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/) const plannerPayloadSchema = z.object({ payload: z.record(z.any()), }) const plannerRangeQuerySchema = z.object({ from: dateSchema.optional(), to: dateSchema.optional(), }) async function requireAuthenticatedUser(request, reply) { const user = await findAuthenticatedUser(request) if (!user) { reply.code(401).send({ message: '인증이 필요합니다.', }) return null } return user } export async function registerPlannerRoutes(app) { app.get('/api/planner', async (request, reply) => { const user = await requireAuthenticatedUser(request, reply) if (!user) { return } const query = plannerRangeQuerySchema.safeParse(request.query ?? {}) if (!query.success) { return reply.code(400).send({ message: '조회 범위가 올바르지 않습니다.', issues: query.error.flatten(), }) } const filters = [eq(plannerEntries.userId, user.id)] if (query.data.from) { filters.push(gte(plannerEntries.entryDate, query.data.from)) } if (query.data.to) { filters.push(lte(plannerEntries.entryDate, query.data.to)) } const entries = await db .select({ entryDate: plannerEntries.entryDate, payload: plannerEntries.payload, createdAt: plannerEntries.createdAt, updatedAt: plannerEntries.updatedAt, }) .from(plannerEntries) .where(and(...filters)) .orderBy(asc(plannerEntries.entryDate)) return { entries, } }) app.get('/api/planner/:entryDate', async (request, reply) => { const user = await requireAuthenticatedUser(request, reply) if (!user) { return } const dateResult = dateSchema.safeParse(request.params.entryDate) if (!dateResult.success) { return reply.code(400).send({ message: '날짜 형식이 올바르지 않습니다.', }) } const [entry] = await db .select() .from(plannerEntries) .where( and( eq(plannerEntries.userId, user.id), eq(plannerEntries.entryDate, dateResult.data), ), ) .limit(1) return { entry: entry ?? null, } }) app.put('/api/planner/:entryDate', async (request, reply) => { const user = await requireAuthenticatedUser(request, reply) if (!user) { return } const dateResult = dateSchema.safeParse(request.params.entryDate) if (!dateResult.success) { return reply.code(400).send({ message: '날짜 형식이 올바르지 않습니다.', }) } const payloadResult = plannerPayloadSchema.safeParse(request.body) if (!payloadResult.success) { return reply.code(400).send({ message: '플래너 저장 데이터가 올바르지 않습니다.', issues: payloadResult.error.flatten(), }) } const now = new Date() const [entry] = await db .insert(plannerEntries) .values({ userId: user.id, entryDate: dateResult.data, payload: payloadResult.data.payload, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: [plannerEntries.userId, plannerEntries.entryDate], set: { payload: payloadResult.data.payload, updatedAt: now, }, }) .returning() return { message: '플래너가 저장되었습니다.', entry, } }) app.delete('/api/planner/:entryDate', async (request, reply) => { const user = await requireAuthenticatedUser(request, reply) if (!user) { return } const dateResult = dateSchema.safeParse(request.params.entryDate) if (!dateResult.success) { return reply.code(400).send({ message: '날짜 형식이 올바르지 않습니다.', }) } const deletedEntries = await db .delete(plannerEntries) .where( and( eq(plannerEntries.userId, user.id), eq(plannerEntries.entryDate, dateResult.data), ), ) .returning() return { message: deletedEntries.length > 0 ? '플래너가 삭제되었습니다.' : '삭제할 플래너가 없습니다.', deleted: deletedEntries.length > 0, } }) }