"use client"; // 클라이언트 훅(useState/useEffect)을 사용하여 세션 표시/로그아웃을 처리합니다. import Image from "next/image"; import Link from "next/link"; import { SearchBar } from "@/app/components/SearchBar"; import React from "react"; export function AppHeader() { const [categories, setCategories] = React.useState }>>([]); const [openSlug, setOpenSlug] = React.useState(null); const [megaOpen, setMegaOpen] = React.useState(false); const [mobileOpen, setMobileOpen] = React.useState(false); const headerRef = React.useRef(null); const [headerBottom, setHeaderBottom] = React.useState(0); const navRefs = React.useRef>({}); const panelRef = React.useRef(null); const blockRefs = React.useRef>({}); const [leftPositions, setLeftPositions] = React.useState>({}); const [panelHeight, setPanelHeight] = React.useState(0); const [blockWidths, setBlockWidths] = React.useState>({}); // 카테고리 로드 React.useEffect(() => { fetch("/api/categories", { cache: "no-store" }) .then((r) => r.json()) .then((d) => setCategories(d?.categories || [])) .catch(() => setCategories([])); }, []); // ESC로 메가메뉴 닫기 React.useEffect(() => { if (!megaOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMegaOpen(false); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [megaOpen]); // 헤더 bottom 좌표를 계산해 fixed 패널 top에 적용 React.useEffect(() => { const measure = () => { const el = headerRef.current; if (!el) return; const rect = el.getBoundingClientRect(); setHeaderBottom(rect.bottom); }; measure(); window.addEventListener("resize", measure); window.addEventListener("scroll", measure, { passive: true }); return () => { window.removeEventListener("resize", measure); window.removeEventListener("scroll", measure); }; }, []); // 헤더 항목과 드롭다운 블록 정렬: 좌표 측정 및 패널 높이 계산 React.useEffect(() => { if (!megaOpen) return; const compute = () => { const container = panelRef.current; if (!container) return; const containerRect = container.getBoundingClientRect(); const nextPositions: Record = {}; categories.forEach((cat) => { const a = navRefs.current[cat.slug]; if (a) { const r = a.getBoundingClientRect(); nextPositions[cat.slug] = Math.max(0, r.left - containerRect.left); } }); setLeftPositions(nextPositions); // 각 블록의 가로 폭 = 다음 항목의 left까지 남은 거리 (마지막은 컨테이너 오른쪽 끝) const nextWidths: Record = {}; const ordered = categories .filter((c) => typeof nextPositions[c.slug] === "number") .sort((a, b) => (nextPositions[a.slug]! - nextPositions[b.slug]!)); ordered.forEach((cat, idx) => { const currentLeft = nextPositions[cat.slug]!; const nextLeft = idx < ordered.length - 1 ? nextPositions[ordered[idx + 1].slug]! : containerRect.width; const width = Math.max(0, nextLeft - currentLeft); nextWidths[cat.slug] = width; }); setBlockWidths(nextWidths); // 패널 높이 = 블록들 중 최대 높이 let maxH = 0; categories.forEach((cat) => { const el = blockRefs.current[cat.slug]; if (el) maxH = Math.max(maxH, el.offsetHeight); }); setPanelHeight(maxH); }; compute(); const onResize = () => compute(); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, [megaOpen, categories]); return (
logo
{mobileOpen && (
setMobileOpen(false)}>
e.stopPropagation()}>
{categories.map((cat) => (
{cat.name}
{cat.boards.map((b) => ( setMobileOpen(false)} className="rounded px-2 py-1 text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900"> {b.name} ))}
))}
)}
); }