Initial commit: add FastAPI MVP (모프) and existing web app
Includes FastAPI+Jinja2+HTMX+SQLite implementation with seed categories, plus deployment templates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
260
apps/web/src/app/prompts/page.tsx
Normal file
260
apps/web/src/app/prompts/page.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export default async function PromptsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{
|
||||
query?: string;
|
||||
entity?: string;
|
||||
tag?: string;
|
||||
model?: string;
|
||||
sort?: "new" | "popular";
|
||||
}>;
|
||||
}) {
|
||||
const sp = await searchParams;
|
||||
const query = (sp.query ?? "").trim();
|
||||
const entity = (sp.entity ?? "").trim();
|
||||
const tag = (sp.tag ?? "").trim();
|
||||
const model = (sp.model ?? "").trim();
|
||||
const sort = sp.sort === "popular" ? "popular" : "new";
|
||||
|
||||
const [entities, tags, models, prompts] = await Promise.all([
|
||||
prisma.entity.findMany({
|
||||
where: { isPublished: true },
|
||||
orderBy: { name: "asc" },
|
||||
take: 100,
|
||||
select: { slug: true, name: true },
|
||||
}),
|
||||
prisma.tag.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
take: 200,
|
||||
select: { slug: true, name: true },
|
||||
}),
|
||||
prisma.promptModel.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
take: 100,
|
||||
select: { slug: true, name: true, provider: true },
|
||||
}),
|
||||
prisma.prompt.findMany({
|
||||
where: {
|
||||
isPublished: true,
|
||||
...(entity ? { entity: { slug: entity } } : {}),
|
||||
...(model ? { model: { slug: model } } : {}),
|
||||
...(tag ? { tags: { some: { tag: { slug: tag } } } } : {}),
|
||||
...(query
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: "insensitive" } },
|
||||
{ descriptionMd: { contains: query, mode: "insensitive" } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
orderBy:
|
||||
sort === "popular"
|
||||
? { likes: { _count: "desc" } }
|
||||
: { createdAt: "desc" },
|
||||
take: 50,
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
descriptionMd: true,
|
||||
createdAt: true,
|
||||
entity: { select: { slug: true, name: true } },
|
||||
model: { select: { slug: true, name: true, provider: true } },
|
||||
tags: { select: { tag: { select: { slug: true, name: true } } } },
|
||||
_count: { select: { likes: true, comments: true, bookmarks: true } },
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-50">
|
||||
<div className="mx-auto max-w-5xl px-6 py-10">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm font-medium text-zinc-500">모프</p>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
프롬프트 검색
|
||||
</h1>
|
||||
</div>
|
||||
<Link
|
||||
href="/entities"
|
||||
className="text-sm font-semibold text-zinc-900 underline underline-offset-4"
|
||||
>
|
||||
대상으로 이동
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<form className="rounded-2xl border border-zinc-200 bg-white p-4">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-xs font-semibold text-zinc-700">
|
||||
키워드
|
||||
</label>
|
||||
<input
|
||||
name="query"
|
||||
defaultValue={query}
|
||||
placeholder="예: 요약, 코드 리뷰, STAR…"
|
||||
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-4 text-sm outline-none focus:border-zinc-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-zinc-700">
|
||||
대상(Entity)
|
||||
</label>
|
||||
<select
|
||||
name="entity"
|
||||
defaultValue={entity}
|
||||
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{entities.map((e) => (
|
||||
<option key={e.slug} value={e.slug}>
|
||||
{e.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-zinc-700">
|
||||
정렬
|
||||
</label>
|
||||
<select
|
||||
name="sort"
|
||||
defaultValue={sort}
|
||||
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
|
||||
>
|
||||
<option value="new">최신</option>
|
||||
<option value="popular">인기(좋아요)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-zinc-700">
|
||||
태그
|
||||
</label>
|
||||
<select
|
||||
name="tag"
|
||||
defaultValue={tag}
|
||||
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{tags.map((t) => (
|
||||
<option key={t.slug} value={t.slug}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-zinc-700">
|
||||
모델
|
||||
</label>
|
||||
<select
|
||||
name="model"
|
||||
defaultValue={model}
|
||||
className="mt-2 h-11 w-full rounded-xl border border-zinc-200 bg-white px-3 text-sm outline-none focus:border-zinc-400"
|
||||
>
|
||||
<option value="">전체</option>
|
||||
{models.map((m) => (
|
||||
<option key={m.slug} value={m.slug}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="h-11 w-full rounded-xl bg-zinc-900 px-4 text-sm font-semibold text-white hover:bg-zinc-800"
|
||||
>
|
||||
적용
|
||||
</button>
|
||||
<Link
|
||||
href="/prompts"
|
||||
className="inline-flex h-11 shrink-0 items-center justify-center rounded-xl border border-zinc-200 bg-white px-4 text-sm font-semibold text-zinc-900 hover:bg-zinc-50"
|
||||
>
|
||||
초기화
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-white p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold">결과</h2>
|
||||
<p className="text-sm text-zinc-500">{prompts.length}개</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-4">
|
||||
{prompts.map((p) => (
|
||||
<Link
|
||||
key={p.id}
|
||||
href={`/prompts/${p.id}`}
|
||||
className="rounded-2xl border border-zinc-200 bg-zinc-50 p-5 hover:bg-white"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-semibold">{p.title}</p>
|
||||
<p className="text-xs text-zinc-500">
|
||||
{p.entity.name}
|
||||
{p.model ? ` · ${p.model.name}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-xs text-zinc-600">
|
||||
<p>좋아요 {p._count.likes}</p>
|
||||
<p>댓글 {p._count.comments}</p>
|
||||
<p>북마크 {p._count.bookmarks}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{p.descriptionMd ? (
|
||||
<p className="mt-2 line-clamp-2 text-sm text-zinc-700">
|
||||
{p.descriptionMd}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{p.tags.length ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{p.tags.map((t) => (
|
||||
<span
|
||||
key={t.tag.slug}
|
||||
className="rounded-full border border-zinc-200 bg-white px-2 py-0.5 text-xs text-zinc-700"
|
||||
>
|
||||
#{t.tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</Link>
|
||||
))}
|
||||
{!prompts.length ? (
|
||||
<div className="rounded-2xl border border-zinc-200 bg-white p-6 text-sm text-zinc-600">
|
||||
결과가 없습니다. 필터를 줄이거나 키워드를 바꿔보세요.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-zinc-200 bg-white p-5">
|
||||
<p className="text-sm font-semibold">API</p>
|
||||
<p className="mt-1 text-sm text-zinc-600">
|
||||
검색 API:{" "}
|
||||
<code className="rounded bg-zinc-50 px-1">/api/prompts</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user