From 4b1c60a9bf3d0409bed3e00f1490ecce9be94ddf Mon Sep 17 00:00:00 2001 From: mota Date: Mon, 13 Oct 2025 11:34:00 +0900 Subject: [PATCH] header --- prisma/seed.js | 2 +- src/app/components/AppHeader.tsx | 168 +++++++++++++++++++++++++------ 2 files changed, 137 insertions(+), 33 deletions(-) diff --git a/prisma/seed.js b/prisma/seed.js index 8978486..31c7d34 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -11,7 +11,7 @@ async function upsertCategories() { { name: "제휴업소 정보", slug: "partner-info", sortOrder: 4, status: "active" }, { name: "방문후기", slug: "reviews", sortOrder: 5, status: "active" }, { name: "소통방", slug: "community", sortOrder: 6, status: "active" }, - { name: "광고/제휴 문반", slug: "ads-affiliates", sortOrder: 7, status: "active" }, + { name: "광고/제휴", slug: "ads-affiliates", sortOrder: 7, status: "active" }, ]; const map = {}; for (const c of categories) { diff --git a/src/app/components/AppHeader.tsx b/src/app/components/AppHeader.tsx index 3275a7d..878af60 100644 --- a/src/app/components/AppHeader.tsx +++ b/src/app/components/AppHeader.tsx @@ -8,7 +8,16 @@ 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" }) @@ -16,8 +25,79 @@ export function AppHeader() { .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 ( -
+
-