Files

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: header props + sticky(기본 false). data-slot="gnb", sticky면 data-sticky.
  • GNBUser: button props + 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는 router preventDefault가 있어도 닫히도록 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-indent 12 = 링크 padding, 위 --hm-gnb-mobile-nested-gap 8). 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만 바꾼다.
  • Sticky verification 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, Badge aria-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 검사 통과. 전체 프로젝트 회귀 테스트와 실기기 터치 검증은 수행하지 않았다.