Skip to content

LnbSidebar

진행 중 (LnbSidebar component — work in progress)

  • 좌측 네비게이션(LNB) 메뉴 컴포넌트. config 기반으로 섹션·메뉴·서브메뉴를 렌더링.
  • 다크 배경(neutral-900), 글래스 톤 (--ds-glass-*) 위주의 색 토큰 사용.
  • 토큰 fix 완료. 단, transition 120ms / 150ms, font-weight, 일부 비표준 rgba, 1px hairline 정책 결정 대기 — 검토 필요 사항 참고.
규칙 (강제) · 코딩 에이전트용 — 사람은 접힌 채로 두어도 됩니다

LNB 강제 규칙. 상세·미리보기는 아래 본문.

마크업 / 구성

  • 템플릿은 빌드타임 include(<!-- include "../../packages/components/html/lnb-sidebar.html" -->)로 넣습니다. .ds-lnb__menu 마크업을 손으로 짓지 마세요.
  • 루트 <aside class="ds-lnb">ds-lnb--expanded / ds-lnb--collapsed 중 정확히 하나를 항상 달고 있어야 합니다 — 폭(208px / 68px)은 그 두 클래스 블록에만 정의돼 있어요(design-system.css.ds-lnb--expanded / .ds-lnb--collapsed). .ds-lnb 자체엔 폭이 없습니다.
  • 메뉴는 LnbConfig = { sections: [{ label?, items }] } config로만 구성합니다(LnbSidebar.tsLnbConfig / LnbSection / LnbItem 타입).
  • id 없는 아이템은 onSelect를 절대 발생시키지 않습니다 — 자식 토글 전용이에요(handleItemClick()). 아코디언 부모 그룹이 그렇습니다.
  • 렌더는 평평한 entries 리스트 + display:contents 래퍼입니다(get entries()). Alpine 3.14에서 중첩 x-for가 깨져서 일부러 이렇게 한 거예요(파일 헤더 주석에 이유 있음) — 중첩 x-for로 "정리"하지 마세요.
  • 아이콘은 getIcon 레지스트리에서 옵니다(iconFor()). 렌더 박스는 18px(design-system.css.ds-lnb__menu-icon).

반응형 3단 (⚠️ 컴포넌트가 아니라 앱이 구현합니다)

  • 기준 앱은 admin(dashboard.htmlsyncLnbViewport()): <768 드로어 / 768–1279 강제 레일(68px) / ≥1280 사용자 설정 복원.
  • 상태 플래그 3개: lnbCollapsed · lnbDrawerOpen · lnbPrefCollapsed. 접기 버튼은 lnbCollapsedlnbPrefCollapsed를 둘 다 써야 해요 — 안 그러면 리사이즈 때 PC 설정이 날아갑니다.
  • 새 breakpoint를 만들지 마세요 — md(768) · xl(1280)만 씁니다. breakpoints 참고.

⚠️ 트랩 — PR #430에서 실제로 터졌고, 되돌리면 재발합니다

  • 드로어 닫기는 onSelect 안에서만 하세요. .ds-lnb__top(또는 .ds-lnb) 컨테이너에 @click="lnbDrawerOpen = false"를 걸면 안쪽 탭에도 발화해서, id 없는 부모 그룹(예: "문서 결재")을 누르는 순간 하위 메뉴가 열리기도 전에 드로어가 닫힙니다. 커밋 c49f31fe가 정확히 이걸 제거했어요 — 다시 넣지 마세요.
  • onToggleCollapse에는 모바일 가드가 필수입니다: if (window.innerWidth < 768) return;. 없으면 드로어가 열린 채 lnbCollapsed=true가 되어, 다시 연 드로어가 68px 데스크톱 레일로 렌더됩니다.
    • ⚠️ apps/general-affairs·apps/management-dashboard에는 이 가드가 없습니다. 다만 먼저 이식이 필요한지부터 확인하세요 — 두 앱은 ?embedded=1이면 LNB를 통째로 숨깁니다(embeddedMode). admin의 embedRegistry가 두 앱을 품게 되면(현재는 team-manager·hr-system만 임베드) 그 LNB는 죽은 껍데기가 되므로 반응형 이식은 헛수고입니다. 두 앱이 독립 실행으로 계속 남고 + 모바일을 지원해야 할 때에만 admin의 반응형 LNB 4종 세트(뷰포트 동기화 · 모바일 가드 · lnbPrefCollapsed 보존 · chevron 숨김)를 통째로 이식하세요. 가드만 따로 넣지 마세요 — 드로어 없는 앱에서는 접기 버튼만 죽습니다.
  • 모바일에서 접기 chevron(.ds-lnb__toggle-inline)은 숨겨야 합니다. 드로어는 항상 "펼침"이라, chevron이 보이면 사용자가 드로어를 레일로 접어버릴 수 있어요.
  • onToggleCollapse를 주면 isCollapsed도 같이 주세요(LnbSidebar.tsisCollapsed?: () => boolean prop). 없으면 DOM 탐침으로 폴백해서 Alpine 한 틱 늦게 반응합니다.
  • openFlyout() 첫 줄의 ev.stopPropagation()을 지우지 마세요 — 연 그 클릭이 @click.outside로 바로 닫아버립니다.
  • 앱 CSS는 @import 뒤라 소스 순서로 이미 이깁니다!important 쓰지 마세요.

라이브 미리보기

아래는 실제로 클릭되는 라이브 미리보기입니다 — 프로덕션과 동일한 .ds-lnb*/.ws-brand 클래스로 렌더되어 active 전환·부모 메뉴 서브메뉴 토글·.ds-lnb--expanded.ds-lnb--collapsed 폭 전환이 그대로 동작합니다. 브랜드 우측 버튼으로 접고, 접힘 상태에서 아이콘에 호버(커스텀 툴팁)·하위 메뉴가 있는 부모를 클릭(우측 플라이아웃)해 보세요. 활성 하위를 가진 부모는 접힘에서도 강조가 유지됩니다.

선택: 직원 관리 · 펼침 — 메뉴 클릭 전환 · 부모 메뉴로 서브메뉴 토글 · 브랜드 우측 버튼으로 접기. 접힘 상태에서: 아이콘 호버(툴팁), 부모 클릭(우측 플라이아웃), 활성 하위를 가진 부모는 강조 유지

패널 모달 내비게이션 (in-modal LNB)

패널 모달(.ds-modal--panel)의 좌측 메뉴 — LNB 패턴의 밝은 배경 인-모달 변형입니다. 좌측 메뉴 클릭 시 우측(모달 현황 자리)이 전환됩니다. (LNB와의 상세 비교는 아래 패널 모달 내비게이션 참고)

연장근무 유형
이름 · 설명 · 배율 · 상태
← 좌측 메뉴 선택 → 이 영역(우측 현황)이 바뀝니다

선택 섹션: 연장근무 유형— 밝은 배경 · 플랫(단일 depth) · active는 bg-white. 다크 레일(.ds-lnb)과 달리 페이지가 아니라 모달 우측 현황을 전환합니다.

단일 출처

파트파일
Alpine 컴포넌트packages/components/src/components/LnbSidebar.ts
HTML partialpackages/components/html/lnb-sidebar.html
스타일packages/static/styles/design-system.css (.ds-lnb*)

구조

  • expanded 폭: 208px / collapsed 폭: 68px
  • header — 브랜드/스위처 + (workspace 패턴) 접기 토글 우측 배치
  • sectionsection-head (라벨 + 구분선) + 메뉴 묶음
  • menu — 단일 메뉴 (id 클릭) 또는 부모 메뉴 (children 토글)
  • submenu — 부모 expand 시 노출 (들여쓰기 34px)
  • bottom (profile + collapse) — 레거시 변형. workspace(admin)는 프로필을 PageHeader 아바타로, 토글을 헤더로 이동해 하단 영역을 제거 (2026-06). 다른 앱 단독 화면은 하단 유지
  • 메뉴 영역 스크롤바는 표시하지 않음 (scrollbar-width: none) — 스크롤 동작은 유지

두 가지 사용 패턴

1. 메뉴만 사용 (가장 일반적)

html
<div x-data="lnbSidebar({
  config: lnbConfig,
  isActive: (id) => activeTab === id,
  onSelect: (id) => switchTab(id),
})">
  <!-- include "../../packages/components/html/lnb-sidebar.html" -->
</div>

lnbConfig 예시:

ts
const lnbConfig: LnbConfig = {
  sections: [
    {
      label: "조직",
      items: [
        { id: "organization", label: "조직관리", icon: "ic-org" },
        { id: "employees",    label: "직원관리", icon: "ic-employee", badge: "5" },
        { id: "roles",        label: "역할관리", icon: "ic-role" },
      ],
    },
    {
      label: "근태/급여",
      items: [
        {
          label: "급여",
          icon: "ic-payroll",
          defaultExpanded: true,
          children: [
            { id: "payroll",   label: "급여대장", icon: "ic-payroll" },
            { id: "allowance", label: "수당 설정", icon: "ic-allow" },
          ],
        },
        { id: "leaves", label: "휴가 관리", icon: "ic-leave" },
      ],
    },
  ],
};

2. 전체 LNB Shell — workspace(admin) 최신 패턴

브랜드(정적, 비클릭) + 접기 토글이 헤더에 함께 배치되고 하단 영역은 없다. 프로필은 PageHeader 아바타로 이동.

html
<aside class="ds-lnb" :class="lnbCollapsed ? 'ds-lnb--collapsed' : 'ds-lnb--expanded'">
  <div class="ds-lnb__header" x-data="workspaceSwitcher({ current: 'admin' })">
    <div class="ws-brand"><!-- Medical 심볼 + hereby Medical / 조직명 텍스트 --></div>
    <button class="ds-lnb__toggle-inline" @click="lnbCollapsed = !lnbCollapsed">…</button>
  </div>
  <div class="ds-lnb__top" x-data="lnbSidebar({...})">
    <!-- include ".../lnb-sidebar.html" -->
  </div>
</aside>

(레거시) 스위처 + 하단 profile/collapse 변형

총무·경영 대시보드 등 단독 화면이 아직 쓰는 기존 변형 — 헤더에 워크스페이스 스위처(드롭다운), 하단 .ds-lnb__bottom에 프로필 + 접기 토글.

패널 모달 내비게이션 (in-modal LNB)

패널 모달(.ds-modal--panel, 예: 근태 「유형·기준 통합 관리」)의 좌측 메뉴는 LNB 패턴을 모달 안으로 가져온 경량 변형입니다. 라이브 미리보기는 페이지 상단 라이브 미리보기 → 패널 모달 내비게이션에서 직접 조작할 수 있습니다.

다크 레일 컴포넌트(.ds-lnb, lnbSidebar())를 그대로 쓰는 게 아니라 밝은 배경의 인-모달 섹션 스위처입니다 — 하지만 "config로 항목을 정의하고, 정체성 아이콘을 붙이고, 하나만 active" 라는 LNB의 핵심 원칙은 동일합니다.

LNB와 무엇이 같고 다른가

항목앱 LNB (.ds-lnb)패널 모달 좌측 메뉴
컴포넌트lnbSidebar() Alpine factory모달 내 인라인 마크업(type-manager.html)
배경다크(neutral-900) + 글래스 톤밝음(bg-neutral-50) + 우측 1px 라인
항목 정의LnbConfig.sections[].itemstypeManagerMenu({ id, label, icon }[])
아이콘24px 아웃라인(ds-lnb__menu-icon)16px(.type-manager__nav-icon)
active 표현is-active (글래스 강조)bg-white·neutral-900·600
계층section/부모/서브메뉴 · 접힘 레일단일 depth(플랫), 접힘 없음
선택 결과페이지/탭 전환우측 콘텐츠(현황 표) 전환

공유 규칙 — 아이콘 일치: 패널 모달 좌측 메뉴의 각 항목 아이콘은 아이콘 사용 규칙과 같은 원칙을 따릅니다 — 그 섹션의 출처 페이지가 LNB·헤더에서 쓰던 정체성 아이콘을 그대로 재사용합니다(근무 설정=슬라이더, 연장근무=시계플러스, 외근=맵핀, 연차 부여 기준=달력체크, 휴가 유형=달력). 같은 화면을 가리키는 LNB·헤더·패널 메뉴의 아이콘은 항상 일치해야 합니다.


바로가기 — 좌측 메뉴가 팝업 안에서 어떻게 보이는지, 패널 전체 라이브 예시·규격은 → 모달 → 패널 모달

API

lnbSidebar(opts: LnbSidebarOptions) Alpine.data factory.

Prop타입설명
configLnbConfig{ sections: LnbSection[] } — 섹션·메뉴 트리
isActive(id: string) => boolean현재 활성 메뉴 판단
onSelect(id: string) => void메뉴 클릭 핸들러

타입

Type필드
LnbItemid?, label, icon, badge?: string | number | LnbBadge, disabled?, children?, defaultExpanded?
LnbBadgetext: string | number, tone?, line?, shape?(기본 round), size?(기본 xs)
LnbSectionlabel?, items: LnbItem[]
LnbConfigsections: LnbSection[]
  • id 없는 item은 클릭해도 select 안 됨 (children 토글만)
  • defaultExpanded → 부모 메뉴는 기본으로 펼쳐짐(미지정=펼침). 접힌 상태로 시작하려면 defaultExpanded: false를 지정
  • badge → 메뉴 우측 칩. 배지는 DS .ds-label 컴포넌트로 렌더된다(전용 스타일 X).
    • 문자열/숫자(예: badge: "5") → 알림 카운트 필(primary·round). 문서 결재 대기 건수 등에 사용
    • 객체(LnbBadge) → DS 라벨 변형을 직접 지정: tone(색)·line(테두리)·shape(square/round)·size(xs/sm/md)
    • 다크 레일 주의: LNB 배경이 어두우므로(neutral-900) 준비중 라벨 등엔 다크 서피스 tone(gray-dark·light-gray-dark)을 써야 글자가 읽힌다. 예) 총무 준비 메뉴 = { text: "준비중", tone: "light-gray-dark", size: "xs", shape: "square" }(solid). 밝은 배경용 gray/light-gray는 글자가 어두워 다크 레일에서 안 보임

States

State표현
defaultcolor: var(--ds-glass-70) (희미한 흰색)
hoverbackground: rgba(255,255,255,0.06) (정확 토큰 부재), color: var(--ds-bw-white)
activebackground: var(--ds-glass-10), color: var(--ds-bw-white)
child-active (collapsed)활성 하위를 가진 부모 아이콘에 active와 동일 강조 (is-child-active) — 하위가 숨겨지는 접힘 레일 전용
disabledcolor: var(--ds-glass-30), pointer-events: none
expanded parentchevron 180° 회전
collapsed sidebarwidth 68px, menu-label · badge · section-label 숨김

접힘(collapsed) 레일 동작

접힘 레일은 라벨이 숨겨지므로 두 가지 보조 UI가 자동으로 동작한다 (별도 옵션 없음):

플라이아웃 — .ds-lnb__flyout

  • 하위 메뉴가 있는 부모를 클릭하면 아이콘 우측에 하위 메뉴 그룹 표시
  • 흰 바탕(--ds-bw-white) + 1px 라인(--color-line-normal) + --radius-8 + --shadow-popover, 행은 아이콘 + 타이틀(+배지)
  • 클릭한 버튼 좌표 기준 fixed 배치 — 레일의 overflow/스크롤에 잘리지 않음 (z-60)
  • 항목 선택·바깥 클릭·같은 부모 재클릭으로 닫힘. disabled 항목(준비중)은 흐림 + 클릭 불가

커스텀 툴팁 — .ds-lnb__tip

  • 접힘 레일에서 메뉴 호버 시 아이콘 우측에 라벨 표시 (neutral-900 바탕 + --ds-bw-white 텍스트 + --radius-4, z-70)
  • 네이티브 title 미사용 — 네이티브 툴팁은 떠 있는 동안 코드로 지울 수 없어 플라이아웃을 가린다. 접근성은 aria-label로 유지
  • 클릭 즉시 숨김. 플라이아웃이 열려 있을 때 억제는 "열린 메뉴 자신"으로 한정 — 다른 메뉴의 툴팁은 그룹 위(z-70 > 60)로 표시

사용된 토큰 (fix 후)

카테고리토큰
Spacing--ds-spacing-{2, 4, 6, 8, 10, 12, 16, 18, 20, 22, 24, 32, 36, 68, 208, 220}
Color (dark)--color-neutral-{25, 50, 100, 700, 900}, --color-red-500, --color-orange-{50, 500}
Glass tints--ds-glass-{08, 10, 18, 30, 55, 60, 70}
Brand--ds-bw-white
Radius--radius-4, --radius-8, --ds-radius-sm, --radius-full
Opacity--ds-opacity-60 (submenu bullet), --ds-opacity-40 (flyout disabled)
Typography--text-b4, --text-b5, --text-b6 (tip), --text-c1, --text-c2
Shadow--ds-shadow-xs (collapsed toggle), --shadow-blur4 (profile menu), --shadow-popover (flyout)
Motion--duration-normal (sidebar width transition)
Flyout/Tip color--ds-bw-white(바탕), --color-line-normal(라인), --color-neutral-{50, 75, 500, 900}

아이콘 사용 규칙

  • 메뉴 아이콘은 @hereby/components icon registry(packages/components/src/icons.ts)의 24px 아웃라인 세트(스트로크 1.5)만 사용
  • 같은 LNB 안에서 아이콘 중복 금지 — 메뉴 식별이 목적이므로 항목마다 구분되는 통상적 의미의 아이콘 사용. 의미가 같은 메뉴(예: 섹션이 다른 두 "설정")만 동일 아이콘 허용
  • 페이지 헤더 아이콘과 일치 — 같은 화면을 가리키는 LNB 항목·헤더 타이틀·embed 레지스트리의 아이콘은 동일해야 한다
  • 2026-06 확장: 명찰(역할)·서식문서·표(시트)·시계(근태)·슬라이더(설정류)·달력시계(스케줄러)·달력체크(연차)·시계플러스(연장근무)·막대차트(현황)·태그(유형)·맵핀(외근)·게이지(대시보드)·목록체크(업무) 13종 추가

검토 필요 사항

토큰 체계가 아직 커버하지 못해 하드코딩으로 남겨둔 항목. 디자이너 결정 후 토큰화 진행:

  • 🟡 rgba(255, 255, 255, 0.06) (4곳: border, hover backgrounds) — --ds-glass-04(0.04) / --ds-glass-08(0.08) 사이. 0.06 토큰 추가 vs 0.08로 정렬 결정 필요
  • 🟡 rgba(255, 255, 255, 0.9) (1곳: profile-name) — --ds-glass-70 초과. 0.9 토큰 추가 필요
  • 🟡 rgba(255, 255, 255, 0.45) (1곳: section-label) — --ds-glass-30/55 사이. 0.45 토큰 또는 정렬
  • 🟡 rgba(143, 143, 143, 0.5) (1곳: collapse toggle border) — neutral-400 + 0.5 alpha. 단일 토큰 부재
  • 🟡 font-weight: 400/500/600/700 — 모두 하드코딩. weight 토큰 미정의
  • 🟡 transition 120ms / 150ms (~8곳) — motion scale 부족
  • 🟡 padding-left 13px / 34px (menu / submenu) — 2px step 미정렬. 메뉴 들여쓰기 정책 검토
  • badge — 전용 .ds-lnb__badge 인셋 스타일 제거 완료. 배지는 이제 DS .ds-label(크기·모양·색)로 렌더되고, .ds-lnb__badge는 접힘 레일에서 배지를 숨기는 훅으로만 남음
  • 🟡 right -10px / -23px (collapse toggle 음수 offset) — half-pixel positioning 정책
  • 🟡 1px hairline (3곳: section-line, profile-menu-divider) — spacing 최소가 2px. hairline 정책 결정
  • 🟡 avatar linear-gradient — 브랜드 그라데이션, 단일 토큰 부재. 컴포넌트 전용으로 유지 가능

사용처

목표 & 로드맵

항목현재목표
Config-driven 렌더링✅ section/item/submenu 트리유지
Spacing 토큰 매핑✅ 완료유지
Color hex → 토큰✅ 완료 (fallback 정합 포함)유지
Glass tint 매핑✅ 정확 매칭만 (08/10/18/30/55/60/70)0.06 / 0.45 / 0.9 추가
Shadow 매핑✅ blur4 / xs유지
Font-weight 토큰화🔴 미정의typography 페이지 weight scale 도입 후 적용
Motion 토큰 확장🟡 fast/normal만120ms / 150ms 등 세분화
2px step 외 spacing🟡 13/34/-10/-23 (메뉴/툴글)step 정렬 또는 layout offset 토큰 도입
Hairline 토큰 정책🔴 미정의border-width / spacing 둘 중 결정
컴포넌트 가이드라인🔴 미작성section 그룹 사용 시점, badge 사용 규칙 등

변경 이력

날짜내용
2026-07-02배지를 DS .ds-label로 렌더: 전용 .ds-lnb__badge 시각 스타일 제거, LnbItem.badgestring | number(카운트 필) 또는 LnbBadge 객체(tone·line·shape·size 변형) 허용. 다크 레일용 tone(gray-dark·light-gray-dark) 지원. 총무 준비중 메뉴 = light-gray-dark solid 배지 + disabled. 라이브 미리보기·API 표 갱신
2026-06-12접힘 레일 UX: 플라이아웃(.ds-lnb__flyout)·커스텀 툴팁(.ds-lnb__tip, 네이티브 title 대체)·is-child-active 강조 추가. workspace 패턴: 접기 토글 헤더(브랜드 우측) 이동·하단 profile 제거(헤더 아바타로)·ws-brand 정적 브랜드. 레일 스크롤바 숨김. 아이콘 13종 추가 + 중복 제거 리맵
2026-05-28토큰 audit + fix: spacing ~50곳, radius 7곳, font-size 8곳, color hex 5곳, glass tint 5곳, color fallback 3곳, shadow 2곳, opacity 1곳, transition 3곳 → DS 토큰 매핑. 시각 변화 ≈ 0
이전config-driven LnbSidebar 컴포넌트 + 4-group 계층 admin 적용