diff --git a/src/app/api/posts/[id]/approve/route.ts b/src/app/api/posts/[id]/approve/route.ts new file mode 100644 index 0000000..3d46f06 --- /dev/null +++ b/src/app/api/posts/[id]/approve/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function POST(_: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const post = await prisma.post.update({ + where: { id }, + data: { status: "published" }, + select: { id: true, status: true }, + }); + return NextResponse.json({ post }); +} + + diff --git a/src/app/api/posts/[id]/comments/route.ts b/src/app/api/posts/[id]/comments/route.ts new file mode 100644 index 0000000..c635fa2 --- /dev/null +++ b/src/app/api/posts/[id]/comments/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function GET(_: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const comments = await prisma.comment.findMany({ + where: { postId: id }, + orderBy: { createdAt: "asc" }, + select: { + id: true, + content: true, + isAnonymous: true, + isSecret: true, + createdAt: true, + }, + }); + return NextResponse.json({ comments }); +} + + diff --git a/src/app/api/posts/[id]/pin/route.ts b/src/app/api/posts/[id]/pin/route.ts new file mode 100644 index 0000000..d7f7abb --- /dev/null +++ b/src/app/api/posts/[id]/pin/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { z } from "zod"; + +const schema = z.object({ pinned: z.boolean(), order: z.number().int().nullable().optional() }); + +export async function POST(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const body = await req.json(); + const parsed = schema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + const { pinned, order } = parsed.data; + const post = await prisma.post.update({ + where: { id }, + data: { isPinned: pinned, pinnedOrder: pinned ? order ?? 0 : null }, + select: { id: true, isPinned: true, pinnedOrder: true }, + }); + return NextResponse.json({ post }); +} + + diff --git a/src/app/api/posts/[id]/recommend/route.ts b/src/app/api/posts/[id]/recommend/route.ts new file mode 100644 index 0000000..fb362bf --- /dev/null +++ b/src/app/api/posts/[id]/recommend/route.ts @@ -0,0 +1,33 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { z } from "zod"; + +const schema = z.object({ userId: z.string().optional(), clientHash: z.string().optional() }).refine( + (d) => !!d.userId || !!d.clientHash, + { message: "Provide userId or clientHash" } +); + +export async function POST(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const body = await req.json().catch(() => ({})); + const parsed = schema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + const { userId, clientHash } = parsed.data; + + const existing = await prisma.reaction.findFirst({ + where: { postId: id, type: "RECOMMEND", userId: userId ?? null, clientHash: clientHash ?? null }, + select: { id: true }, + }); + if (!existing) { + await prisma.reaction.create({ data: { postId: id, type: "RECOMMEND", userId: userId ?? null, clientHash: clientHash ?? null } }); + await prisma.postStat.upsert({ + where: { postId: id }, + update: { recommendCount: { increment: 1 } }, + create: { postId: id, recommendCount: 1 }, + }); + } + + return NextResponse.json({ ok: true }); +} + + diff --git a/src/app/api/posts/[id]/report/route.ts b/src/app/api/posts/[id]/report/route.ts new file mode 100644 index 0000000..d3bcdfd --- /dev/null +++ b/src/app/api/posts/[id]/report/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { z } from "zod"; + +const schema = z.object({ reason: z.string().min(1) }); + +export async function POST(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const body = await req.json().catch(() => ({})); + const parsed = schema.safeParse(body); + if (!parsed.success) return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + + const report = await prisma.report.create({ + data: { targetType: "POST", postId: id, reason: parsed.data.reason }, + }); + + await prisma.postStat.upsert({ + where: { postId: id }, + update: { reportCount: { increment: 1 } }, + create: { postId: id, reportCount: 1 }, + }); + + await prisma.adminNotification.create({ + data: { type: "REPORT", message: `신고 발생: post ${id}`, targetType: "POST", targetId: id }, + }); + + return NextResponse.json({ ok: true, reportId: report.id }); +} + + diff --git a/src/app/api/posts/[id]/route.ts b/src/app/api/posts/[id]/route.ts new file mode 100644 index 0000000..d035870 --- /dev/null +++ b/src/app/api/posts/[id]/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; + +export async function GET(_: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const post = await prisma.post.findUnique({ + where: { id }, + include: { + board: { select: { id: true, name: true, slug: true } }, + }, + }); + if (!post) return NextResponse.json({ error: "Not found" }, { status: 404 }); + return NextResponse.json({ post }); +} + + diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index f76d41a..c481c6b 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -17,10 +17,66 @@ export async function POST(req: Request) { return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); } const { boardId, authorId, title, content, isAnonymous } = parsed.data; + const board = await prisma.board.findUnique({ where: { id: boardId } }); + const requiresApproval = board?.requiresApproval ?? false; const post = await prisma.post.create({ - data: { boardId, authorId: authorId ?? null, title, content, isAnonymous: !!isAnonymous }, + data: { + boardId, + authorId: authorId ?? null, + title, + content, + isAnonymous: !!isAnonymous, + status: requiresApproval ? "hidden" : "published", + }, }); return NextResponse.json({ post }, { status: 201 }); } +const listQuerySchema = z.object({ + page: z.coerce.number().min(1).default(1), + pageSize: z.coerce.number().min(1).max(100).default(10), + boardId: z.string().optional(), + q: z.string().optional(), +}); + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const parsed = listQuerySchema.safeParse(Object.fromEntries(searchParams)); + if (!parsed.success) { + return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); + } + const { page, pageSize, boardId, q } = parsed.data; + const where = { + ...(boardId ? { boardId } : {}), + ...(q + ? { + OR: [ + { title: { contains: q } }, + { content: { contains: q } }, + ], + } + : {}), + } as const; + + const [total, items] = await Promise.all([ + prisma.post.count({ where }), + prisma.post.findMany({ + where, + orderBy: [{ isPinned: "desc" }, { createdAt: "desc" }], + skip: (page - 1) * pageSize, + take: pageSize, + select: { + id: true, + title: true, + createdAt: true, + boardId: true, + isPinned: true, + status: true, + }, + }), + ]); + + return NextResponse.json({ total, page, pageSize, items }); +} +