2025-10-09 11:23:27 +09:00
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
|
import prisma from "@/lib/prisma";
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
|
|
const createPostSchema = z.object({
|
|
|
|
|
boardId: z.string().min(1),
|
|
|
|
|
authorId: z.string().optional(),
|
|
|
|
|
title: z.string().min(1),
|
|
|
|
|
content: z.string().min(1),
|
|
|
|
|
isAnonymous: z.boolean().optional(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export async function POST(req: Request) {
|
|
|
|
|
const body = await req.json();
|
|
|
|
|
const parsed = createPostSchema.safeParse(body);
|
|
|
|
|
if (!parsed.success) {
|
|
|
|
|
return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
const { boardId, authorId, title, content, isAnonymous } = parsed.data;
|
2025-10-09 14:18:55 +09:00
|
|
|
const board = await prisma.board.findUnique({ where: { id: boardId } });
|
2025-10-09 17:16:27 +09:00
|
|
|
// 사진형 보드 필수 이미지 검증: content 내 이미지 링크 최소 1개
|
|
|
|
|
const isImageOnly = (board?.requiredFields as any)?.imageOnly;
|
|
|
|
|
const minImages = (board?.requiredFields as any)?.minImages ?? 0;
|
|
|
|
|
if (isImageOnly || minImages > 0) {
|
|
|
|
|
const imageLinks = (content.match(/!\[[^\]]*\]\([^\)]+\)/g) ?? []).length;
|
|
|
|
|
if (imageLinks < (minImages || 1)) {
|
|
|
|
|
return NextResponse.json({ error: { message: `이미지 최소 ${minImages || 1}개 필요` } }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-09 11:23:27 +09:00
|
|
|
const post = await prisma.post.create({
|
2025-10-09 14:18:55 +09:00
|
|
|
data: {
|
|
|
|
|
boardId,
|
|
|
|
|
authorId: authorId ?? null,
|
|
|
|
|
title,
|
|
|
|
|
content,
|
|
|
|
|
isAnonymous: !!isAnonymous,
|
2025-11-02 04:39:28 +09:00
|
|
|
status: "published",
|
2025-10-09 14:18:55 +09:00
|
|
|
},
|
2025-10-09 11:23:27 +09:00
|
|
|
});
|
|
|
|
|
return NextResponse.json({ post }, { status: 201 });
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-09 14:18:55 +09:00
|
|
|
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(),
|
2025-10-09 16:31:46 +09:00
|
|
|
sort: z.enum(["recent", "popular"]).default("recent").optional(),
|
2025-10-09 16:54:24 +09:00
|
|
|
tag: z.string().optional(), // Tag.slug
|
|
|
|
|
author: z.string().optional(), // User.nickname contains
|
|
|
|
|
start: z.coerce.date().optional(), // createdAt >= start
|
|
|
|
|
end: z.coerce.date().optional(), // createdAt <= end
|
2025-10-09 14:18:55 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
}
|
2025-10-09 16:54:24 +09:00
|
|
|
const { page, pageSize, boardId, q, sort = "recent", tag, author, start, end } = parsed.data;
|
2025-10-09 14:18:55 +09:00
|
|
|
const where = {
|
2025-10-09 16:49:06 +09:00
|
|
|
NOT: { status: "deleted" as const },
|
2025-10-09 14:18:55 +09:00
|
|
|
...(boardId ? { boardId } : {}),
|
|
|
|
|
...(q
|
|
|
|
|
? {
|
|
|
|
|
OR: [
|
|
|
|
|
{ title: { contains: q } },
|
|
|
|
|
{ content: { contains: q } },
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2025-10-09 16:54:24 +09:00
|
|
|
...(tag
|
|
|
|
|
? {
|
|
|
|
|
postTags: { some: { tag: { slug: tag } } },
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
...(author
|
|
|
|
|
? {
|
|
|
|
|
author: { nickname: { contains: author } },
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
...(start || end
|
|
|
|
|
? {
|
|
|
|
|
createdAt: {
|
|
|
|
|
...(start ? { gte: start } : {}),
|
|
|
|
|
...(end ? { lte: end } : {}),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2025-10-09 14:18:55 +09:00
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
const [total, items] = await Promise.all([
|
|
|
|
|
prisma.post.count({ where }),
|
|
|
|
|
prisma.post.findMany({
|
|
|
|
|
where,
|
2025-10-09 16:31:46 +09:00
|
|
|
orderBy:
|
|
|
|
|
sort === "popular"
|
|
|
|
|
? [{ isPinned: "desc" }, { stat: { recommendCount: "desc" } }, { createdAt: "desc" }]
|
|
|
|
|
: [{ isPinned: "desc" }, { createdAt: "desc" }],
|
2025-10-09 14:18:55 +09:00
|
|
|
skip: (page - 1) * pageSize,
|
|
|
|
|
take: pageSize,
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
createdAt: true,
|
|
|
|
|
boardId: true,
|
|
|
|
|
isPinned: true,
|
|
|
|
|
status: true,
|
2025-10-24 21:24:51 +09:00
|
|
|
author: { select: { nickname: true } },
|
2025-10-09 16:31:46 +09:00
|
|
|
stat: { select: { recommendCount: true, views: true, commentsCount: true } },
|
2025-10-09 16:57:29 +09:00
|
|
|
postTags: { select: { tag: { select: { name: true, slug: true } } } },
|
2025-10-09 14:18:55 +09:00
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
2025-10-09 16:31:46 +09:00
|
|
|
return NextResponse.json({ total, page, pageSize, items, sort });
|
2025-10-09 14:18:55 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-09 11:23:27 +09:00
|
|
|
|