Files
msgapp/src/app/components/AppHeader.tsx

117 lines
5.1 KiB
TypeScript
Raw Normal View History

2025-10-10 11:22:43 +09:00
"use client";
// 클라이언트 훅(useState/useEffect)을 사용하여 세션 표시/로그아웃을 처리합니다.
import Image from "next/image";
import Link from "next/link";
import { SearchBar } from "@/app/components/SearchBar";
2025-10-10 11:22:43 +09:00
import React from "react";
export function AppHeader() {
const [categories, setCategories] = React.useState<Array<{ id: string; name: string; slug: string; boards: Array<{ id: string; name: string; slug: string }> }>>([]);
const [openSlug, setOpenSlug] = React.useState<string | null>(null);
2025-10-13 08:49:51 +09:00
const [mobileOpen, setMobileOpen] = React.useState(false);
// 카테고리 로드
2025-10-10 11:22:43 +09:00
React.useEffect(() => {
fetch("/api/categories", { cache: "no-store" })
.then((r) => r.json())
.then((d) => setCategories(d?.categories || []))
.catch(() => setCategories([]));
2025-10-10 11:22:43 +09:00
}, []);
return (
2025-10-13 09:45:53 +09:00
<header className="relative flex items-center justify-between px-4 py-3 border-b bg-white/80 backdrop-blur supports-[backdrop-filter]:bg-white/60">
<div className="flex items-center gap-3 z-[100]">
2025-10-13 08:49:51 +09:00
<button
aria-label="메뉴 열기"
aria-expanded={mobileOpen}
aria-controls="mobile-menu"
onClick={() => setMobileOpen((v) => !v)}
2025-10-13 09:45:53 +09:00
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-neutral-200 xl:hidden"
2025-10-13 08:49:51 +09:00
>
<span className="flex flex-col items-center justify-center gap-1">
<span className="block h-0.5 w-4 bg-neutral-800" />
<span className="block h-0.5 w-4 bg-neutral-800" />
<span className="block h-0.5 w-4 bg-neutral-800" />
</span>
</button>
<Link href="/" aria-label="홈" className="shrink-0">
2025-10-13 09:45:53 +09:00
<Image src="/logo.png" alt="logo" width={120} height={28} priority className="w-20 md:w-28 xl:w-[120px] h-auto" />
</Link>
</div>
2025-10-13 09:13:52 +09:00
<nav className="flex items-center gap-4">
<div className="hidden items-center gap-4 xl:flex">
2025-10-13 08:49:51 +09:00
{categories.map((cat) => {
const isOpen = openSlug === cat.slug;
return (
<div
key={cat.id}
onMouseEnter={() => setOpenSlug(cat.slug)}
onMouseLeave={() => setOpenSlug((s) => (s === cat.slug ? null : s))}
className="relative group"
>
<div className="relative">
<Link
href={`/boards?category=${cat.slug}`}
className="px-2 py-2 text-sm font-medium text-neutral-700 transition-colors duration-200 hover:text-neutral-900"
>
{cat.name}
</Link>
<span
className={`pointer-events-none absolute left-1 right-1 -bottom-0.5 h-0.5 origin-left rounded bg-neutral-900 transition-all duration-200 ${
isOpen ? "scale-x-100 opacity-100" : "scale-x-0 opacity-0 group-hover:opacity-100 group-hover:scale-x-100"
}`}
/>
</div>
<div
className={`absolute left-0 top-full z-50 mt-2 min-w-56 rounded-md border border-neutral-100 bg-white p-2 shadow-[0_8px_24px_rgba(0,0,0,0.08)] transition-all duration-200 ${
isOpen ? "opacity-100 translate-y-0" : "pointer-events-none opacity-0 -translate-y-1"
}`}
>
<div className="flex flex-col gap-1">
{cat.boards.map((b) => (
2025-10-13 08:49:51 +09:00
<Link
key={b.id}
href={`/boards/${b.id}`}
className="rounded px-2 py-1 text-sm text-neutral-700 transition-colors duration-150 hover:bg-neutral-100 hover:text-neutral-900"
>
{b.name}
</Link>
))}
</div>
</div>
2025-10-13 08:49:51 +09:00
</div>
);
})}
2025-10-13 09:13:52 +09:00
</div>
<div className="hidden md:block">
<SearchBar/>
</div>
</nav>
2025-10-13 08:49:51 +09:00
{mobileOpen && (
2025-10-13 09:45:53 +09:00
<div className="fixed inset-0 h-[100vh] z-40 bg-black/60 backdrop-blur-sm" onClick={() => setMobileOpen(false)}>
<div className="absolute left-0 xl:top-0 h-[100vh] w-4/5 max-w-sm bg-white p-4 shadow-xl overflow-y-auto" onClick={(e) => e.stopPropagation()}>
<div className="mb-3 h-10 flex items-center justify-between">
2025-10-13 08:49:51 +09:00
</div>
2025-10-13 09:45:53 +09:00
<div className="flex flex-col gap-3">
2025-10-13 08:49:51 +09:00
<SearchBar />
{categories.map((cat) => (
<div key={cat.id}>
<div className="my-2 font-semibold text-neutral-800">{cat.name}</div>
<div className="flex flex-col gap-1">
{cat.boards.map((b) => (
<Link key={b.id} href={`/boards/${b.id}`} onClick={() => setMobileOpen(false)} className="rounded px-2 py-1 text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900">
{b.name}
</Link>
))}
</div>
</div>
))}
<div className="mt-3" />
</div>
</div>
</div>
)}
</header>
);
}