diff --git a/src/app/api/posts/[id]/view/route.ts b/src/app/api/posts/[id]/view/route.ts new file mode 100644 index 0000000..793fe15 --- /dev/null +++ b/src/app/api/posts/[id]/view/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getUserIdFromRequest } from "@/lib/auth"; + +export async function POST(req: Request, context: { params: Promise<{ id: string }> }) { + const { id } = await context.params; + const userId = getUserIdFromRequest(req); + const ip = req.headers.get("x-forwarded-for") || undefined; + const userAgent = req.headers.get("user-agent") || undefined; + await prisma.postViewLog.create({ data: { postId: id, userId: userId ?? null, ip, userAgent } }); + await prisma.postStat.upsert({ where: { postId: id }, update: { views: { increment: 1 } }, create: { postId: id, views: 1 } }); + return NextResponse.json({ ok: true }); +} + + diff --git a/src/app/api/uploads/route.ts b/src/app/api/uploads/route.ts new file mode 100644 index 0000000..9cf5e5c --- /dev/null +++ b/src/app/api/uploads/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { promises as fs } from "fs"; +import path from "path"; + +export const runtime = "nodejs"; + +export async function POST(req: Request) { + const form = await req.formData(); + const file = form.get("file") as File | null; + if (!file) return NextResponse.json({ error: "file required" }, { status: 400 }); + + const bytes = await file.arrayBuffer(); + const buffer = Buffer.from(bytes); + const ext = path.extname(file.name || "") || ".bin"; + const uploadsDir = path.join(process.cwd(), "public", "uploads"); + await fs.mkdir(uploadsDir, { recursive: true }); + const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`; + const filepath = path.join(uploadsDir, filename); + await fs.writeFile(filepath, buffer); + const url = `/uploads/${filename}`; + return NextResponse.json({ url }); +} + + diff --git a/src/app/components/UploadButton.tsx b/src/app/components/UploadButton.tsx new file mode 100644 index 0000000..a726e83 --- /dev/null +++ b/src/app/components/UploadButton.tsx @@ -0,0 +1,33 @@ +"use client"; +import { useState } from "react"; +import { useToast } from "@/app/components/ui/ToastProvider"; + +export function UploadButton({ onUploaded }: { onUploaded: (url: string) => void }) { + const { show } = useToast(); + const [loading, setLoading] = useState(false); + async function onChange(e: React.ChangeEvent) { + const f = e.target.files?.[0]; + if (!f) return; + const fd = new FormData(); + fd.append("file", f); + try { + setLoading(true); + const r = await fetch("/api/uploads", { method: "POST", body: fd }); + const data = await r.json(); + if (!r.ok) throw new Error(JSON.stringify(data)); + onUploaded(data.url); + show("업로드 완료"); + } catch (e) { + show("업로드 실패"); + } finally { + setLoading(false); + e.currentTarget.value = ""; + } + } + return ; +} + + diff --git a/src/app/posts/[id]/edit/page.tsx b/src/app/posts/[id]/edit/page.tsx index 355d585..40dd223 100644 --- a/src/app/posts/[id]/edit/page.tsx +++ b/src/app/posts/[id]/edit/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { useToast } from "@/app/components/ui/ToastProvider"; +import { UploadButton } from "@/app/components/UploadButton"; export default function EditPostPage() { const params = useParams<{ id: string }>(); @@ -43,6 +44,7 @@ export default function EditPostPage() {

글 수정

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