Files
frontend/apps/dm_new/components/Lnb.tsx
T
2026-02-26 14:39:25 +09:00

179 lines
5.8 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import {
fetchAnonymousMenu,
findActiveTopMenu,
hasAnyChild,
isMenuActive,
type MenuItem,
} from "@/app/store/common";
export type LnbItem = {
label: string;
href?: string;
children?: LnbItem[];
};
function findOpenMenuCode(items: MenuItem[], pathname: string): number | null {
const matched = items.find((item) => {
if (isMenuActive(pathname, item)) return true;
return (item.children ?? []).some((child) => isMenuActive(pathname, child));
});
return matched?.menuCd ?? null;
}
export default function Lnb() {
const pathname = usePathname();
const [menus, setMenus] = useState<MenuItem[]>([]);
const [openMenuCode, setOpenMenuCode] = useState<number | null>(null);
useEffect(() => {
let isMounted = true;
(async () => {
try {
const loaded = await fetchAnonymousMenu();
if (!isMounted) return;
console.log("Fetched LNB menus:", loaded);
setMenus(loaded);
} catch (error) {
console.error("Failed to load DN LNB menu", error);
if (!isMounted) return;
setMenus([]);
}
})();
return () => {
isMounted = false;
};
}, []);
const currentTopMenu = useMemo(
() => findActiveTopMenu(menus, pathname),
[menus, pathname],
);
const secondLevelMenus = useMemo(
() => currentTopMenu?.children ?? [],
[currentTopMenu],
);
// console.log("LNB render", { pathname, currentTopMenu, secondLevelMenus, menus });
useEffect(() => {
const defaultOpen = findOpenMenuCode(secondLevelMenus, pathname);
setOpenMenuCode(defaultOpen);
}, [pathname, secondLevelMenus]);
return (
<aside
className={[
"fixed left-0 top-[83px]", // ✅ 화면 고정 + TopNav 아래로
"w-[260px] min-w-[260px] shrink-0",
"h-[calc(100vh-83px)]", // ✅ 남은 높이만큼
"overflow-y-auto", // ✅ LNB 내부 스크롤
"bg-white z-50", // ✅ 겹침 방지
].join(" ")}
>
{/* 타이틀 */}
<div className="flex h-[120px] items-center justify-center bg-gradient-to-r from-cyan-500 to-green-500 text-[40px] font-bold text-white">
{currentTopMenu?.menuKornNm?.trim().split(/\s+/).pop() ?? ""}
</div>
{/* 메뉴 */}
<ul>
{secondLevelMenus.map((item) => {
const hasChildren = hasAnyChild(item);
const active = isMenuActive(pathname, item);
if (!hasChildren) {
return (
<li key={item.menuCd}>
<Link
href={item.urlPath || "#"}
className={[
"block border-b border-slate-300 px-5 py-4 text-center text-base font-semibold transition-colors",
active
? "cursor-default bg-white text-sky-500"
: "bg-[#F0F0F0] text-[#767676] hover:bg-white hover:text-sky-500",
].join(" ")}
>
{item.menuKornNm}
</Link>
</li>
);
}
const isOpen = openMenuCode === item.menuCd;
const childMenus = item.children ?? [];
return (
<li key={item.menuCd}>
<button
type="button"
onClick={() =>
setOpenMenuCode((prev) => (prev === item.menuCd ? null : item.menuCd))
}
className={[
"flex w-full items-center justify-between border-b border-slate-300 px-5 py-6 text-base font-semibold",
active ? "bg-slate-100 text-slate-900" : "bg-slate-100 text-slate-500",
].join(" ")}
aria-expanded={isOpen}
>
<span className="w-full text-center">{item.menuKornNm}</span>
<span
aria-hidden
className={[
"ml-3 shrink-0 transition-transform duration-200",
isOpen ? "rotate-180" : "rotate-0",
].join(" ")}
>
<img
src="/images/landing/drop_arrow.svg"
alt="menu toggle icon"
className="h-7 w-7"
/>
</span>
</button>
<div
className={[
"grid transition-[grid-template-rows] duration-200 ease-out",
isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
].join(" ")}
>
<div className="overflow-hidden bg-white">
<ul>
{childMenus.map((child) => {
const childActive = isMenuActive(pathname, child);
return (
<li key={child.menuCd}>
<Link
href={child.urlPath || "#"}
className={[
"block border-b border-slate-200 bg-slate-50 px-5 py-4 pl-[60px] text-[18px] font-medium transition-colors",
childActive
? "cursor-default bg-white text-sky-500"
: "text-slate-500 hover:bg-white hover:text-sky-500",
].join(" ")}
>
<span className="mr-2 text-current">-</span>
{child.menuKornNm}
</Link>
</li>
);
})}
</ul>
</div>
</div>
</li>
);
})}
</ul>
</aside>
);
}