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 Link from 'next/link';
import type {
SidebarMenu,
SidebarMenuChild,
SidebarMenuItem,
} from '@/lib/domain/sidebar-menu';
import { useSidebarNav, type SidebarNavState } from '../_hooks/use-sidebar-nav';
import styles from './admin-sidebar.module.scss';
import { SidebarMenuIcon } from './sidebar-menu-icon';
interface AdminSidebarProps {
/** 서버가 계정 권한에 맞춰 구성해 준 메뉴. 이 컴포넌트는 라우트 목록을 알지 못한다. */
menu: SidebarMenu;
}
/**
* Basic 레이아웃 사이드바 — 시안 snb-area(3041:11860)를 옮겼다. 섹션별 GNB로 이루어지고,
* GNB는 1depth > 2depth > 3depth 중첩이다.
*
* **메뉴 데이터를 소유하지 않는다.** 권한마다 항목이 달라 매 요청 서버가 조회해 넘겨주며
* (`fetchSidebarMenu`), 여기서는 받은 트리를 그리고 펼침·활성 상태만 판정한다. 그래서 새 화면이
* 생겨도 이 파일은 손대지 않는다.
*/
export function AdminSidebar({ menu }: AdminSidebarProps) {
const nav = useSidebarNav(menu);
return (
<nav className={styles.sidebar} aria-label="주요 메뉴">
{menu.sections.map((section) => (
<section
key={section.id}
className={`${styles.section} ${styles.sectionNav}`}
>
<h2 className={styles.sectionTitle}>{section.label}</h2>
<ul className={styles.list}>
{section.items.map((item) => (
<SidebarItem key={item.id} item={item} nav={nav} />
))}
</ul>
</section>
))}
</nav>
);
}
/** 1depth — 자식이 있으면 펼침 버튼, 없으면 링크다. */
function SidebarItem({
item,
nav,
}: {
item: SidebarMenuItem;
nav: SidebarNavState;
}) {
const hasChildren = (item.children?.length ?? 0) > 0;
const isActive = nav.containsActive(item);
const isOpen = nav.isItemOpen(item);
const rowClass = `${styles.itemButton} ${isActive ? styles.itemButtonActive : ''}`;
const inner = (
<>
<span className={styles.itemIcon}>
<SidebarMenuIcon name={item.icon} />
</span>
<span className={styles.itemLabel}>{item.label}</span>
</>
);
return (
<li className={styles.item}>
{hasChildren ? (
<button
type="button"
className={rowClass}
aria-expanded={isOpen}
onClick={() => nav.toggleItem(item.id)}
>
{inner}
</button>
) : (
<Link href={item.href ?? '#'} className={rowClass}>
{inner}
</Link>
)}
{hasChildren && isOpen && (
<ul className={styles.childGroup}>
{item.children?.map((child) => (
<SidebarChild key={child.id} child={child} nav={nav} />
))}
</ul>
)}
</li>
);
}
/** 2depth — 자식이 있으면 펼침 버튼(별·캐럿 표시), 없으면 링크다. */
function SidebarChild({
child,
nav,
}: {
child: SidebarMenuChild;
nav: SidebarNavState;
}) {
const hasChildren = (child.children?.length ?? 0) > 0;
const isOpen = nav.isChildOpen(child);
const isActive = nav.isActive(child.href);
const rowClass = `${styles.childRow} ${
isActive || (hasChildren && isOpen) ? styles.childRowActive : ''
}`;
// 시안은 자식이 없는 항목도 별·캐럿 자리를 비워 두고 투명하게만 만든다 — 라벨 시작
// 위치를 줄마다 맞추기 위해서다.
const mark = (
<span
className={`${styles.childMark} ${hasChildren ? '' : styles.childMarkHidden}`}
>
<i className="fox-ico fox-ico-Star" aria-hidden="true" />
</span>
);
const caret = (
<span
className={`${styles.childMark} ${hasChildren ? '' : styles.childMarkHidden}`}
>
{isOpen ? <i className="fox-ico fox-ico-CaretUp" aria-hidden="true" /> : <i className="fox-ico fox-ico-CaretDown" aria-hidden="true" />}
</span>
);
return (
<li className={styles.child}>
{hasChildren ? (
<button
type="button"
className={rowClass}
aria-expanded={isOpen}
onClick={() => nav.toggleChild(child.id)}
>
{mark}
<span className={styles.childLabel}>{child.label}</span>
{caret}
</button>
) : (
<Link href={child.href ?? '#'} className={rowClass}>
{mark}
<span className={styles.childLabel}>{child.label}</span>
{caret}
</Link>
)}
{hasChildren && isOpen && (
<ul className={styles.leafGroup}>
{child.children?.map((leaf) => (
<li key={leaf.id}>
<Link
href={leaf.href}
className={`${styles.leaf} ${
nav.isActive(leaf.href) ? styles.leafActive : ''
}`}
>
<span className={styles.childMark}>
<i className="fox-ico fox-ico-Star" aria-hidden="true" />
</span>
{leaf.label}
</Link>
</li>
))}
</ul>
)}
</li>
);
}