File name
Commit message
Commit date
File name
Commit message
Commit date
09-16
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
'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<Record<string, boolean>>({});
const [childOverrides, setChildOverrides] = useState<Record<string, boolean>>(
{}
);
// 현재 경로를 품은 가지 — 기본 펼침 대상이다.
const openByPath = useMemo(() => {
const items = new Set<string>();
const children = new Set<string>();
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,
};
}