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