From 7342c9bea2fab42812bae339ae5a4c28fe9a128e Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Thu, 9 Oct 2025 16:49:06 +0900 Subject: [PATCH] =?UTF-8?q?7.1=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20CRUD=20API?= =?UTF-8?q?=20=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=20o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/posts/[id]/route.ts | 27 +++++++++++++++++ src/app/api/posts/route.ts | 1 + src/app/components/PostList.tsx | 2 +- src/app/posts/[id]/edit/page.tsx | 51 ++++++++++++++++++++++++++++++++ src/app/posts/[id]/page.tsx | 15 ++++++++++ src/app/posts/new/page.tsx | 40 +++++++++++++++++++++++++ todolist.txt | 2 +- 7 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/app/posts/[id]/edit/page.tsx create mode 100644 src/app/posts/[id]/page.tsx create mode 100644 src/app/posts/new/page.tsx diff --git a/src/app/api/posts/[id]/route.ts b/src/app/api/posts/[id]/route.ts index d035870..a2fbc77 100644 --- a/src/app/api/posts/[id]/route.ts +++ b/src/app/api/posts/[id]/route.ts @@ -1,5 +1,8 @@ import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; +import { z } from "zod"; +import { getUserIdFromRequest } from "@/lib/auth"; +import { requirePermission } from "@/lib/rbac"; export async function GET(_: Request, context: { params: Promise<{ id: string }> }) { const { id } = await context.params; @@ -13,4 +16,28 @@ export async function GET(_: Request, context: { params: Promise<{ id: string }> return NextResponse.json({ post }); } +const updateSchema = z.object({ + title: z.string().min(1).optional(), + content: z.string().min(1).optional(), +}); + +export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const userId = getUserIdFromRequest(req); + await requirePermission({ userId, resource: "POST", action: "UPDATE" }); + const body = await req.json(); + const parsed = updateSchema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + const post = await prisma.post.update({ where: { id }, data: parsed.data }); + return NextResponse.json({ post }); +} + +export async function DELETE(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const userId = getUserIdFromRequest(req); + await requirePermission({ userId, resource: "POST", action: "DELETE" }); + const post = await prisma.post.update({ where: { id }, data: { status: "deleted" } }); + return NextResponse.json({ post }); +} + diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index 4c21d6f..e91d48c 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -48,6 +48,7 @@ export async function GET(req: Request) { } const { page, pageSize, boardId, q, sort = "recent" } = parsed.data; const where = { + NOT: { status: "deleted" as const }, ...(boardId ? { boardId } : {}), ...(q ? { diff --git a/src/app/components/PostList.tsx b/src/app/components/PostList.tsx index 9367f0f..879738b 100644 --- a/src/app/components/PostList.tsx +++ b/src/app/components/PostList.tsx @@ -55,7 +55,7 @@ export function PostList({ boardId, sort = "recent", q }: { boardId?: string; so {items.map((p) => (
  • - {p.title} + {p.title} {new Date(p.createdAt).toLocaleString()}
    diff --git a/src/app/posts/[id]/edit/page.tsx b/src/app/posts/[id]/edit/page.tsx new file mode 100644 index 0000000..355d585 --- /dev/null +++ b/src/app/posts/[id]/edit/page.tsx @@ -0,0 +1,51 @@ +"use client"; +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { useToast } from "@/app/components/ui/ToastProvider"; + +export default function EditPostPage() { + const params = useParams<{ id: string }>(); + const id = params?.id as string; + const router = useRouter(); + const { show } = useToast(); + const [form, setForm] = useState<{ title: string; content: string } | null>(null); + const [loading, setLoading] = useState(false); + useEffect(() => { + (async () => { + const r = await fetch(`/api/posts/${id}`); + if (!r.ok) return; + const { post } = await r.json(); + setForm({ title: post.title, content: post.content }); + })(); + }, [id]); + async function submit() { + if (!form) return; + try { + setLoading(true); + const r = await fetch(`/api/posts/${id}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify(form), + }); + const data = await r.json(); + if (!r.ok) throw new Error(JSON.stringify(data)); + show("수정되었습니다"); + router.push(`/posts/${id}`); + } catch (e) { + show("수정 실패"); + } finally { + setLoading(false); + } + } + if (!form) return
    불러오는 중...
    ; + return ( +
    +

    글 수정

    + setForm({ ...form, title: e.target.value })} /> +