'use client'; import { usePathname } from 'next/navigation'; import { useCallback, useMemo, useState } from 'react'; import type { SidebarMenu, SidebarMenuChild, SidebarMenuItem, } from '@/lib/domain/sidebar-menu'; /** * 사이드바 펼침·활성 상태(ViewModel). 트리는 서버가 준 값을 그대로 받고, 이 훅은 "지금 어디에 * 있는가"와 "무엇이 펼쳐져 있는가"만 판정한다. * * 펼침은 **현재 경로에서 파생된 기본값 + 사용자가 누른 값**의 2층이다. 그래서 링크를 눌러 * 이동하면 그 가지가 저절로 열리고, 사용자가 직접 접으면 그 선택이 유지된다. 열림 목록을 * 상태로만 들고 있으면 이동할 때마다 손으로 다시 열어야 한다. */ export interface SidebarNavState { /** 이 href가 현재 경로인가. */ isActive: (href?: string) => boolean; /** 1depth 항목이 펼쳐져 있는가. */ isItemOpen: (item: SidebarMenuItem) => boolean; toggleItem: (id: string) => void; /** 2depth 항목이 펼쳐져 있는가. */ isChildOpen: (child: SidebarMenuChild) => boolean; toggleChild: (id: string) => void; /** 자손 중에 현재 경로가 있는가 — 1depth를 활성으로 칠할지 판단한다. */ containsActive: (item: SidebarMenuItem) => boolean; } function childContainsPath(child: SidebarMenuChild, pathname: string): boolean { return ( child.href === pathname || (child.children?.some((leaf) => leaf.href === pathname) ?? false) ); } function itemContainsPath(item: SidebarMenuItem, pathname: string): boolean { return ( item.href === pathname || (item.children?.some((child) => childContainsPath(child, pathname)) ?? false) ); } export function useSidebarNav(menu: SidebarMenu): SidebarNavState { const pathname = usePathname(); // 사용자가 직접 누른 것만 담는다. 없으면 경로에서 파생된 기본값을 쓴다. const [itemOverrides, setItemOverrides] = useState>({}); const [childOverrides, setChildOverrides] = useState>( {} ); // 현재 경로를 품은 가지 — 기본 펼침 대상이다. const openByPath = useMemo(() => { const items = new Set(); const children = new Set(); for (const section of menu.sections) { for (const item of section.items) { if (!itemContainsPath(item, pathname)) { continue; } items.add(item.id); for (const child of item.children ?? []) { if (childContainsPath(child, pathname)) { children.add(child.id); } } } } return { items, children }; }, [menu, pathname]); const isActive = useCallback( (href?: string) => href !== undefined && href === pathname, [pathname] ); const isItemOpen = useCallback( (item: SidebarMenuItem) => itemOverrides[item.id] ?? openByPath.items.has(item.id), [itemOverrides, openByPath] ); const isChildOpen = useCallback( (child: SidebarMenuChild) => childOverrides[child.id] ?? openByPath.children.has(child.id), [childOverrides, openByPath] ); const toggleItem = useCallback( (id: string) => setItemOverrides((prev) => ({ ...prev, [id]: !(prev[id] ?? openByPath.items.has(id)), })), [openByPath] ); const toggleChild = useCallback( (id: string) => setChildOverrides((prev) => ({ ...prev, [id]: !(prev[id] ?? openByPath.children.has(id)), })), [openByPath] ); const containsActive = useCallback( (item: SidebarMenuItem) => itemContainsPath(item, pathname), [pathname] ); return { isActive, isItemOpen, toggleItem, isChildOpen, toggleChild, containsActive, }; }