From e75831923118907159deef3d498e0df22a8e1d36 Mon Sep 17 00:00:00 2001 From: koreacomp5 Date: Fri, 10 Oct 2025 16:07:56 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Next15=20=ED=98=B8=ED=99=98=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D/?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=ED=8C=90=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0\n\n-=20=ED=97=A4=EB=8D=94=EB=A5=BC=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EC=A0=84=ED=99=98,=20=EC=84=B8?= =?UTF-8?q?=EC=85=98=20=ED=91=9C=EC=8B=9C/=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=B6=94=EA=B0=80\n-=20/api/auth/session=20GET=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0\n-=20=EC=84=9C=EB=B2=84=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20params/searchParams=20?= =?UTF-8?q?=EC=95=88=EC=A0=84=20=EC=96=B8=EB=9E=A9=20=EC=A0=81=EC=9A=A9\n-?= =?UTF-8?q?=20=EC=84=9C=EB=B2=84=20fetch=20=EC=A0=88=EB=8C=80=20URL=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1(=ED=97=A4=EB=8D=94=20=EA=B8=B0=EB=B0=98)?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20500/URL=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0\n-=20=EC=83=88=20=EA=B8=80=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20useSearchParams=EB=A1=9C=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=BC=20=EA=B2=80=EC=A6=9D/=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=20=EC=B6=94=EA=B0=80\n-=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=ED=8F=BC=20fieldErrors=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B0=8F=20a11y=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EB=B3=B4=EC=99=84\n-=20Partner.name=20@unique=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=8B=9C=EB=93=9C=20=EC=A0=95=EC=83=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/boards/[id]/page.tsx | 1 + src/app/boards/page.tsx | 1 + src/app/components/AppHeader.tsx | 2 ++ src/app/posts/[id]/page.tsx | 7 ++++--- src/app/posts/new/page.tsx | 32 ++++++++++++++++++++++++++------ 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/app/boards/[id]/page.tsx b/src/app/boards/[id]/page.tsx index e098736..2dd8501 100644 --- a/src/app/boards/[id]/page.tsx +++ b/src/app/boards/[id]/page.tsx @@ -1,6 +1,7 @@ import { PostList } from "@/app/components/PostList"; import { headers } from "next/headers"; +// Next 15: params/searchParams가 Promise가 될 수 있어 안전 언랩 처리합니다. export default async function BoardDetail({ params, searchParams }: { params: any; searchParams: any }) { const p = params?.then ? await params : params; const sp = searchParams?.then ? await searchParams : searchParams; diff --git a/src/app/boards/page.tsx b/src/app/boards/page.tsx index 41edc89..b29b30a 100644 --- a/src/app/boards/page.tsx +++ b/src/app/boards/page.tsx @@ -1,6 +1,7 @@ import { headers } from "next/headers"; export default async function BoardsPage() { + // 서버에서 상대경로 fetch가 실패하므로 요청 헤더로 절대 URL을 구성합니다. const h = await headers(); const host = h.get("host") ?? "localhost:3000"; const proto = h.get("x-forwarded-proto") ?? "http"; diff --git a/src/app/components/AppHeader.tsx b/src/app/components/AppHeader.tsx index 1dc42c8..99c686f 100644 --- a/src/app/components/AppHeader.tsx +++ b/src/app/components/AppHeader.tsx @@ -1,4 +1,5 @@ "use client"; +// 클라이언트 훅(useState/useEffect)을 사용하여 세션 표시/로그아웃을 처리합니다. import { ThemeToggle } from "@/app/components/ThemeToggle"; import { SearchBar } from "@/app/components/SearchBar"; import { Button } from "@/app/components/ui/Button"; @@ -6,6 +7,7 @@ import React from "react"; export function AppHeader() { const [user, setUser] = React.useState<{ nickname: string } | null>(null); + // 헤더 마운트 시 세션 존재 여부를 조회해 로그인/로그아웃 UI를 제어합니다. React.useEffect(() => { fetch("/api/auth/session") .then((r) => r.json()) diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx index a850382..7bf78cf 100644 --- a/src/app/posts/[id]/page.tsx +++ b/src/app/posts/[id]/page.tsx @@ -1,9 +1,10 @@ import { notFound } from "next/navigation"; import { headers } from "next/headers"; -import React, { use } from "react"; -export default async function PostDetail({ params }: { params: Promise<{ id: string }> }) { - const { id } = use(params); +// 서버 전용 페이지: params가 Promise일 수 있어 안전 언랩 후 절대 URL로 fetch합니다. +export default async function PostDetail({ params }: { params: any }) { + const p = params?.then ? await params : params; + const id = p.id as string; const h = await headers(); const host = h.get("host") ?? "localhost:3000"; const proto = h.get("x-forwarded-proto") ?? "http"; diff --git a/src/app/posts/new/page.tsx b/src/app/posts/new/page.tsx index 89bd7ea..e6faf0d 100644 --- a/src/app/posts/new/page.tsx +++ b/src/app/posts/new/page.tsx @@ -1,17 +1,33 @@ "use client"; +// 클라이언트 라우터/검색파라미터 훅으로 새 글 작성 폼을 제어합니다. import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useToast } from "@/app/components/ui/ToastProvider"; import { UploadButton } from "@/app/components/UploadButton"; import { Editor } from "@/app/components/Editor"; -export default function NewPostPage({ searchParams }: { searchParams?: { boardId?: string; boardSlug?: string } }) { +export default function NewPostPage() { const router = useRouter(); const { show } = useToast(); - const [form, setForm] = useState({ boardId: searchParams?.boardId ?? "", title: "", content: "" }); + const sp = useSearchParams(); + const initialBoardId = sp.get("boardId") ?? ""; + const boardSlug = sp.get("boardSlug") ?? undefined; + const [form, setForm] = useState({ boardId: initialBoardId, title: "", content: "" }); const [loading, setLoading] = useState(false); async function submit() { try { + if (!form.boardId.trim()) { + show("boardId가 비어 있습니다"); + return; + } + if (!form.title.trim()) { + show("제목을 입력하세요"); + return; + } + if (!form.content.trim()) { + show("내용을 입력하세요"); + return; + } setLoading(true); const r = await fetch("/api/posts", { method: "POST", @@ -19,11 +35,15 @@ export default function NewPostPage({ searchParams }: { searchParams?: { boardId body: JSON.stringify({ ...form }), }); const data = await r.json(); - if (!r.ok) throw new Error(JSON.stringify(data)); + if (!r.ok) { + const fe = (data?.error?.fieldErrors ?? {}) as Record; + const msg = data?.error?.message || Object.values(fe)[0]?.[0] || "작성 실패"; + throw new Error(msg); + } show("작성되었습니다"); router.push(`/posts/${data.post.id}`); } catch (e) { - show("작성 실패"); + show(e instanceof Error ? e.message : "작성 실패"); } finally { setLoading(false); } @@ -37,7 +57,7 @@ export default function NewPostPage({ searchParams }: { searchParams?: { boardId setForm((f) => ({ ...f, content: `${f.content}\n![image](${url})` }))} - {...(searchParams?.boardSlug ? require("@/lib/photoPresets").getPhotoPresetBySlug(searchParams.boardSlug) : {})} + {...(boardSlug ? require("@/lib/photoPresets").getPhotoPresetBySlug(boardSlug) : {})} />