diff --git a/.cursor/.prompt/게시판 대분류작업.md b/.cursor/.prompt/게시판 대분류작업.md index f436cb1..4ccb8df 100644 --- a/.cursor/.prompt/게시판 대분류작업.md +++ b/.cursor/.prompt/게시판 대분류작업.md @@ -12,9 +12,9 @@ - [x] 1.6 ERD 재생성(`npx prisma generate`) API -- [ ] 2.1 Admin: 카테고리 CRUD 엔드포인트 추가(`src/app/api/admin/categories/route.ts`) -- [ ] 2.2 Admin: 게시판 생성/수정 요청에 `categoryId` 허용(`src/app/api/admin/boards/...`) -- [ ] 2.3 Public: 게시판 목록 조회에 `category` 포함 및 `?category` 필터 지원(`src/app/api/boards/route.ts`) +- [x] 2.1 Admin: 카테고리 CRUD 엔드포인트 추가(`src/app/api/admin/categories/route.ts`) +- [x] 2.2 Admin: 게시판 생성/수정 요청에 `categoryId` 허용(`src/app/api/admin/boards/...`) +- [x] 2.3 Public: 게시판 목록 조회에 `category` 포함 및 `?category` 필터 지원(`src/app/api/boards/route.ts`) - [ ] 2.4 RBAC 검토: `ADMIN` 또는 `BOARD` 권한으로 카테고리 관리 허용 프론트엔드 diff --git a/src/app/api/admin/boards/[id]/route.ts b/src/app/api/admin/boards/[id]/route.ts index a962eb6..dc87a7f 100644 --- a/src/app/api/admin/boards/[id]/route.ts +++ b/src/app/api/admin/boards/[id]/route.ts @@ -5,7 +5,7 @@ export async function PATCH(req: Request, context: { params: Promise<{ id: strin const { id } = await context.params; const body = await req.json().catch(() => ({})); const data: any = {}; - for (const k of ["name", "slug", "description", "sortOrder", "readLevel", "writeLevel", "allowAnonymousPost", "allowSecretComment", "requiresApproval", "status", "type", "isAdultOnly"]) { + for (const k of ["name", "slug", "description", "sortOrder", "readLevel", "writeLevel", "allowAnonymousPost", "allowSecretComment", "requiresApproval", "status", "type", "isAdultOnly", "categoryId"]) { if (k in body) data[k] = body[k]; } if ("requiredTags" in body) { diff --git a/src/app/api/admin/boards/route.ts b/src/app/api/admin/boards/route.ts index 4cb4781..a5014a2 100644 --- a/src/app/api/admin/boards/route.ts +++ b/src/app/api/admin/boards/route.ts @@ -17,6 +17,8 @@ export async function GET() { requiresApproval: true, type: true, status: true, + categoryId: true, + category: { select: { id: true, name: true, slug: true } }, }, }); return NextResponse.json({ boards }); diff --git a/src/app/api/admin/categories/[id]/route.ts b/src/app/api/admin/categories/[id]/route.ts new file mode 100644 index 0000000..ae35f4b --- /dev/null +++ b/src/app/api/admin/categories/[id]/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const body = await req.json().catch(() => ({})); + const data: any = {}; + for (const k of ["name", "slug", "sortOrder", "status"]) { + if (k in body) data[k] = body[k]; + } + const category = await prisma.boardCategory.update({ where: { id }, data }); + return NextResponse.json({ category }); +} + +export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + await prisma.boardCategory.delete({ where: { id } }); + return NextResponse.json({ ok: true }); +} + + diff --git a/src/app/api/admin/categories/route.ts b/src/app/api/admin/categories/route.ts new file mode 100644 index 0000000..f002837 --- /dev/null +++ b/src/app/api/admin/categories/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { z } from "zod"; + +export async function GET() { + const categories = await prisma.boardCategory.findMany({ + orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], + }); + return NextResponse.json({ categories }); +} + +const createSchema = z.object({ + name: z.string().min(1), + slug: z.string().min(1), + sortOrder: z.coerce.number().int().optional(), + status: z.enum(["active", "hidden"]).optional(), +}); + +export async function POST(req: Request) { + const body = await req.json().catch(() => ({})); + const parsed = createSchema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + const category = await prisma.boardCategory.create({ data: parsed.data }); + return NextResponse.json({ category }, { status: 201 }); +} + + diff --git a/src/app/api/boards/route.ts b/src/app/api/boards/route.ts index 400b894..cd87c3c 100644 --- a/src/app/api/boards/route.ts +++ b/src/app/api/boards/route.ts @@ -1,8 +1,19 @@ import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -export async function GET() { +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const category = searchParams.get("category"); // slug or id + const where: any = {}; + if (category) { + if (category.length === 25 || category.length === 24) { + where.categoryId = category; + } else { + where.category = { slug: category }; + } + } const boards = await prisma.board.findMany({ + where, orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], select: { id: true, @@ -13,6 +24,7 @@ export async function GET() { requiresApproval: true, allowAnonymousPost: true, isAdultOnly: true, + category: { select: { id: true, name: true, slug: true } }, }, }); return NextResponse.json({ boards });