'use client'; import { usePathname } from 'next/navigation'; import { useCallback, useState } from 'react'; /** 사이드바 최상위 링크 노드 — 그룹에 속하지 않는 단일 링크(예: 홈). */ export interface SidebarNavLink { type: 'link'; label: string; href: string; } /** 사이드바 그룹 노드 — 라우트가 없는 헤딩이며, 자식 링크를 펼침/접힘으로 보여준다. */ export interface SidebarNavGroup { type: 'group'; label: string; children: SidebarNavLink[]; } export type SidebarNavNode = SidebarNavLink | SidebarNavGroup; export interface SidebarNavState { /** href가 현재 경로와 일치하는가(활성 링크 여부). */ isLinkActive: (href: string) => boolean; /** 그룹 라벨이 현재 펼침 상태인가. */ isGroupExpanded: (label: string) => boolean; /** 그룹 펼침/접힘을 토글한다. */ toggleGroup: (label: string) => void; } /** tree에서 현재 pathname을 자식으로 포함하는 그룹의 라벨을 찾는다(없으면 null). */ function findActiveGroupLabel( tree: SidebarNavNode[], pathname: string ): string | null { const activeGroup = tree.find( (node): node is SidebarNavGroup => node.type === 'group' && node.children.some((link) => link.href === pathname) ); return activeGroup?.label ?? null; } /** * 사이드바 내비게이션 트리의 화면 로컬 상태(ViewModel 대응) — 그룹 라벨을 키로 펼침/접힘 * 여부를 들고 있고, 현재 경로 기준 활성 링크·활성 그룹 판정도 함께 제공한다. 트리 데이터 * 자체는 호출부가 소유하고 이 훅에 넘긴다(이 훅은 상태·판정 로직만 책임). * * 현재 경로가 속한 그룹은 항상 펼쳐져 있어야 한다(기획 ADM_MEM_101). 최초 렌더뿐 아니라 * 클라이언트 내비게이션으로 pathname이 바뀌는 매 순간에도 이 불변식을 지켜야 하므로, * `use-sidebar.ts`(모바일 오버레이를 pathname 변화에 맞춰 닫는 로직)가 쓰는 것과 동일한 * "렌더 중 상태 조정" 패턴(pathname 파생값의 변화를 렌더 중 감지해 즉시 setState)을 쓴다 — * effect 안에서 setState하면 `react-hooks/set-state-in-effect` 린트 규칙에 걸리고 * effect발 cascading render를 유발하므로 이 프로젝트에서는 쓰지 않는다. * * 사용자가 수동으로 접은 그룹을 강제로 다시 펼치지는 않는다 — activeGroupLabel 자체가 * 바뀔 때만(= 다른 그룹의 경로로 새로 이동했을 때만) 그 그룹을 편다. 같은 그룹 내 다른 * 링크로 이동하는 경우(activeGroupLabel 불변)에는 사용자가 접어둔 상태를 그대로 둔다. */ export function useSidebarNav(tree: SidebarNavNode[]): SidebarNavState { const pathname = usePathname(); const activeGroupLabel = findActiveGroupLabel(tree, pathname); const [expandedGroups, setExpandedGroups] = useState< Record >(() => (activeGroupLabel ? { [activeGroupLabel]: true } : {})); const [prevActiveGroupLabel, setPrevActiveGroupLabel] = useState(activeGroupLabel); if (activeGroupLabel !== prevActiveGroupLabel) { setPrevActiveGroupLabel(activeGroupLabel); if (activeGroupLabel !== null) { setExpandedGroups((prev) => ({ ...prev, [activeGroupLabel]: true })); } } const isLinkActive = useCallback( (href: string) => pathname === href, [pathname] ); const isGroupExpanded = useCallback( (label: string) => Boolean(expandedGroups[label]), [expandedGroups] ); const toggleGroup = useCallback((label: string) => { setExpandedGroups((prev) => ({ ...prev, [label]: !prev[label] })); }, []); return { isLinkActive, isGroupExpanded, toggleGroup }; }