File name
Commit message
Commit date
File name
Commit message
Commit date
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, 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<string, boolean>
>(() => (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 };
}