From 6d37881dd70a5726f8c78a227db69fadcdd2656b Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Thu, 9 Oct 2025 17:05:19 +0900 Subject: [PATCH] =?UTF-8?q?7.5=20=EC=B6=94=EC=B2=9C/=EC=8B=A0=EA=B3=A0,=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=B9=B4=EC=9A=B4=ED=8A=B8=20o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/posts/[id]/view/route.ts | 15 +++++++++++++ src/app/api/uploads/route.ts | 24 ++++++++++++++++++++ src/app/components/UploadButton.tsx | 33 ++++++++++++++++++++++++++++ src/app/posts/[id]/edit/page.tsx | 2 ++ src/app/posts/[id]/page.tsx | 19 ++++++++++++++++ src/app/posts/new/page.tsx | 2 ++ todolist.txt | 4 ++-- 7 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/app/api/posts/[id]/view/route.ts create mode 100644 src/app/api/uploads/route.ts create mode 100644 src/app/components/UploadButton.tsx 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 })} />