24 KiB
GNB (Pattern)
2026-10-11 v2 추가 요청: 콘텐츠 LNB와 본문 사이 세로 구분선은 공통
gnb.css에서 1px solid--hm-sem-stroke-input-default(Default#DEDBD2)를 사용한다. 기존--hm-sem-stroke-default(#9A9284) 연결만 교체하며 외곽 Border·배경·Green LNB 2Depth 및 Storybook 목차의 Alpha 8%는 유지한다.
2026-10-10 추가 수정 요청 기준: 기존 Reference 콘텐츠 LNB는 본문과 동일 배경·세로 divider 1개, 아이콘 기본 생략(선택적 허용), 해당 컬러 계열 선택·Hover. data-hm-color="brown" 범위는 Brown 색상. 신규 AI GNB_ONLY Sidebar 금지 정책 유지. 과거 검증 기록은 당시 결과이며 현재 검증은 추가 수정 보고서를 따릅니다.
SINGLE NAVIGATION MODE. 독립 GNB / LNB / Dashboard Mode는 함께 사용하지 않는다. 2026-10-08 확정: GNB ONLY는 기존 LNB 부품을 재사용하는 내부 Sidebar를 포함할 수 있다(§15). 공통 정책이 이전 조합 기록보다 우선한다.
현재 HM 구현의 기준 문서. 공통 원칙은 docs/guidelines/프로젝트-지침.md(§6.10 Pattern 계층, §6.11 Layout 단계 기준)를 따른다.
Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (Patterns/GNB, Variant B Top-header)
Implementation: current @hm-design/ui primitives / patterns
1. 상태
| 항목 | 내용 |
|---|---|
| 구현 상태 | 구현 완료 (2026-10-03 ui-viz 시각 재정합) |
| 기반 | HM primitives 조합 (새 dependency 없음) |
| 대표 Story | Patterns/Layout/GNB → GNB (verification: Default, Navigation Menu Open, User Menu Open, Long Labels, With Logo, Utilities, Mobile, Mobile Menu Open) |
| 이력 | 첫 구현은 밝은 surface 64px였다. Layout은 ui-viz 시각 우선이라는 결정(지침 §6.11)으로 green 70px로 재정합했다 |
| 보류 | §13 |
2. 목적 / 사용 범위
서비스 전역 상단 바(브랜드, 주 메뉴, 사용자). 새 Navigation/Menu primitive가 아니라 기존 primitive의 조합이다. 메뉴 데이터는 소비자가 children으로 그린다.
| 역할 | |
|---|---|
| GNB | 전역 영역·서비스 이동 |
| LNB | 현재 영역 내부의 세부 navigation (lnb.md) |
| Dashboard | 별도의 Dashboard Navigation Mode. GNB와 결합하지 않는다 |
ui-viz에서 GNB(Variant B, top-header)와 LNB(Variant A, sidebar)는 서로 대체하는 별개 Layout이다. 독립 Navigation Mode의 혼합은 금지한다. GNB ONLY의 내부 Sidebar에서 LNB 구현 부품을 재사용하는 것은 별도 LNB Mode 추가가 아니다(§15).
3. 구성 요소
| export | 역할 |
|---|---|
GNB |
<header> 바. sticky prop |
GNBBrand |
브랜드 링크(<a>, asChild 가능). 로고(img/svg, 있을 때만) + <span>(<small> eyebrow, <strong> 이름) |
GNBNav |
주 메뉴 영역(desktop 전용). 안에 NavigationMenu viewport={false} |
GNBUtilities |
오른쪽 영역 |
GNBUser |
사용자 trigger(Avatar + 이름/역할, 글자는 mobile에서 숨김). DropdownMenuTrigger asChild로 사용 |
GNBMobileMenu / GNBMobileLink / GNBMobileGroup |
mobile 햄버거 + Sheet + 링크/그룹 |
검색·알림은 컴포넌트가 아니다. ghost Button + Tooltip + Badge + Popover(+Command) 조합을 GNBUtilities에 둔다(Story Utilities).
4. 구조
<GNB sticky>
<GNBMobileMenu title="메뉴">…<GNBMobileLink href active>…</GNBMobileLink></GNBMobileMenu>
<GNBBrand href="/"><span><small>한맥 업무 시스템</small><strong>프로젝트 대시보드</strong></span></GNBBrand>
<GNBNav>
<NavigationMenu viewport={false} aria-label="주 메뉴">
<NavigationMenuList>
<NavigationMenuItem><NavigationMenuLink href active>대시보드</NavigationMenuLink></NavigationMenuItem>
<NavigationMenuItem value="projects">
<NavigationMenuTrigger aria-current="page">프로젝트</NavigationMenuTrigger>
<NavigationMenuContent><ul><li><NavigationMenuLink href>…</NavigationMenuLink></li></ul></NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</GNBNav>
<GNBUtilities>
<DropdownMenu><DropdownMenuTrigger asChild><GNBUser name description><Avatar/></GNBUser></DropdownMenuTrigger>…</DropdownMenu>
</GNBUtilities>
</GNB>
viewport={false}가 필수다: 2Depth 패널은 각 trigger 아래에서 content-width(max-content)로 열리며 짧은 패널도 trigger 폭 이상을 유지한다. trigger 중심에 정렬하고 viewport 가장자리에서는 clamp한다. 긴 라벨은 한 줄을 유지하며 최대 가용 폭을 넘으면 패널 안에서 가로 스크롤한다(공유 viewport는 메뉴 전체 아래에 중앙 정렬된다).
5. ui-viz 비교 (2026-10-03, 원본: dashboard-composition.md §19, top-header-specimen.css/jsx)
HISTORICAL_RECORD: 아래 표는 2026-10-03 비교 기록이다. 현재 패널 폭·정렬은 §4, Current/Open 우선순위는 §7을 따른다.
| 항목 | ui-viz | 기존 HM(첫 구현) | 최종 HM | 분류 |
|---|---|---|---|---|
| 높이 | Header 70 (위 4 + 메뉴 면 66) | 64 | 70 (--hm-gnb-height) |
MATCHED |
| 배경 | 세로 4-stop #1F8265/#165C47/#114838/#08241C 0/30/65/100%, 높이 82px(70+12), 반복 없음 |
흰 surface | 같은 stop(--hm-green-tone-1~4), 82px 단일 gradient |
MATCHED |
| 콘텐츠 접점 | 12px, 콘텐츠 상단 radius 12, 뒤로 gradient 연속 | 없음 | ::after 12px strip(--hm-gnb-join). 콘텐츠가 positioned·뒤에 오고 radius를 가져야 한다 |
IMPLEMENTATION_CHANGE |
| 로고 | 있을 때만, 가로(로고 왼쪽·이름 오른쪽), 없으면 영역·간격 없음 | 이름만 | 같음. eyebrow(11/700, #BDDFD4) + 이름(16/700), 1px 글자 테두리 |
MATCHED |
| 메뉴 위치 | grid 좌우 같은 폭 → header 전체 가운데 | brand 옆 왼쪽 | minmax(0,1fr) auto minmax(0,1fr) grid, 중앙 오차 0 |
MATCHED |
| 메뉴 항목 | 위 4·높이 66, 최소 112, padding 0 16, radius 4 4 0 0, White 700 16, 아이콘 없음, 글자 테두리 없음 | 34px text button | 같음 (Navigation Menu를 GNB 범위 안에서만 재색칠) | MATCHED |
| hover | Black 10% + White | --accent |
Black 10% + White | MATCHED |
| current / selected shape | Beige(bg.content) + #123328, 위 radius 4, 아래 좌우 12px 오목 곡선, hover가 덮지 않음 |
연한 초록 면 | 같음 (data-active/aria-current + ::before/::after radial) |
MATCHED |
| 열림(open) | White 면 + #123328, 곡선 숨김 |
--accent |
같음 (data-state=open). current와 구분 |
MATCHED |
| 2뎁스 패널 | White, 부모 폭, 항목 56/16/16·24·700 가운데, hover·현재 Beige, 하단 radius 8, shadow 0 6px 16px .1 |
320px floating | 같음 | MATCHED |
| utility | 원형 버튼·avatar 제거(2026-10-01 결정). 이름·역할만 | 검색·알림·Avatar | 기본은 이름·역할만. 검색·알림은 선택 조합(흰 ghost Button) | MATCHED / 선택 조합 |
| 사용자 영역 | 이름 16/700 + 역할 12 #BDDFD4, 오른쪽 끝 (ui-viz는 avatar 없음) |
Avatar + 글자 | HM Avatar(default 32px) + 글자 + Dropdown Menu. mobile은 Avatar만 | INTENTIONAL_CHANGE (2026-10-03 사용자 지시로 Avatar 추가) |
| desktop 구조 | Brand / Menu / Profile | 같음 | 같음 | MATCHED |
| mobile 구조 | 없음(≤1100px/48px은 OUT_OF_SCOPE) | 햄버거 + Sheet | 유지. breakpoint 1024px | INTENTIONAL_CHANGE (ui-viz에 기준 없음) |
| 2뎁스 열기 | hover/click/ArrowDown, Escape + focus 복귀 | Radix | Radix Navigation Menu(hover delay, 키보드 model) | IMPLEMENTATION_CHANGE |
6. Public API
GNB:headerprops +sticky(기본 false).data-slot="gnb", sticky면data-sticky.GNBUser:buttonprops +name(필수),description, children = Avatar(default size, 이미지 실패 시AvatarFallback). 기본aria-label은"<name> <description> 사용자 메뉴"이며 description이 없으면"<name> 사용자 메뉴"다. 명시적aria-label/aria-labelledby는 소비자가 지정할 수 있고, 화면에 보이는 이름·설명을 포함해야 한다.data-slot은 spread 뒤에 둬서DropdownMenuTrigger asChild가 덮지 않는다.GNBMobileMenu: 햄버거(lg:hidden)와 Sheet.title,side,open/defaultOpen/onOpenChange.GNBMobileLink는 routerpreventDefault가 있어도 닫히도록 context로 직접 닫는다(Radix Close는 prevented click을 건너뛴다).
7. Navigation 원칙
- 주 메뉴는 HM Navigation Menu다.
<nav>landmark는 Navigation Menu가 렌더하므로GNBNav는 role을 만들지 않는다. Menubar는 쓰지 않는다. - 현재 위치: Link는
active(data-active+aria-current="page"), 하위가 있는 Trigger는 소비자가aria-current="page"를 준다. 하위 링크의active는 패널 안 Beige. - 상태 구분: Hover = Black 10%, Current = Beige + Primary, Open = White + Primary(패널과 이어짐). Current + Hover는 Current, Open + Hover는 Open을 유지하며 Current + Open에서는 Open visual이 우선한다. Focus는 GNB의 contextual navigation outline을 유지한다. Radix의
data-state,data-active,aria-current로 시각만 맞춘다. - Navigation Menu primitive는 수정하지 않았다. 모든 재색칠은
[data-slot="gnb"] …안에서만 일어나며 Navigation Menu Story는 변하지 않는다.
8. Desktop / Mobile
AI 구현 필수: GNBNav는 모바일에서 숨겨질 뿐 모바일 메뉴를 자동 생성하지 않는다. 최초 구현부터 Desktop GNBNav와 Mobile GNBMobileMenu를 함께 넣고 같은 메뉴 데이터·현재 경로를 사용한다. GNBMobileMenu가 햄버거와 Sheet를 모두 제공하므로 별도 메뉴 버튼이나 Sheet를 추가하지 않는다. 기본 GNB는 GNBMobileLink / GNBMobileGroup을 사용한다. 링크 선택은 router의 preventDefault() 여부와 관계없이 Sheet를 닫으며, 닫기 버튼·Escape·focus trap·메뉴 버튼으로의 포커스 복귀는 기존 Sheet/Radix 동작을 유지한다.
Reference는 대표 Story 소스의 All(Patterns/Layout/GNB → GNB, patterns-layout-gnb--all) 안 Overview·Usage · GNB Sidebar를 좁은 폭에서 확인한다. Mobile / Mobile Menu Open은 숨김 verification이며 동작 검사에만 사용한다. Sidebar를 포함하면 §15의 공개 API 소비 예제를 따른다. LNB 모바일 링크 재사용 시 자동 닫기 Context가 다르므로 controlled Sheet를 route handler에서 닫고 Desktop 복귀 시에도 닫는다. 이는 GNB ONLY 내부 Composition이며 별도 LNB ONLY 모드가 아니다.
- Desktop(1024px 이상): 위 표. 높이 70, 좌우 28, column gap 24.
- Mobile(1023px 이하): 높이 56, padding 16. 주 메뉴·eyebrow·사용자 글자를 숨기고 햄버거(흰 ghost Button,
aria-label)를 맨 앞에, 사용자는 같은 Avatar 하나만 보인다(중복 없음). 햄버거 → HM Sheet(left): 제목, 링크(44px), 그룹 제목. Radix가 focus trap·Escape·focus 복귀를 맡는다. breakpoint를 768에서 1024로 올린 이유: 메뉴 4개(항목 최소 112) 가운데 정렬은 그 이하에서 brand/profile이 너무 좁아진다. ui-viz에는 반응형 기준이 없다. - 중첩 그룹(2026-10-06):
GNBMobileGroup안에GNBMobileGroup을 두면 한 단계 들여쓴다(--hm-gnb-mobile-nested-indent12 = 링크 padding, 위--hm-gnb-mobile-nested-gap8). GNB 자체의 깊은 메뉴를 Sheet에 같은 트리로 보이기 위한 CSS만 추가했고 API·동작은 그대로다. 접기/펼치기와 상위 그룹 current 표시는 없다(모든 단계가 펼쳐진 목록, 현재 위치는 링크의aria-current). 글자는 GNB Sheet 계약 14/20/500(current 700) 그대로다. LNB 모바일 표현은 LNB 전용 API를 따른다.
9. Styling / Token
packages/ui/src/theme/gnb.css의 --hm-gnb-*.
| Token | 값 |
|---|---|
--hm-gnb-height |
70px (mobile 56px). Layout이 top offset으로 읽는다 |
--hm-gnb-join |
12px. 콘텐츠 접점(바 아래로 gradient가 이어지는 길이) |
--hm-gnb-background |
세로 4-stop gradient(--hm-green-tone-1~4) |
--hm-gnb-padding-x / --hm-gnb-column-gap |
28 / 24 (mobile 16) |
--hm-gnb-z-index |
40 (sticky일 때만; overlay는 portal z-50) |
| menu | inset-top 4, join 12, min-width 112, padding-x 16, 16/700 |
| current / open / panel | Beige(--hm-sem-bg-content)+--hm-sem-action-primary / White / White, 하단 radius 8, shadow |
새 Primitive/Semantic은 GNB에서 추가하지 않았다. 4-stop은 공개 --hm-sem-action-surface-stop-*, 글자 보조색은 --hm-sem-palette-green-tint를 참조한다. 하위 패널 radius 8·shadow, 글자 테두리는 ui-viz가 검토값으로 둔 값을 그대로 가져왔다.
10. Position / z-index / Layout 연결
- 기본 relative(일반 흐름),
sticky옵션, fixed 없음. GNB Mode 페이지가 sticky를 쓸지 결정한다. - 접점은 일반/sticky 모두 같은 12px gradient를 콘텐츠의 둥근 윗모서리 바깥에만 mask로 그린다. sticky stacking context에서도 콘텐츠 중앙을 초록색 띠로 덮지 않는다. 높이·gradient·메뉴·프로필·색상은 공통이고 sticky selector는 position/top/z-index만 바꾼다.
Stickyverification Story와 대표 Story의 Sticky 예시는 같은 스크롤 영역을 사용한다. 테두리와 스크롤바 폭을 제거해 일반 예시와 폭을 맞추며, 휠/터치와 포커스 후 PageDown/End/Home으로 스크롤할 수 있다.- z-index: GNB 40 < portal overlay 50. 하위 패널은 GNB 안(z 2~3)에서 열려 콘텐츠 위에 그려진다. 체계는 바꾸지 않았다.
- GNB Mode 콘텐츠 계약: 바 다음 콘텐츠 면은
position: relative, DOM상 뒤, 윗모서리border-radius: var(--hm-gnb-join) var(--hm-gnb-join) 0 0, 시작 y =var(--hm-gnb-height)(접점 strip과 겹침, 밀어내지 않음). 높이는--hm-gnb-height로 읽는다.
11. Accessibility / 검증
<header>+ Navigation Menu<nav aria-label>, mobile<nav aria-label>, 그룹role="group". 아이콘 버튼aria-label, Badgearia-hidden.- Focus: 흰 outline(초록 위), current/open은 초록 outline.
- 확인(2026-10-03, Storybook): 1280 데스크톱 높이 70, 메뉴 항목 위 4·높이 66·최소 112, 메뉴 중앙 오차 0, current Beige
#F5F0E5/#123328/700, 열림 White 패널·긴 하위 이름 줄바꿈. 1024/1023 전환, 390 햄버거→Sheet 열림, Escape·링크 클릭 모두 닫힘 + 햄버거로 focus 복귀(녹색 재정합 후 재확인), 가로 overflow 없음(1024, 1023, 390, long labels).npm run typecheck,npm run build통과. Navigation Menu Story computed style 변화 없음.
12. 변경하지 않은 것
Navigation Menu, Dropdown Menu, Sheet, Avatar, Button, Tooltip, Badge, Popover, Command 코드·theme CSS는 수정하지 않았다.
13. 남은 차이 / 보류
| 항목 | 분류 |
|---|---|
| ui-viz 후보 1뎁스 18/28·폭 160·padding 24 (ui-viz도 FOLLOW_UP) | FOLLOW_UP. 현재 2Depth 패널은 고정 160px이 아니라 §4의 content-width/center/clamp |
| 하위 패널 radius 8·shadow, 글자 테두리 (ui-viz REVIEW VALUE) | 값 그대로 적용 |
| 독립 GNB + LNB 또는 GNB + Dashboard | 사용 금지. GNB ONLY 내부 Sidebar 부품 재사용은 §15 범위에서 허용 |
| sticky일 때 접점 strip 처리, 콘텐츠 radius 소유 | 접점 mask 정합 완료(2026-10-06). 콘텐츠 radius는 소비 Layout 소유 |
| 긴 메뉴명이 공간을 넘으면 메뉴 영역에서 잘림 | 한계 |
| 로고 asset 없음(테스트 이미지만), 로고 크기·간격은 ui-viz D-070 미확정 | MISSING |
| 실기기 터치, 1100~1280 사이 시각 | 미확인 |
| GNB Sheet 그룹 접기·상위 current 표시 | 미지원. 현재 링크 목록 방식 유지 |
| Notification Center, 전역 검색 결과, Organization/App Switcher | 후속 |
14. 관련 파일
packages/ui/src/patterns/gnb/{gnb.tsx,index.ts}
packages/ui/src/theme/gnb.css
src/patterns/gnb.stories.tsx
15. GNB ONLY Sidebar Composition (2026-10-08)
대표 Patterns/Layout/GNB → GNB의 Usage · GNB Sidebar에서 확인한다. 직접 동작 검증은 숨김 GNB Sidebar Story(patterns-layout-gnb--sidebar)를 사용한다. 기존 LNB → VERIFICATION ONLY · GNB + LNB는 운영 기준으로 승격하지 않는다.
구조는 GNB Header → Sidebar + Main Content다. Sidebar는 공개 LNB / LNBNav / LNBGroup / LNBItem을 그대로 사용한다. 기존 DOM/data-slot을 유지하고 브랜드·사용자 영역은 GNB만 담당한다. LNBBrand / LNBProfile은 기본 조합에서 생략한다. 새로운 GNBSidebar 공개 컴포넌트·메뉴 로직은 없다.
공용 @hm-design/ui/theme.css의 opt-in HTML attribute 계약:
- 프레임:
data-hm-gnb-layout="sidebar". 소비자가 높이를 지정하며 Grid가 GNB의 자연 높이와 남은 본문 높이를 배분한다. - 본문: 프레임의 직계
data-hm-gnb-body. Desktop은 기존--hm-lnb-width열 + 가용 콘텐츠 열, Mobile은 콘텐츠 한 열이다. - Sidebar: 본문의 직계
<LNB data-hm-gnb-sidebar>. 기존data-slot="lnb"는 유지한다. 이 Reference 범위에서 본문과 같은 배경, 세로 divider 한 개를 사용한다. Green은 Primary,data-hm-color="brown"은 기존 Brown 팔레트로 선택·Hover를 표현한다. 펼침 화살표는 유지하며 아이콘은 선택적으로 전달한다. 신규 AI GNB_ONLY에는 Sidebar를 추가하지 않는다. - Main: 본문의 직계
<main data-hm-gnb-main>. 공용 CSS가 면·접점·스크롤을 제공하며 콘텐츠 padding은 소비자가 정한다. GNB의 상단 접점과 LNB의 측면 접점은 기존 값을 재사용한다.
메뉴 폭·폰트·높이·padding·gap·Depth·스크롤은 LNB 계약을 재사용한다. 이 콘텐츠 Reference에만 아이콘 기본 생략, 동일 배경·세로 구분선 한 개, 해당 컬러 계열 Hover·선택을 적용하고 Green LNB의 오목한 선택 접점은 사용하지 않는다. 그룹은 기존 Collapsible의 open / onOpenChange를 사용하고 GNB NavigationMenu 열림과 독립적으로 관리한다. 선택 시 새 자동 펼침을 추가하지 않는다.
Desktop은 ≥1024px, Mobile은 ≤1023px이다. Mobile에서 Sidebar는 기존 LNB CSS로 숨기고 GNBMobileMenu 하나에 LNBMobileGroup / LNBMobileLink 목록을 넣는다. LNBMobileHeader나 LNBMobileMenu를 함께 렌더하지 않는다. 모바일 Sheet 기본 면, 44px 링크, 14/20/500(current 700), 중첩 들여쓰기, Escape·포커스 복귀를 유지한다.
LNBMobileLink의 자동 닫기 Context는 LNBMobileMenu 전용이다. GNBMobileMenu 안에서 재사용할 때는 아래처럼 Sheet를 controlled로 연결하고 동일 route handler에서 닫는다. router가 preventDefault()를 호출해도 닫혀야 한다. Desktop 복귀 시 Sheet 상태를 닫는 처리도 소비자 Composition에 둔다. 모바일 그룹은 기존 펼쳐진 링크 목록이며 데스크톱 Collapsible을 복제하지 않는다.
소비자 사용 예제
아래는 실제 공개 API와 공용 Theme만 사용하는 React 예제다. 메뉴 데이터·현재 경로·그룹 상태는 소비자 소유이며 Story 파일을 import할 필요가 없다. 실제 앱에서는 go의 상태 갱신을 router 이동과 현재 경로 읽기로 연결한다.
import { useEffect, useState, type MouseEvent, type ReactNode } from "react"
import {
GNB, GNBBrand, GNBNav, GNBUtilities, GNBUser, GNBMobileMenu,
LNB, LNBNav, LNBGroup, LNBItem, LNBMobileGroup, LNBMobileLink,
NavigationMenu, NavigationMenuList, NavigationMenuItem,
NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink,
Avatar, AvatarFallback, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
} from "@hm-design/ui"
import "@hm-design/ui/theme.css"
type Node = { id: string; label: string; children?: Node[] }
const tree: Node[] = [
{ id: "overview", label: "대시보드" },
{ id: "projects", label: "프로젝트 관리", children: [
{ id: "projects/design", label: "디자인 관리", children: [
{ id: "projects/design/tokens", label: "토큰 관리" },
{ id: "projects/design/components", label: "컴포넌트 관리" },
] },
{ id: "projects/tasks", label: "업무 목록" },
] },
]
export function Example() {
const [path, setPath] = useState("projects/design/tokens")
const [groups, setGroups] = useState<Record<string, boolean>>({ projects: true, "projects/design": true })
const [sheetOpen, setSheetOpen] = useState(false)
useEffect(() => {
const desktop = window.matchMedia("(min-width: 1024px)")
const sync = () => { if (desktop.matches) setSheetOpen(false) }
desktop.addEventListener("change", sync)
return () => desktop.removeEventListener("change", sync)
}, [])
const onPath = (id: string) => path === id || path.startsWith(`${id}/`)
const go = (id: string) => (event: MouseEvent) => {
event.preventDefault()
setPath(id)
setSheetOpen(false)
}
const desktopMenu = (nodes: Node[]): ReactNode => nodes.map(node => node.children ? (
<LNBGroup key={node.id} label={node.label} current={onPath(node.id)}
open={groups[node.id] ?? false} onOpenChange={open => setGroups(s => ({ ...s, [node.id]: open }))}>
{desktopMenu(node.children)}
</LNBGroup>
) : <LNBItem key={node.id} href={`#${node.id}`} active={path === node.id} onClick={go(node.id)}>{node.label}</LNBItem>)
const mobileMenu = (nodes: Node[]): ReactNode => nodes.map(node => node.children ? (
<LNBMobileGroup key={node.id} label={node.label}>{mobileMenu(node.children)}</LNBMobileGroup>
) : <LNBMobileLink key={node.id} href={`#${node.id}`} active={path === node.id} onClick={go(node.id)}>{node.label}</LNBMobileLink>)
return <div data-hm-gnb-layout="sidebar" style={{ height: "100dvh" }}>
<GNB>
<GNBMobileMenu title="메뉴" open={sheetOpen} onOpenChange={setSheetOpen}>
<div data-slot="lnb-mobile-nav">{mobileMenu(tree)}</div>
</GNBMobileMenu>
<GNBBrand href="#overview" onClick={go("overview")}><span><strong>프로젝트 관리</strong></span></GNBBrand>
<GNBNav>
<NavigationMenu viewport={false} aria-label="주 메뉴">
<NavigationMenuList>
<NavigationMenuItem><NavigationMenuLink href="#overview" active={path === "overview"} onClick={go("overview")}>대시보드</NavigationMenuLink></NavigationMenuItem>
<NavigationMenuItem value="projects">
<NavigationMenuTrigger aria-current={onPath("projects") ? "page" : undefined}>프로젝트</NavigationMenuTrigger>
<NavigationMenuContent><ul><li>
<NavigationMenuLink href="#projects/tasks" active={path === "projects/tasks"} onClick={go("projects/tasks")}>업무 목록</NavigationMenuLink>
</li></ul></NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</GNBNav>
<GNBUtilities>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<GNBUser name="김한맥" description="프로젝트 관리자"><Avatar><AvatarFallback>김</AvatarFallback></Avatar></GNBUser>
</DropdownMenuTrigger>
<DropdownMenuContent align="end"><DropdownMenuItem>프로필</DropdownMenuItem></DropdownMenuContent>
</DropdownMenu>
</GNBUtilities>
</GNB>
<div data-hm-gnb-body>
<LNB data-hm-gnb-sidebar><LNBNav aria-label="서비스 메뉴">{desktopMenu(tree)}</LNBNav></LNB>
<main data-hm-gnb-main><h1>프로젝트 업무</h1><p>현재 경로: {path}</p></main>
</div>
</div>
}
사용자 메뉴와 콘텐츠 padding은 기존 GNB 사용 예제에 따라 소비자가 추가한다. Theme Source, LNB 원본, Runtime 계약, 공개 API는 변경하지 않는다. 공통 요약은 AGENTS.md, 탐색 경로는 Reference Index를 따른다.
검증(2026-10-08, 기존 Storybook 6024): 1440px Sidebar 272px / Green Light 2, 2·3Depth 선택·그룹 Enter/Space·Hover·선택 경로·메뉴 스크롤 확인. 768px·375px에서 GNB 56px / Sidebar 숨김 / Main 전체 폭 / 흰 Sheet 1개 / 링크 44px·14px / 선택 시 닫기·Escape·포커스 복귀 확인. 열린 Sheet에서 Desktop 복귀 시 닫힘과 선택 경로 보존 확인. 기존 GNB Overview·LNB 대표 메뉴·Dashboard Header computed style 비교 동일, 검사한 화면의 Console error 없음. 앱 TypeScript 검사 통과. 전체 프로젝트 회귀 테스트와 실기기 터치 검증은 수행하지 않았다.