File name
Commit message
Commit date
08-14
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 };
}