Files
HMDesign/docs/patterns/dashboard-layout.md

31 KiB
Raw Permalink Blame History

Dashboard (Pattern)

SINGLE NAVIGATION MODE / 2026-10-08 Sidebar 운영 정책. DASHBOARD ONLY 운영 화면은 Sidebar를 사용하지 않는다. 전용 Header(48px)·업무 상태 필터와 Content를 사용하며 Header는 GNB가 아니다. 독립 GNB / LNB / Dashboard 모드는 함께 사용하지 않는다. 공통 정책이 이전 조합·Sidebar 허용 기록보다 우선한다.

1. 현재 계약

Dashboard 운영 화면은 전용 Header/Menu·업무 상태 필터와 Content로 구성하며 모든 Sidebar 사용을 금지한다. 외부 GNB와 일반 LNB를 결합하지 않는다. 운영 Reference는 Sidebar 없는 대표 Patterns/Layout/Dashboard → Dashboard Story다.

공개 API 역할
DashboardLayout 바깥 scroll root + 내부 frame. 운영 화면은 header, children을 사용하고 sidebar를 전달하지 않음
DashboardHeader Dashboard 내부 Header. 기본 높이 48px
DashboardSidebar 기존 호환성·검증 전용 공개 API. 신규 Dashboard 운영 화면 사용 금지. 기존 폭 272px·메뉴 세로 스크롤 기능은 유지
DashboardMain <main>, 콘텐츠 및 세로 스크롤
DashboardContent 콘텐츠 stack, gap 24px

DashboardSidebar 공개 Export와 선택적 sidebar slot은 기존 소비자 호환성·검증을 위해 유지한다. API 존재가 운영 허용을 뜻하지 않으며 신규 운영 화면에는 사용하지 않는다. GNB/LNB나 Sidebar를 전달하던 운영 화면은 적합한 단일 Navigation Mode의 운영 구성으로 마이그레이션한다. slot은 임의 ReactNode를 받으며 런타임 자동 차단은 없다. 운영 사용 금지와 기능 제거는 별도 작업이며, API 제거·런타임 차단은 소비처 영향 조사와 사용자 결정 후 진행한다.

<DashboardLayout
  header={<DashboardHeader>Dashboard Header / Menu</DashboardHeader>}
>
  <DashboardMain>
    <PageHeader>…</PageHeader>
    <DashboardContent>…</DashboardContent>
  </DashboardMain>
</DashboardLayout>

2. 최소 폭·스크롤·스타일

  • 운영 최소 폭 = Sidebar 없는 960px(--hm-dashboard-min-width).
  • Sidebar 포함 1232px(960 + 기존 Sidebar 폭 272px)은 호환성·검증 구성의 값이며 현재 운영 기준이 아니다. 해당 구현·CSS는 호환성을 위해 유지한다.
  • 최소 폭 이하에서는 바깥 dashboard-scroll 안에서 전용 Header/Content 전체를 가로 스크롤한다. 모바일에서도 Sidebar를 추가하지 않는다.
  • Main이 세로 스크롤을 담당하고 바깥 frame 높이는 --hm-dashboard-height(기본 100dvh)다. className, style, DOM props는 바깥 root에 전달한다.
  • Root 기본 접근성은 role="region", aria-label="Dashboard", tabIndex={0}. data-navigation-mode="DASHBOARD"는 해당 체계를 식별한다.
  • Main padding 34px, content gap 24px. Header 콘텐츠 radius 12px와 관련 shadow 유지. 기존 Sidebar 콘텐츠 radius 18px·shadow는 호환성·검증용 구현에만 남는다.
  • Dashboard는 --hm-dashboard-*와 공통 Semantic만 사용한다. GNB 높이·join, LNB 폭·gradient, 1024px 모바일 숨김 override에 의존하지 않는다.
  • Filter Bar는 Dashboard 최소 폭 frame 안에서 한 줄과 기존 control 폭을 유지한다. 넓은 개별 콘텐츠는 Main 또는 카드 내부에서 스크롤한다.

3. Storybook

대표 Patterns/Layout/Dashboard → Dashboard는 Compact Header 48px + 상태 필터 + 콘텐츠다. Compact 상태 바 geometry(높이 40px, 중심 50% + 18px, 28px control, 36px padding, radius 16px 및 오목한 날개), 전체/진행/보류/완료 count 색상은 기존 기준을 유지한다. count 진행 #00c896 / 보류 #ff9500는 2026-10-07 사용자 결정에 따른 Story 전용 값이다.

Dashboard 업무 상태 공통 계약

업무 상태 계약은 packages/ui/src/patterns/dashboard-status/contract.ts가 기준이다. active | hold | done은 실제 상태이고, all | active | hold | done | issue는 필터 ID다. issue는 상태를 대체하지 않는 독립 Boolean flag이며 hold + issue처럼 동시에 표현할 수 있다. done의 표시명은 완료를 유지한다.

계약은 label, tone, 접근성 label, count 계산(countDashboardFilter), 필터 매칭(matchesDashboardFilter)을 제공한다. React adapter는 기존 Badge, Button, FilterBar, Progress를 사용하고, 다른 Runtime은 동일한 data-status, data-issue="true", data-filter, aria-pressed vocabulary를 사용한다. 선택 상태는 aria-pressed="true"가 우선하며 hover/focus/disabled는 대상 Runtime의 interaction state로 동일하게 매핑한다. 상태 색상 원본은 packages/ui/src/patterns/dashboard-status/dashboard-status.css이며 @hm-design/ui/dashboard-status.css로 공개한다. 기존 src/patterns/dashboard-status-contract.css는 원본을 import하는 호환 경로로 유지한다. 이 변수는 기존 Story 색상을 보존하는 compatibility mapping이며 HM Theme token은 변경하지 않는다. Static HTML 기준 샘플은 docs/patterns/dashboard-status-static.html이다.

패키지 소비자는 앱 CSS entry에서 Theme 다음에 상태 CSS를 명시적으로 로드한다. React API import만으로 CSS가 자동 적용되지는 않는다.

@import "tailwindcss";
@import "@hm-design/ui/tailwind.css";
@import "@hm-design/ui/theme.css";
@import "@hm-design/ui/dashboard-status.css";

상태 CSS는 변수 정의만 제공한다. 기존 Badge variant="outline"에는 화면의 wrapper에서 아래 mapping을 적용한다. 완료의 흰색 ink가 유지되므로 기존 어두운 Badge 배경도 함께 유지한다. issue Badge는 실제 상태 Badge와 별도로 렌더링한다.

이 recipe와 일치하더라도 소비자의 HM slot CSS는 Guard REUSE-003 NOT VERIFIED를 유지한다. CSS Intent 근거 연결은 정확한 issue·문서 SHA·실측 변수를 연결하며 스타일 적용 확인과 사용자 디자인 의도 승인을 구분한다.

.dashboard-status [data-status="active"] { --dashboard-status-color: var(--hm-dashboard-status-active); }
.dashboard-status [data-status="hold"] { --dashboard-status-color: var(--hm-dashboard-status-hold); }
.dashboard-status [data-status="done"] { --dashboard-status-color: var(--hm-dashboard-status-done); }
.dashboard-status [data-status="issue"][data-issue="true"] { --dashboard-status-color: var(--hm-dashboard-status-issue); }
.dashboard-status [data-slot="badge"][data-status] {
  --hm-badge-outline-text: var(--dashboard-status-color);
  --hm-badge-outline-background: color-mix(in srgb, var(--hm-sem-palette-green-dark-2) 90%, transparent);
  --hm-badge-outline-border-color: color-mix(in srgb, var(--hm-sem-palette-black) 50%, transparent);
}

HTML adapter는 같은 상태 변수를 자신의 color 등에 연결한다. package dist를 정적 자산으로 배치하면 theme.css 다음에 dashboard-status.css를 <link>로 로드한다. 저장소의 기존 Static HTML 예제는 호환 CSS 경로를 그대로 사용한다. React와 HTML 모두 done은 --hm-dashboard-status-done을 참조하며, 이 alias는 :root와 [data-hm-theme]에서 각 scope의 white token으로 재계산한다. active/hold/issue 색상과 Runtime Theme의 73개 계약은 유지한다.

Guard의 REUSE-003는 위 소비 mapping에도 검토 위치를 남긴다. Token 선언과 실제 Badge custom property/color/background/border를 통과해도 요청 디자인과의 의미적 일치는 자동 승인하지 않는다. CSS Intent·동적 Token 증거 계약에 따라 위치별 STATIC/RUNTIME/DESIGN_INTENT/FINAL을 보고하고, 실제 실행하지 않은 Theme와 상태는 NOT VERIFIED로 남긴다. 기존 대표 Story·상태 색상·Layout은 그대로 유지한다.

React 소비 예제: 데이터 → 상태 변수 → Badge

active(진행) → --hm-dashboard-status-active, hold(보류) → --hm-dashboard-status-hold, done(완료) → --hm-dashboard-status-done이다. 독립 issue === true는 --hm-dashboard-status-issue로 별도 Badge를 표시한다. tone은 의미 분류이며 Badge variant나 CSS를 자동 선택하지 않는다. 위 CSS mapping과 import 순서를 앱 CSS entry에 적용한 다음 아래 공개 API를 사용한다.

import {
  Badge, dashboardFilterDefinitions, matchesDashboardFilter,
  type DashboardFilterId, type DashboardStatusRecord,
} from "@hm-design/ui"

type Work = DashboardStatusRecord & { id: string }
const works: Work[] = [
  { id: "T-1", status: "active" },
  { id: "T-2", status: "hold", issue: true },
  { id: "T-3", status: "done" },
]

export function WorkBadges({ filter = "all" }: { filter?: DashboardFilterId }) {
  const visible = works.filter(work => matchesDashboardFilter(work, filter))
  return <div className="dashboard-status">
    {visible.map(work => <div key={work.id}>
      <Badge variant="outline" data-status={work.status}>
        {dashboardFilterDefinitions[work.status].label}
      </Badge>
      {work.issue === true && <Badge variant="outline" data-status="issue" data-issue="true">
        {dashboardFilterDefinitions.issue.label}
      </Badge>}
    </div>)}
  </div>
}

filter="issue"에서도 T-2의 상태는 hold로 유지되고 보류 Badge와 이슈 Badge가 함께 표시된다. issue를 DashboardStatusId에 추가하거나 destructive 등 다른 Badge variant에 임의로 연결하지 않는다.

Reference: React 代表 Storyの statusBadge と Story CSSの Badge mapping、公開状態 CSS 原本、Static HTML。上の例はこれらの状態・色 mappingを公開 APIで消費する例であり、Storyモジュールは importしない。コード・Propsはソースと照合済みで、この文書改善作業では例の実行・画面検証はしていない。

Sidebar 검증 Story WithSidebar, SidebarOnly, ThirdLevel, LongContent, Framed는 기존 기능·상태 데이터를 보존하는 VERIFICATION ONLY — 운영 사용 금지 화면이다. verification 태그로 기본 Story 메뉴에서 제외하며 이름과 화면에 운영 금지를 표시한다. 신규 운영 화면 Reference로 사용하지 않는다. Loading, Empty도 verification 전용이며 Sidebar 없는 Compact 구성을 검사한다. 대표 Story ID patterns-layout-dashboard-layout--all과 기존 검증 Story ID는 유지한다. 이전 GNB + LNB / GNB Only / LNB Only Story 구성은 §5의 역사 기록이다.

4. 단일 모드 통합 검증 (2026-10-07, headless Edge)

HISTORICAL RECORD: 아래 결과는 2026-10-07 당시 코드의 검증 기록이다. Sidebar 포함 1232px 사례는 현재 운영 허용이나 이번 정책 수정의 검증 결과를 뜻하지 않는다. 현재 운영 계약은 §1–3의 Sidebar 없는 960px 구성이다.

  • Navigation/Theme Story × viewport 총 48개 케이스 통과. 1280, 1232, 1024, 1023, 960, 959, 390, 320px에서 주요 모드를 확인했다.
  • Dashboard에 외부 GNB/LNB 없음, Compact 960px / Sidebar 포함 1232px 최소 폭과 내부 가로 스크롤 유지, 문서 가로 넘침 0px.
  • LNB 1024/1023 전환, Sheet Escape·포커스 복귀·preventDefault 링크 선택·Desktop/Mobile 선택 상태 공유 확인. Dashboard 그룹 Enter/Space, 현재 링크 및 hover 유지 확인.
  • Theme Preview는 GNB/LNB 중 하나만 렌더링하고 Dashboard Theme 콘텐츠는 외부 Navigation을 제외한다. Runtime Theme 계약 검증 136개 통과, 브라우저 runtime error 0.
  • 타입 검사, UI package build, Storybook static build, Production 앱 build, git diff --check 통과. 기존 문서 예시 bg-[var(...)] CSS 경고 및 Storybook chunk 크기 경고는 남아 있다.

5. 이전 구현·검증 이력 (HISTORICAL RECORD)

아래 내용은 정책 전환 이전 기록을 보존한 것이다. 현재 구현·운영·AI 생성의 근거로 사용하지 않는다. GNB/LNB 혼합, 모바일 체계 전환, 교차 CSS 변수 및 이전 Story 구성은 위 §1–3과 공통 SINGLE NAVIGATION MODE 정책으로 대체되었다. 아래 테스트 수치는 이전 코드의 결과이며 이번 변경의 검증 결과가 아니다.

2026-10-07 단일 모드 전환 이전 기록

Dashboard Layout (Pattern)

현재 HM 구현의 기준 문서. 공통 원칙은 docs/guidelines/프로젝트-지침.md(§6.10, §6.11)를 따른다.

Layout visual reference: 외부 ui_viz Reference (ZIP 미포함) (dashboard-composition.md §7·§8·§19, Variant A Sidebar / Variant B Top-header)
Implementation: current @hm-design/ui patterns / primitives

1. 상태

항목 내용
구현 상태 구현 완료 (2026-10-03)
기반 GNB, LNB, Page Header, Card, Badge, Progress, Filter Bar, Data Table 조합 (새 dependency 없음)
대표 Story Patterns/Layout/Dashboard Layout → Dashboard Layout (verification: Third Level, GNB Only, LNB Only, Long Content, Loading, Empty, Framed)
함께 만든 Pattern Page Header (page-header.md)
보류 §12

2. 목적 / 범위

GNB / LNB / Page Header / Main을 실제 화면으로 조립해 Layout contract를 확정한다. 새 UI를 만들지 않는다. 메뉴·페이지 내용은 소비자가 slot과 children으로 넣고, Dashboard는 메뉴 schema를 모른다. 소유하는 것: 화면 프레임(높이), main 스크롤, 콘텐츠 면(배경·padding·둥근 모서리·그림자), 콘텐츠 세로 리듬(gap).

3. ui-viz visual reference

ui-viz에서 확인한 구조(실제 파일: compositions/dashboard-variant-specimens.{jsx,css}, top-header-specimen.{jsx,css}, storybook/{LNB,GNB,Layout}.stories.jsx, layout-composition.jsx, dashboard-composition.md):

영역 ui-viz 구조 현재 HM 대응 이번 구현
조립 Variant A(LNB 272 + 콘텐츠), Variant B(GNB 70 + 콘텐츠), Variant C(Compact 48). GNB+LNB 동시 사용 예 없음 DashboardLayout slot 방식. 둘 다 / 하나만
GNB 70px green, 콘텐츠와 12px 접점 GNB slot header
LNB 272px green, 콘텐츠와 18px 접점 LNB slot sidebar
Content surface #F5F0E5, padding 34, 접점 쪽 radius(LNB 18 / GNB 12), 그림자 -8px 0 18px(LNB) · 0 -8px 18px(GNB) rgba(7,30,24,.18), 별도 border 없음 신규 DashboardMain
스크롤 "메뉴/헤더는 고정, 콘텐츠 영역에서만 스크롤"(Confirmed). overflow-y:auto, overscroll-behavior: contain 신규 DashboardMain이 스크롤 소유
Page heading breadcrumb 12px(정적 p, 컴포넌트 아님) → 제목 28 → 설명, space-between·align end·gap 24, 하단 20px Breadcrumb + 신규 Pattern PageHeader
간격 외곽 34, 카드 사이 24, 카드 padding 24, 카드 제목→본문 24, 조회→결과 16 Card, DashboardContent 24 gap 토큰, 나머지는 Card/Filter Bar 값
조회 결과 카드 안 검색(라벨 좌측, 최대 360) + 표 Filter Bar + Data Table Card 안에서 조합 (gap 16)
KPI Dashboard에 Panel/Card 4종(KPI 전용 아님) Card + Badge + Progress Story 조합만

4. GNB / LNB 조립 정책

구성 사용 근거
GNB only (Variant B) header ui-viz 기준 있음
LNB only (Variant A) sidebar 모든 viewport에서 Sidebar 유지. 최소 폭 이하에서는 Dashboard 전체 가로 스크롤
GNB + LNB header + sidebar ui-viz에 기준 없음. 사용자 요청으로 조립한 INTENTIONAL 구성(대표 Story)
Brand / Profile ownership (2026-10-06, lnb.md §3c)
구성 Desktop Brand Desktop Profile Mobile Brand Mobile Profile
GNB + LNB GNB(GNBBrand) GNB(GNBUser), LNBProfile 없음 GNB Mobile Header GNB Sheet의 GNBUser, 별도 LNB Profile 없음
LNB Only LNBBrand LNBProfile 동일한 LNBBrand 동일한 LNBProfile
GNB only GNB(GNBBrand) GNB(GNBUser) GNB Mobile Header GNB Profile

Brand/서비스명과 Profile의 소유권은 소비자가 composition에서 LNBBrand와 LNBProfile을 넣고 빼서 정한다. LNB Only의 LNBProfile은 DropdownMenuTrigger asChild로 합성하며 작은 viewport에서도 Sidebar와 함께 유지한다. GNB + LNB에서는 LNBProfile을 렌더링하지 않는다.

GNB+LNB일 때: GNB가 전체 폭 위에 평평하게 놓이고 LNB와 콘텐츠가 그 아래 한 줄. GNB의 12px 접점 띠는 숨기고(::after) 콘텐츠는 왼쪽 모서리만 18px, 위쪽 오른쪽 모서리는 직각이다. 작은 viewport에서도 이 접점을 유지한다. GNB 자체의 모바일 헤더·Sheet 정책은 그대로 유지한다.

5. 구조 / API

<DashboardLayout header={<GNB>…</GNB>} sidebar={<LNB>…</LNB>}>
  <DashboardMain>
    <PageHeader>…</PageHeader>
    <DashboardContent>…Card, Filter Bar, Data Table…</DashboardContent>
  </DashboardMain>
</DashboardLayout>
  • DashboardLayout: 바깥 data-slot="dashboard-scroll"이 가로 스크롤을 맡고, 안쪽 data-slot="dashboard-layout"(flex column)이 최소 폭과 header/sidebar slot을 유지한다. 기존 className, style, DOM props는 바깥 scroll root에 전달된다. data-header/data-sidebar는 안쪽 frame에 있다. DOM 순서는 header → (sidebar → main)이다.
  • Scroll root는 기본 role="region", aria-label="Dashboard", tabIndex={0}이며 DOM props로 변경할 수 있다. CSS 변수는 root에서 내부 frame으로 상속된다.
  • DashboardMain: <main>, 콘텐츠 면이자 세로 스크롤 컨테이너. 고유 id/aria-label이 필요하면 소비자가 준다.
  • DashboardContent: 섹션 세로 stack(gap 24). 섹션 스타일은 입히지 않는다.
  • GNB / LNB를 wrapper로 다시 감싸지 않고 그대로 slot에 넣는다.

6. Content Surface (접점 소유권)

GNB / LNB는 콘텐츠 radius를 만들지 않는다. 자기 쪽 "연장 띠"만 둔다(GNB ::after 12px = --hm-gnb-join, LNB ::after 18px = --hm-lnb-join). 콘텐츠 면이 그 위에 positioned로 얹혀 둥근 모서리에서 띠가 보이게 한다(DashboardMain이 position: relative이고 DOM상 뒤).

상태 radius shadow
header gnb-join gnb-join 0 0 0 -8px 18px rgba(7,30,24,.18)
sidebar (모든 viewport) lnb-join 0 0 lnb-join -8px 0 18px rgba(7,30,24,.18)
header + sidebar (모든 viewport) sidebar와 같음, GNB 띠 숨김 같음
(둘 다 없음) 0 없음

값은 --hm-gnb-join, --hm-lnb-join을 읽는다(하드코딩 없음). 콘텐츠 면 뒤 색(--hm-dashboard-background)은 LNB 끝 색 --hm-lnb-background-end.

7. Main Layout

  • padding 34px(--hm-dashboard-padding, 모든 viewport), 세로 gap 24px(--hm-dashboard-gap), 배경 --hm-sem-bg-content.
  • min-width: 0: Main, DashboardContent와 그 직접 자식. 표·Carousel·Combobox가 페이지를 밀지 않는다(검증: 1600px 넓은 내용도 Main scrollWidth를 늘리지 않음).
  • 스크롤: 바깥 Dashboard frame 높이는 --hm-dashboard-height(기본 100dvh)이며 가로 scrollbar도 이 높이에 포함된다. Main이 세로 스크롤, 바깥 frame이 Dashboard 전체 가로 스크롤을 맡는다. Main은 overflow-x:auto, overscroll-behavior-y:contain으로 가로 제스처가 바깥 frame으로 전달된다. LNB는 메뉴 영역만 자체 세로 스크롤한다.
  • 임베드 시 높이는 --hm-dashboard-height로 바꾼다(Story Framed).

8. Page Header

별도 Pattern(page-header.md)으로 분리했다. 모든 화면이 반복해서 쓰고(Breadcrumb + h1 + actions), 간격(하단 20)이 ui-viz Layout contract라서다. Breadcrumb는 HM Breadcrumb를 그대로 쓰며 Page Header는 간격만 맡는다.

9. Desktop / Mobile

Dashboard Minimum Width Policy: HORIZONTAL_SCROLL
Content minimum width: 960px
With Sidebar: 960px + 272px = 1232px
Below minimum width: KEEP_LAYOUT_AND_SCROLL
Compact / Sidebar mobile hide: NONE
  • 충분한 폭에서는 기존 배치를 사용한다. 작은 viewport에서는 내부 최소 폭을 유지하고 Dashboard 전용 root에서만 가로 스크롤한다. Sidebar slot은 항상 렌더링된다.
  • 960px 근거: KPI 4 × 200px + 간격 3 × 24px + padding 2 × 34px + 세로 scrollbar 여유 20px. Compact 상태 바 실측 약 549px, 브랜드 약 125px, 날개 35px도 이 폭에서 겹치지 않는다.
  • Sidebar 구성은 콘텐츠 최소 폭에 LNB 폭을 더한다. Desktop은 기존 --hm-lnb-width를 읽고, ≤1023px에서는 Dashboard 내부에만 --hm-lnb-width: var(--hm-dashboard-sidebar-width, 272px)와 LNB display:flex를 적용한다. 공통 lnb.css의 모바일 숨김은 유지된다.
  • 기본 콘텐츠 최소 폭은 --hm-dashboard-min-width로 조정할 수 있다. 기본값 960px은 현재 Desktop 구성 측정 기준이며 별도 콘텐츠를 넣으면 그 구성을 기준으로 조정한다.
  • Compact 메뉴는 숨기거나 wrap하지 않는다. Dashboard 안 Filter Bar도 한 줄과 기존 control 폭을 유지하며 공통 Filter Bar 모바일 정책은 변경하지 않는다. 본문 텍스트는 자연스럽게 줄바꿈한다.
  • GNB의 전역 메뉴·모바일 Sheet와 일반 LNB의 공통 responsive 정책, Theme/Color Token, Story ID/hierarchy는 변경하지 않는다.

10. Accessibility / Keyboard

  • landmark: header(GNB), nav[aria-label=주 메뉴], aside(LNB) 안 nav[aria-label=업무 메뉴], main, nav[aria-label=breadcrumb]. 화면의 h1은 Page Header 제목 하나.
  • Tab 순서(측정): GNB(브랜드, 메뉴 4, 사용자) → LNB(항목, 그룹, 하위) → Breadcrumb → 페이지 액션 → 검색 → 상태 → 초기화 → 표. focus trap은 Sheet/Dialog에서만.
  • skip-to-content 링크는 만들지 않았다(§12). aside에는 이름이 없고 안의 nav가 이름을 갖는다.

11. 비교 (ui-viz ↔ 현재 HM)

항목 ui-viz 현재 HM 분류
GNB 70 / LNB 272 같음 --hm-gnb-height / --hm-lnb-width MATCHED
콘텐츠 면 색·padding 34 #F5F0E5, 34 같음 MATCHED
접점 radius (LNB 18 / GNB 12), shadow 같음 같음. 소유를 Dashboard로 이동 MATCHED
헤더·메뉴 고정, 콘텐츠만 스크롤 같음 DashboardMain 스크롤 MATCHED
접점 띠가 콘텐츠 뒤에서 이어짐 형제 absolute GNB/LNB ::after + positioned Main IMPLEMENTATION_CHANGE
Page heading 간격(하단 20, h 28, gap 24) 같음 PageHeader MATCHED
Breadcrumb 12px 정적 p, 컴포넌트 아님 HM Breadcrumb(14px, 링크) INTENTIONAL_CHANGE (사용자 지시)
제목 태그 h2 h1 INTENTIONAL_CHANGE (페이지당 h1)
카드 Content Card(흰색), padding 24 HM Card MATCHED
조회 + 표 결과 카드 안, 검색 → 표 16 Card 안 Filter Bar + Data Table, gap 16 MATCHED
표 density ui-viz Table HM Data Table(Table primitive) MATCHED (primitive 기준)
GNB + LNB 동시 없음 조립(GNB 평평, LNB 아래) INTENTIONAL_CHANGE / FOLLOW_UP
반응형 ≤1100px/48px OUT_OF_SCOPE Dashboard 최소 폭 유지 + native 가로 스크롤. GNB 전역 Sheet 정책 유지 INTENTIONAL_CHANGE
스크롤바 기본 기본 —
ui-viz 화면과 픽셀 비교 — 미실시(ui-viz Storybook 미실행) —

12. 남은 차이 / 누락

항목 분류
GNB+LNB brand 중복 해결 (2026-10-06, §4 Brand ownership: GNB만, LNBBrand 없음)
GNB+LNB 조립 시각 확정(두 gradient 이음새) FOLLOW_UP — 사용자 결정 필요
LNB only의 모바일 탐색 동일 Sidebar를 가로 스크롤로 탐색 (2026-10-07 최소 폭 정책, §9)
skip-to-content, main 이름 FOLLOW_UP
ui-viz Variant C(Compact 48 + Status Filter Bar) 대표 Story에 구현됨. 최소 폭 960px으로 유지
실기기 touch/trackpad, ui-viz 픽셀 비교 미확인
문서 스크롤 방식(frame 높이 없이 body 스크롤) 변형 필요 시 --hm-dashboard-height: auto + GNB sticky로 가능하나 검증하지 않음
누락 컴포넌트 점검
항목 필요도 이유 다음 단계
KPI Card 권장 Card+Badge+Progress를 4곳에서 직접 조합. 반복되면 Pattern 실제 제품 요구 때
Page-level Empty State 권장 표 Empty만 있고 페이지 전체 빈 상태(첫 사용·권한 없음) 없음 요구 확인 후
Page Loading 후속 Skeleton + Data Table loading 조합으로 충분(Story Loading) 보류
Bulk Action Bar 권장 표 선택 후 일괄 작업 영역 없음 Data Table 확장 때
Filter Chip / 적용 조건 표시 후속 Filter Bar 문서 §12에 기록됨 보류
Mobile Filter Drawer 후속 Dashboard에서는 최소 폭 안에서 기존 Filter Bar를 유지 보류
Notification Center, Global Search Result 후속 GNB 문서 보류
Skip link 권장 키보드 사용자: GNB/LNB 건너뛰기 접근성 보완 때

13. 검증 (2026-10-07, 최소 폭 정책)

현재 검증 결과는 §9의 HORIZONTAL_SCROLL 정책을 따른다. 아래 Desktop 전용 정책 검증은 이전 작업 이력이다.

당시 Storybook 포트 6007, Chromium, 높이 900px에서 실측했다.

viewport Compact 내부 폭 Compact 가로 스크롤 GNB + LNB 내부 폭 GNB + LNB 가로 스크롤 메뉴·콘텐츠 레이아웃 붕괴
1280 1265 없음 1280 없음 유지 없음
1024 1009 없음 1232 있음 유지 없음
1023 1008 없음 1232 있음 유지 없음
768 960 있음 1232 있음 유지 없음
390 960 있음 1232 있음 유지 없음
320 960 있음 1232 있음 유지 없음
  • 대표 Compact Story는 바깥 Story 설명 때문에 문서에 세로 scrollbar 15px이 있다. 위 내부 폭은 이를 제외한 가용 폭이며 100vw를 사용하지 않는다. 모든 조합에서 문서 가로 넘침은 0px이다.
  • Sidebar 폭 272px, Main padding 34px, Filter Bar 검색 폭 280px·높이 34px이 모든 폭에서 유지된다. KPI 4개는 작은 viewport에서도 한 줄, 각 약 201px이다. Main 자체 가로 넘침은 0px이다.
  • Third Level, GNB Only, LNB Only, Long Content, Loading, Empty, Framed를 1280·320px에서 추가 확인했다. 문서 가로 넘침 0px, 넓은 1600px 내용은 카드 내부에서만 스크롤, Framed 높이는 scrollbar를 포함해 640px으로 유지된다.
  • 320px에서 native 가로 wheel 입력으로 Compact scrollLeft 655px / Sidebar 구성 912px(각 최대값)에 도달하고 0px으로 복귀했다. 문서 scrollLeft는 0px으로 유지된다. 스크롤 root의 ArrowRight 입력도 동작한다.
  • Compact Tab 순서 전체 → 활성 → 대기 → 비활성 → 검색을 확인했다. 비활성 버튼에 focus하면 scrollLeft 435px으로 자동 이동하며 버튼 전체와 focus indicator가 viewport 안에 보인다. 스크롤 JS, touch-action 제한, 입력 이벤트 차단은 추가하지 않았다. 실제 touch·trackpad·Shift+wheel 장치는 별도 실기기 확인이 필요하다.
  • 일반 GNB Story의 모바일 높이 56px·desktop nav 숨김은 유지된다. 공통 GNB/LNB CSS와 JS는 수정하지 않았다.
  • 실제 터치 장치 테스트는 수행하지 않았다.
  • npm run typecheck, 최종 UI package build, Storybook static build, git diff --check 통과. 기존 문서 예시 bg-[var(...)] CSS 경고와 500kB chunk 경고는 유지된다.
이전 Desktop 전용 정책 검증 (2026-10-07, Storybook / Chrome)
  • Dashboard Story 9개 × 1280·1024·1023·390 = 36개 조합 통과. 대표 Compact 메뉴는 Desktop 표시, Mobile 높이 0. Sidebar는 Desktop 폭 272px, Mobile DOM에서 제거된다.
  • 1280: GNB + LNB Main 폭 1008px. 1024: Main 폭 752px. 1023·390: Main이 각각 1023·390px 전체 폭을 사용하며 빈 sidebar 공간 없음. 모든 조합에서 문서 및 Main 가로 overflow 0.
  • Mobile: Dashboard 로컬 메뉴·Compact 상태 필터가 접근성 tree에 없고 Tab으로 접근되지 않는다. GNB Sheet에는 기존 전역 메뉴만 표시하고 Dashboard 업무 메뉴는 없다.
  • Desktop LNB hover·active 변경·그룹 열림/닫힘 통과. 열린 LNB Profile portal은 1024 → 1023 전환 시 함께 제거되고 1024로 복귀하면 sidebar가 다시 표시된다.
  • 패키지 build·typecheck 통과. Story 수정 후 루트 tsc -b 통과. 브라우저 runtime error 없음.
이전 검증 이력

아래 모바일 메뉴 관련 기록은 이전 정책의 이력이다. 현재 정책은 §9이며 모든 viewport에서 Dashboard 메뉴·콘텐츠를 유지하고 최소 폭 이하에서 가로 스크롤한다.

  • 1280: 문서 스크롤 없음(scrollHeight == clientHeight), Main만 스크롤(scrollTop 400에서 GNB y 0·LNB y 70 유지). h1 1개, landmark 위 목록.
  • 1024 desktop / 1023 mobile 전환, 768, 390: 문서·Main 가로 overflow 0. GNB-only, LNB-only, Long Content(긴 제목 + 1600px 내용) 모두 overflow 0.
  • 390: 모바일 Sheet에 전역/로컬 그룹 표시, current 표시.
  • 2026-10-06 Third Level: 1280·1024 GNB 70 + LNB 272(3뎁스 active "전체 업무", 상위 "업무 관리"·"업무 현황" current), 1023·768·390 GNB 56 + 햄버거, LNB display:none, Sheet에서 업무 메뉴 → 업무 관리(+12) → 업무 현황(+24) → 3뎁스 링크, active 동일. Sheet에서 다른 3뎁스를 고르면 LNB active도 같이 바뀐다. 5개 폭 모두 문서 가로 overflow 0.
  • 2026-10-06 LNB Only 모바일: 1280·1024 GNB 없음 + LNB 272, radius 18 0 0 18; 1023·768·390 상단 바 56(햄버거 1개, header 1개) + LNB 숨김, radius 12 12 0 0, 가로 overflow 0. Sheet는 "업무 메뉴" 하나(전역 메뉴 없음), active 동일. Dashboard Layout·Third Level(GNB + LNB)은 5개 폭 모두 GNB 하나(1023 이하 56 + 햄버거 1개), Sheet 전역 + 업무 메뉴 그대로.
  • 2026-10-06 Brand ownership: Dashboard Layout·Third Level 1280·1024 brand = GNB 1개(LNBBrand 없음, LNB 메뉴 top 98 = GNB 70 + 28), 1023·390 GNB 모바일 바 1개. LNB Only 1280·1024 LNBBrand 1개(상단 바 없음), 1023·390 Local 모바일 바 1개. 서비스명은 모든 폭에서 1개. 수치는 lnb.md §8.
  • 기존 Pattern(GNB 70/12px 띠, LNB 272, Data Table, Filter Bar) Story 오류·overflow 없음.
  • npm run typecheck, npm run build 통과.

14. 관련 파일

packages/ui/src/patterns/dashboard-layout/{dashboard-layout.tsx,index.ts}
packages/ui/src/patterns/page-header/{page-header.tsx,index.ts}
packages/ui/src/theme/{dashboard-layout.css,page-header.css}
src/patterns/dashboard-layout.stories.tsx