Files
frontend/apps/dm_new/app/(landing)/reuse/_components/Lnb.tsx
T
2026-02-26 14:15:24 +09:00

183 lines
6.7 KiB
TypeScript

// apps/dm/app/(landing)/reuse/_components/Lnb.tsx
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
export type LnbItem = {
label: string;
href?: string; // href 없는 부모(2뎁스 컨테이너)도 허용
children?: LnbItem[];
};
function isExactActive(pathname: string, href?: string) {
if (!href) return false;
return pathname === href;
}
export default function Lnb({ title, items }: { title: string; items: LnbItem[] }) {
const pathname = usePathname();
// ✅ 현재 경로가 속한 2depth 그룹을 기본 open
const defaultOpenLabel = useMemo(() => {
const found = items.find((it) => it.children?.some((c) => isExactActive(pathname, c.href)));
return found?.label ?? null;
}, [items, pathname]);
// ✅ 아코디언: 한 번에 하나만 열리도록 openLabel 1개만 관리
const [openLabel, setOpenLabel] = useState<string | null>(defaultOpenLabel);
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="h-[120px] flex items-center justify-center
text-white text-[40px] font-bold
bg-gradient-to-r from-cyan-500 to-green-500"
>
{title}
</div>
{/* 메뉴 */}
<ul>
{items.map((item) => {
const hasChildren = !!item.children?.length;
// 1뎁스 active
const active1 = isExactActive(pathname, item.href);
// 2뎁스 active(자식 중 exact)
const active2 = item.children?.some((c) => isExactActive(pathname, c.href)) ?? false;
const active = active1 || active2;
// ✅ children 있는 경우: 아코디언 헤더 + 패널
if (hasChildren && !item.href) {
const isOpen = openLabel === item.label;
return (
<li key={item.label}>
{/* 아코디언 헤더 */}
<button
type="button"
onClick={() => setOpenLabel((prev) => (prev === item.label ? null : item.label))}
className={[
"w-full px-5 py-6",
"flex items-center justify-between",
"border-b border-slate-300",
"text-center 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.label}</span>
{/* ✅ 우측 화살표(이미지처럼) */}
<span
aria-hidden
className={[
"ml-3 shrink-0 transition-transform duration-200",
isOpen ? "rotate-180" : "rotate-0",
].join(" ")}
>
<Image
src="/images/landing/drop_arrow.svg"
alt="드롭다운 화살표 아이콘"
width={28}
height={28}
className="w-7 h-7"
/>
</span>
</button>
{/* 패널: height auto 애니메이션(그리드 트릭) */}
<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>
{item.children!.map((child) => {
const childActive = isExactActive(pathname, child.href);
return (
<li key={child.href ?? child.label}>
{child.href ? (
<Link
href={child.href}
className={[
"block px-5 py-4 pl-[60px] text-[18px] font-medium",
"border-b border-slate-200 transition-colors",
childActive
? "bg-white text-sky-500 cursor-default"
: "bg-slate-50 text-slate-500 hover:bg-white hover:text-sky-500",
].join(" ")}
>
{/* 이미지처럼 불릿 필요하면 */}
<span className="mr-2 text-current">
•
</span>
{child.label}
</Link>
) : (
<div className="block px-5 py-4 text-[18px] font-medium border-b border-slate-200 bg-slate-50 text-slate-500">
{child.label}
</div>
)}
</li>
);
})}
</ul>
</div>
</div>
</li>
);
}
// ✅ 일반 1뎁스 메뉴(기존 유지)
return (
<li key={item.href ?? item.label}>
{item.href ? (
<Link
href={item.href}
className={[
"block px-5 py-4 text-center text-base font-semibold",
"border-b border-slate-300 transition-colors",
active
? "bg-white text-sky-500 cursor-default"
: "bg-[#F0F0F0] text-[#767676] hover:bg-white hover:text-sky-500",
].join(" ")}
>
{item.label}
</Link>
) : (
<div
className={[
"block px-5 py-4 text-center text-base font-semibold",
"border-b border-slate-300",
"bg-slate-100 text-slate-500",
].join(" ")}
>
{item.label}
</div>
)}
</li>
);
})}
</ul>
</aside>
);
}