v0.1.17 - 목표 패널 및 D-DAY 선택 추가

This commit is contained in:
2026-04-21 18:37:06 +09:00
parent 440f0f46a1
commit 4355185203
11 changed files with 429 additions and 21 deletions

View File

@@ -32,5 +32,18 @@ export function ensureDatabaseSchema() {
CREATE UNIQUE INDEX IF NOT EXISTS planner_entries_user_date_unique
ON planner_entries (user_id, entry_date);
CREATE TABLE IF NOT EXISTS goals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
target_date TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
color TEXT NOT NULL DEFAULT '#1c1917',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
completed_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
`)
}

View File

@@ -31,3 +31,15 @@ export const plannerEntries = sqliteTable(
userDateUnique: uniqueIndex('planner_entries_user_date_unique').on(table.userId, table.entryDate),
}),
)
export const goals = sqliteTable('goals', {
id: integer('id').primaryKey({ autoIncrement: true }),
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
targetDate: text('target_date').notNull(),
status: text('status').notNull().default('active'),
color: text('color').notNull().default('#1c1917'),
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
completedAt: integer('completed_at', { mode: 'timestamp_ms' }),
})

103
backend/src/routes/goals.js Normal file
View File

@@ -0,0 +1,103 @@
import { and, asc, eq, like } from 'drizzle-orm'
import { z } from 'zod'
import { db } from '../db/client.js'
import { goals } from '../db/schema.js'
import { findAuthenticatedUser } from '../lib/authSession.js'
const goalSchema = z.object({
title: z.string().trim().min(1).max(80),
targetDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
color: z.string().trim().min(4).max(32).optional(),
})
const goalQuerySchema = z.object({
query: z.string().trim().optional(),
status: z.enum(['active', 'done', 'archived', 'all']).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 registerGoalRoutes(app) {
app.get('/api/goals', async (request, reply) => {
const user = await requireAuthenticatedUser(request, reply)
if (!user) {
return
}
const query = goalQuerySchema.safeParse(request.query ?? {})
if (!query.success) {
return reply.code(400).send({
message: '목표 조회 조건이 올바르지 않습니다.',
issues: query.error.flatten(),
})
}
const filters = [eq(goals.userId, user.id)]
if (query.data.status && query.data.status !== 'all') {
filters.push(eq(goals.status, query.data.status))
}
if (query.data.query) {
filters.push(like(goals.title, `%${query.data.query}%`))
}
const items = await db
.select()
.from(goals)
.where(and(...filters))
.orderBy(asc(goals.targetDate), asc(goals.id))
return { goals: items }
})
app.post('/api/goals', async (request, reply) => {
const user = await requireAuthenticatedUser(request, reply)
if (!user) {
return
}
const payload = goalSchema.safeParse(request.body)
if (!payload.success) {
return reply.code(400).send({
message: '목표 입력값이 올바르지 않습니다.',
issues: payload.error.flatten(),
})
}
const now = new Date()
const [goal] = await db
.insert(goals)
.values({
userId: user.id,
title: payload.data.title,
targetDate: payload.data.targetDate,
color: payload.data.color ?? '#1c1917',
status: 'active',
createdAt: now,
updatedAt: now,
})
.returning()
return reply.code(201).send({
message: '목표가 추가되었습니다.',
goal,
})
})
}

View File

@@ -4,6 +4,7 @@ import { env } from './config.js'
import { sqlite } from './db/client.js'
import { ensureDatabaseSchema } from './db/init.js'
import { registerAuthRoutes } from './routes/auth.js'
import { registerGoalRoutes } from './routes/goals.js'
import { registerPlannerRoutes } from './routes/planner.js'
const app = Fastify({
@@ -18,6 +19,7 @@ await app.register(cors, {
})
await registerAuthRoutes(app)
await registerGoalRoutes(app)
await registerPlannerRoutes(app)
app.get('/health', async () => {
@@ -39,6 +41,7 @@ app.get('/api/meta', async () => ({
orm: 'drizzle',
notes: [
'회원가입, 로그인, 현재 사용자 확인 API가 준비되어 있습니다.',
'사용자별 목표 목록과 생성 API가 준비되어 있습니다.',
'사용자별 플래너 저장 및 조회 API가 준비되어 있습니다.',
],
}))