fix: Next15 호환 업데이트 및 인증/게시판 기능 개선\n\n- 헤더를 클라이언트 컴포넌트로 전환, 세션 표시/로그아웃 추가\n- /api/auth/session GET 추가, 로그인/회원가입 페이지 연결\n- 서버 컴포넌트에서 params/searchParams 안전 언랩 적용\n- 서버 fetch 절대 URL 구성(헤더 기반)으로 500/URL 오류 해결\n- 새 글 페이지 useSearchParams로 전환 및 폼 검증/에러 표시 추가\n- 회원가입 폼 fieldErrors 표시 및 a11y 속성 보완\n- Partner.name @unique 추가 및 시드 정상화

This commit is contained in:
koreacomp5
2025-10-10 16:07:56 +09:00
parent f4959138d7
commit e758319231
5 changed files with 34 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
import { PostList } from "@/app/components/PostList"; import { PostList } from "@/app/components/PostList";
import { headers } from "next/headers"; import { headers } from "next/headers";
// Next 15: params/searchParams가 Promise가 될 수 있어 안전 언랩 처리합니다.
export default async function BoardDetail({ params, searchParams }: { params: any; searchParams: any }) { export default async function BoardDetail({ params, searchParams }: { params: any; searchParams: any }) {
const p = params?.then ? await params : params; const p = params?.then ? await params : params;
const sp = searchParams?.then ? await searchParams : searchParams; const sp = searchParams?.then ? await searchParams : searchParams;

View File

@@ -1,6 +1,7 @@
import { headers } from "next/headers"; import { headers } from "next/headers";
export default async function BoardsPage() { export default async function BoardsPage() {
// 서버에서 상대경로 fetch가 실패하므로 요청 헤더로 절대 URL을 구성합니다.
const h = await headers(); const h = await headers();
const host = h.get("host") ?? "localhost:3000"; const host = h.get("host") ?? "localhost:3000";
const proto = h.get("x-forwarded-proto") ?? "http"; const proto = h.get("x-forwarded-proto") ?? "http";

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
// 클라이언트 훅(useState/useEffect)을 사용하여 세션 표시/로그아웃을 처리합니다.
import { ThemeToggle } from "@/app/components/ThemeToggle"; import { ThemeToggle } from "@/app/components/ThemeToggle";
import { SearchBar } from "@/app/components/SearchBar"; import { SearchBar } from "@/app/components/SearchBar";
import { Button } from "@/app/components/ui/Button"; import { Button } from "@/app/components/ui/Button";
@@ -6,6 +7,7 @@ import React from "react";
export function AppHeader() { export function AppHeader() {
const [user, setUser] = React.useState<{ nickname: string } | null>(null); const [user, setUser] = React.useState<{ nickname: string } | null>(null);
// 헤더 마운트 시 세션 존재 여부를 조회해 로그인/로그아웃 UI를 제어합니다.
React.useEffect(() => { React.useEffect(() => {
fetch("/api/auth/session") fetch("/api/auth/session")
.then((r) => r.json()) .then((r) => r.json())

View File

@@ -1,9 +1,10 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { headers } from "next/headers"; import { headers } from "next/headers";
import React, { use } from "react";
export default async function PostDetail({ params }: { params: Promise<{ id: string }> }) { // 서버 전용 페이지: paramsPromise일 수 있어 안전 언랩 후 절대 URL로 fetch합니다.
const { id } = use(params); 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 h = await headers();
const host = h.get("host") ?? "localhost:3000"; const host = h.get("host") ?? "localhost:3000";
const proto = h.get("x-forwarded-proto") ?? "http"; const proto = h.get("x-forwarded-proto") ?? "http";

View File

@@ -1,17 +1,33 @@
"use client"; "use client";
// 클라이언트 라우터/검색파라미터 훅으로 새 글 작성 폼을 제어합니다.
import { useState } from "react"; import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { useToast } from "@/app/components/ui/ToastProvider"; import { useToast } from "@/app/components/ui/ToastProvider";
import { UploadButton } from "@/app/components/UploadButton"; import { UploadButton } from "@/app/components/UploadButton";
import { Editor } from "@/app/components/Editor"; import { Editor } from "@/app/components/Editor";
export default function NewPostPage({ searchParams }: { searchParams?: { boardId?: string; boardSlug?: string } }) { export default function NewPostPage() {
const router = useRouter(); const router = useRouter();
const { show } = useToast(); 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); const [loading, setLoading] = useState(false);
async function submit() { async function submit() {
try { try {
if (!form.boardId.trim()) {
show("boardId가 비어 있습니다");
return;
}
if (!form.title.trim()) {
show("제목을 입력하세요");
return;
}
if (!form.content.trim()) {
show("내용을 입력하세요");
return;
}
setLoading(true); setLoading(true);
const r = await fetch("/api/posts", { const r = await fetch("/api/posts", {
method: "POST", method: "POST",
@@ -19,11 +35,15 @@ export default function NewPostPage({ searchParams }: { searchParams?: { boardId
body: JSON.stringify({ ...form }), body: JSON.stringify({ ...form }),
}); });
const data = await r.json(); const data = await r.json();
if (!r.ok) throw new Error(JSON.stringify(data)); if (!r.ok) {
const fe = (data?.error?.fieldErrors ?? {}) as Record<string, string[]>;
const msg = data?.error?.message || Object.values(fe)[0]?.[0] || "작성 실패";
throw new Error(msg);
}
show("작성되었습니다"); show("작성되었습니다");
router.push(`/posts/${data.post.id}`); router.push(`/posts/${data.post.id}`);
} catch (e) { } catch (e) {
show("작성 실패"); show(e instanceof Error ? e.message : "작성 실패");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -37,7 +57,7 @@ export default function NewPostPage({ searchParams }: { searchParams?: { boardId
<UploadButton <UploadButton
multiple multiple
onUploaded={(url) => setForm((f) => ({ ...f, content: `${f.content}\n![image](${url})` }))} onUploaded={(url) => setForm((f) => ({ ...f, content: `${f.content}\n![image](${url})` }))}
{...(searchParams?.boardSlug ? require("@/lib/photoPresets").getPhotoPresetBySlug(searchParams.boardSlug) : {})} {...(boardSlug ? require("@/lib/photoPresets").getPhotoPresetBySlug(boardSlug) : {})}
/> />
<button disabled={loading} onClick={submit}>{loading ? "저장 중..." : "등록"}</button> <button disabled={loading} onClick={submit}>{loading ? "저장 중..." : "등록"}</button>
</div> </div>