PageHeader
진행 중 (PageHeader component — work in progress)
- 페이지 본문 영역 상단에 자리 잡는 tab-level 타이틀바. admin / team-manager 등 공통 페이지 헤더로 사용.
- 3개 region (TitleBar / ActionGroup / Toolbar) 구조로 분할되어 있어 페이지별 액션을 끼워넣을 수 있음.
- 토큰 fix 완료 (시각 변화 0). 단, font-weight / line-height / letter-spacing / transition 일부는 정책 결정 대기 — 검토 필요 사항 참고.
규칙 (강제) · 코딩 에이전트용 — 사람은 접힌 채로 두어도 됩니다
PageHeader 강제 규칙. 상세는 아래 본문.
템플릿 3종 — 서로 바꿔 쓸 수 없어요
| 파일 | 언제 |
|---|---|
html/page-header.html | ActionGroup이 없는 페이지에서만. <header class="ds-page-header">를 이미 emit 하므로 또 감싸지 마세요. |
html/page-header-title-bar.html | 셸을 직접 쓸 때 region 1 |
html/page-header-toolbar.html | 셸을 직접 쓸 때 region 3 |
- 세 파셜 모두 주인 없는 마크업이라, 조상에
x-data="pageHeader({...})"가 반드시 있어야 합니다. - ActionGroup이 있으면
page-header.html을 include 하지 마세요. 셸을 직접 쓰고 region을 title-bar →<div class="ds-page-header__action-group">→ toolbar 순서로 넣으세요. - 순서가 레이아웃을 지탱합니다.
.ds-page-header는justify-content: flex-start이고, 오른쪽 정렬은 오직.ds-page-header__title-bar블록의margin-right: auto한 줄이 만듭니다. title-bar를 빼거나 순서를 바꾸면 레이아웃이 무너집니다(space-between폴백 없음). - 헤더 줄을 감싸는 래퍼에는
.ds-page-header-layer를 씁니다(--ds-z-page-header40) — Tailwindz-*유틸을 붙이지 마세요. 표의 sticky thead(30)보다 위(아니면 thead가 알림 팝업을 가림), 모달(50)보다는 아래입니다. - ⚠️ 팝업이 뜨면 툴바의 「불편·기능 신고」는 딤에 가려 못 누릅니다. 그 순간을 위한 대체 진입점은 딤 위에 뜨는 플로팅 버튼(
.ds-report-fab)이고, 헤더가 아니라cs-report.html이 소유합니다 → FAB 배치 계약. 헤더 안 버튼만 따로 올리려 하지 마세요 — 래퍼의z-index(그리고position: sticky)가 쌓임 맥락을 만들어 그 안의z-index는 바깥과 겨루지 못합니다(자식에position: fixed를 줘도 같습니다). 줄 전체를 올리면 팝업 중에 상단바만 밝게 남아 어색합니다. - ActionGroup은 반드시
.ds-page-header__action-group래퍼로 감싸세요 — 맨 버튼을 헤더에 직접 넣으면 gap·shrink가 틀어집니다.
props
showNotification의 기본값은true입니다. 종을 숨기려면 **명시적으로showNotification: false**를 넘겨야 해요(생략 ≠ 숨김).showProfile은 기본false(opt-in).- 반응형 prop과 정적 prop은 이름이 다릅니다:
getTitle/getIcon/getShowBack등get*는 매 렌더 재평가,title/icon/showBack은 init에서 한 번만 캡처. 값이 나중에 바뀌는데title:로 넘기면 헤더가 조용히 얼어붙습니다. showProfile: true는 SDK 스토어를 필수로 요구합니다($store.hereby.currentUser,logout()).- 툴바는
notification-bell.html+notificationBell팩토리를 끌어옵니다 — 등록 안 되어 있으면 툴바 전체가 터집니다. onOrgInfo를 안 넘기면 프로필 팝오버의 '조직 설정' 줄이 통째로 사라집니다(showOrgInfo: !!onOrgInfoCb).
⚠️ 트랩 ① 여기서 radius를 고쳐도 아무 일도 안 일어납니다 전역 <button> 4px !important 강제(design-system.css의 button:not([class*="bg-"]):not(.ds-tab-item)… 규칙)에서 .ds-page-header__avatar만 제외돼 있습니다. 나머지는 6px를 선언만 하고 실제로는 4px로 렌더돼요:
| 요소 | 선언값 | 실제 렌더 |
|---|---|---|
.ds-page-header__back | 6px | 4px |
.ds-page-header__refresh | 6px | 4px |
.ds-page-header__profile-menu-item | 6px | 4px |
→ 선언을 고쳐도 소용없어요. 바꾸려면 그 button:not(…) 규칙의 :not() 체인에 클래스를 추가해야 합니다.
- 참고: 아바타는 원형이 아니라 8px 라운드 사각형(32px 박스)입니다. 원형인 건 팝오버 안의
.ds-page-header__profile-menu-thumb(<span>이라 강제와 무관)예요.
⚠️ 트랩 ② 팀 셀렉트에 background: 축약형을 쓰면 chevron이 사라집니다.ds-page-header__team-select 블록은 background-color + background-image: var(--ds-dropdown-chevron)를 일부러 분리해서 씁니다(블록 안에 경고 주석 있음). background: 하나로 합치면 chevron이 지워지고 컨트롤이 맨몸으로 렌더돼요.
⚠️ 트랩 ③ admin은 헤더를 덮어씁니다
- admin
main.css의.ds-page-header { border-bottom: none }블록(미디어쿼리 밖)이 이를 전역으로 겁니다. 구분선은 바깥 sticky 컨테이너가 그려요 — admin에서 그 컨테이너 밖에 헤더를 놓으면 밑줄이 없습니다. - 같은 파일의
@media (max-width: 767px)안에 있는.ds-page-header블록: 767px 이하에서 헤더는 56px(min-height: var(--ds-spacing-56))이고 아이콘이 숨겨집니다. "헤더는 64px"는 ≥768px에서만 참이에요.
라이브 미리보기
실제
.ds-page-header*클래스로 렌더되는 인터랙티브 데모 — 풀 상태(back + icon + title + refresh + ActionGroup + team filter + bell + 프로필 아바타). ↻ 버튼은 약 1초 로딩(스피너 + disabled), 팀 필터로 선택을 바꾸고, 우측 끝 아바타를 눌러 프로필 팝오버를 확인하세요.
조직 관리
선택 팀: 전체 팀 · 새로고침: 대기— ↻ 클릭 시 약 1초 로딩, 팀 필터로 선택 변경, 우측 끝 아바타 클릭 시 프로필 팝오버
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/PageHeader.ts |
| Wrapper partial | packages/components/html/page-header.html |
| TitleBar partial | packages/components/html/page-header-title-bar.html |
| Toolbar partial | packages/components/html/page-header-toolbar.html |
| 스타일 | packages/static/styles/design-system.css (.ds-page-header*) |
구조 (3 Region)
┌─ TitleBar ────────────────┬─ ActionGroup ─┬─ Toolbar ──────────────────────┐
│ ← [icon] Title ↻ │ [page btns] │ team ▼ │ 🔔 (프로필) │
└───────────────────────────┴───────────────┴────────────────────────────────┘
페이지 정체성 페이지별 액션 공용 도구- TitleBar (필수) — 뒤로가기(opt-in) · 아이콘(opt-in) · 제목 · 새로고침(opt-in). 모든 호출 위치에서 partial 재사용으로 동일.
- ActionGroup (opt-in) — 페이지가 직접 작성하는 액션 버튼 (예: admin의 "역할 관리 →"). 없으면 wrapper 통째 생략 가능.
- Toolbar (필수) — 팀 필터(opt-in) · 구분선 · 알림 종(기본 ON) · 프로필 아바타(opt-in, 맨 끝). 모든 호출에서 partial 재사용.
전체 헤더 높이는 64px (이전 65px → 2px step 정렬).
두 가지 호출 패턴
1. 단순 — ActionGroup 없음
<div x-data="pageHeader({ title: '직원관리', icon: 'ic-employee', showRefresh: true })">
<!-- include "../../packages/components/html/page-header.html" -->
</div>→ wrapper partial 한 줄로 TitleBar + Toolbar 자동 렌더링.
2. ActionGroup 필요 — <header> shell 직접 작성
<div x-data="pageHeader({ title: '조직관리', icon: 'ic-organization', showRefresh: true })">
<header class="ds-page-header">
<!-- include "../../packages/components/html/page-header-title-bar.html" -->
<div class="ds-page-header__action-group">
<button class="btn-primary">역할 관리 →</button>
</div>
<!-- include "../../packages/components/html/page-header-toolbar.html" -->
</header>
</div>→ 3 region을 순서대로 배치. TitleBar의 margin-right: auto가 ActionGroup + Toolbar를 우측에 정렬.
API
pageHeader(opts: PageHeaderOptions) Alpine.data factory.
Props (모두 optional)
| 그룹 | Prop | 타입 | 설명 |
|---|---|---|---|
| Title | title | string | 정적 제목 |
getTitle | () => string | 호스트 상태에 반응하는 제목 source | |
| Icon | icon | string | 아이콘 이름 (@hereby/components icon registry) |
getIcon | () => string | 반응형 아이콘 source | |
| Back nav | showBack | boolean | 뒤로가기 ← 버튼 표시 (정적) |
getShowBack | () => boolean | 반응형 toggle | |
onBack | () => void | 클릭 핸들러 | |
| Refresh | showRefresh | boolean | 새로고침 ↻ 버튼 표시 |
onRefresh | () => void | 클릭 핸들러 | |
getRefreshLoading | () => boolean | 로딩 중일 때 disable + 스피너 | |
| Team filter | showTeamFilter | boolean | 팀 드롭다운 표시 |
getShowTeamFilter | () => boolean | 반응형 toggle | |
getTeams | () => Team[] | 팀 목록 | |
getSelectedTeamId | () => string | 현재 선택 ID | |
onTeamChange | (id: string) => void | 변경 핸들러 | |
| Notification | showNotification | boolean | 알림 종 표시 (기본 true) |
| Profile | showProfile | boolean | 프로필 아바타 표시 (기본 false, opt-in) |
onLogout | () => void | 로그아웃 핸들러 — 호스트의 확인 플로우(예: admin doLogout)를 연결. 미지정 시 store 직접 로그아웃 |
Team 타입: { id: string; name: string; memberCount?: number }
프로필 아바타 & 팝오버
LNB 하단에 있던 사용자 프로필을 헤더 우측 끝으로 이동하며 도입된 영역 (LNB 개편, 2026-06).
- 아바타 —
.ds-page-header__avatar. 32px ·--radius-8· 브랜드 그라데이션(컴포넌트 전용 예외) · 메일 첫 글자(대문자). 알림 벨과의 시각 간격 24(= toolbar gap 8 + margin 16). 전역 버튼 radius 강제(4px) 규칙의 제외 명단에 등록되어 있음 - 팝오버 —
.ds-page-header__profile-menu. 아바타 클릭 시 우측 정렬 드롭다운: 썸네일(64px 원형) → 메일 → 이름(있을 때만) → 구분선 → 내 정보 · 로그아웃(danger). 알림 팝업과 동일한 1px 테두리(--color-line-normal) +--shadow-blur4 - 동작 — 내 정보는 공유 MyInfoForm을 여는 window
open-my-info이벤트 사용. 로그아웃은onLogout콜백(없으면$store.hereby.logout()). 바깥 클릭으로 닫힘 - 색은 전부 DS 토큰:
neutral-25/100/600/900,neutral-75(hover),red-500(로그아웃),--ds-bw-white
시각 미리보기
아이콘은
packages/components/src/icons.ts에 등록된 실제 hereby 아이콘 SVG 그대로 사용. 아래 1)~4)는 개별 상태 정적 레퍼런스입니다.
1) 미니멀 — title + Toolbar (기본 호출)
대시보드
2) Title + Icon + Refresh
직원 관리
3) Back nav + Title + Refresh (embed wrapper)
휴가 신청 상세
4) 풀 상태 — Back + Icon + Title + Refresh + ActionGroup + Team filter + Bell + 프로필
조직 관리
→ 사용된 실제 아이콘: chevron-left (back), ic-employee / ic-calendar / ic-organization (title icon), ic-refresh, ic-bell. TitleBar의 margin-right: auto가 ActionGroup + Toolbar를 우측에 정렬. 우측 끝 아바타(showProfile)는 radius 8 · 벨과 간격 24(gap 8 + margin 16).
사용된 토큰 (fix 후)
| 카테고리 | 토큰 |
|---|---|
| Spacing | --ds-spacing-{2, 8, 12, 14, 16, 20, 22, 28, 64} + 프로필 {4, 6, 8, 10, 16, 32, 64, 240} |
| Color (text/border/bg) | --color-neutral-{25, 50, 75, 100, 300, 500, 600, 700, 900}, --color-orange-500, --color-red-500(로그아웃), --color-line-normal, --ds-bw-white |
| Radius | --radius-4 (team-select), --ds-radius-sm (back/refresh 버튼, 6px), --radius-8 (아바타), --radius-16 (프로필 팝오버), --radius-32 (팝오버 썸네일) |
| Opacity | --ds-opacity-50 (disabled refresh), --ds-opacity-90 (아바타 hover) |
| Typography | --text-b2 (title 18px), --text-b6 (team-select·메일 12px), --text-b4/--text-b5/--text-h3 (프로필 팝오버) |
| Shadow | --shadow-blur4 (프로필 팝오버) |
검토 필요 사항
토큰 체계가 아직 커버하지 못해 하드코딩으로 남겨둔 항목. 디자이너 결정 후 토큰화 진행:
- 🟡 font-weight: 700 (title) — font-weight 토큰 미정의 (typography 페이지에도 명시됨)
- 🟡 title line-height: 1.3 — B2 기본(1.4)과 다름. PageHeader 전용 타이포 필요 여부 결정
- 🟡 title letter-spacing: -0.3px — B2(-0.18px) / H3(-0.24px) 어디와도 다름 — line-height 항목처럼 PageHeader 전용 타이포 정의 여부를 함께 결정 필요
- 🟡 transition 0.15s / 0.3s (4곳) — motion scale 부족 (
--duration-fast100ms /--duration-normal200ms만 정의) - 🟡 divider width: 1px — spacing 토큰 최소가 2px. hairline 1px 토큰 정책 결정 필요
- ✅ min-height: 65px → 64px — 2px step 정렬을 위해 1px 축소 적용 (시각 영향 미미)
- ✅ color fallback을 잘못된
#767676에서 neutral-500 실제값#5e5e5e로 교체 (var(--color-neutral-500, #5e5e5e))
사용처
- admin (apps/admin/src/views/dashboard.html) — 모든 native tab의 헤더 (조직관리, 직원관리, 역할관리 등에서 ActionGroup 사용)
- admin embed (apps/admin/src/views/tabs/embed.html) — 임베드된 다른 앱(team-manager 등) 위에 외부 헤더로 적용
- team-manager (apps/team-manager/src/app.ts) — embedded mode (chrome-less iframe)에선 자체 헤더 숨김, admin의 외부 PageHeader가 표시 담당
목표 & 로드맵
| 항목 | 현재 | 목표 |
|---|---|---|
| 3 Region 구조 | ✅ TitleBar / ActionGroup / Toolbar | 유지 |
| Spacing 토큰 매핑 | ✅ 완료 | 유지 |
| Color 토큰 매핑 | ✅ 완료 (fallback 정합성 포함) | 유지 |
| Radius 토큰 매핑 | ✅ 완료 | 유지 |
| Font-size 토큰 매핑 | ✅ b2 / b6 | 유지 |
| Font-weight 토큰화 | 🔴 미정의 | typography 페이지에서 weight scale 정의 후 적용 |
| Title typography 정렬 | 🟡 line-height/letter-spacing 비표준 | 디자이너 검토 후 표준 매핑 or 전용 토큰 정의 |
| Motion scale 확장 | 🟡 0.15s/0.3s 사용 중 | --duration-{xs, sm, md, lg} 등 세분화 |
| Hairline 1px 토큰 정책 | 🔴 미정의 | spacing 또는 별도 border 토큰 결정 |
| 컴포넌트 단위 가이드라인 | 🔴 미작성 | 호출 패턴 1/2 권장 시점, ActionGroup 디자인 규칙 등 |
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-06-12 | 프로필 아바타+팝오버 도입 (showProfile/onLogout opt-in) — LNB 하단 프로필을 헤더 우측 끝으로 이동. 아바타 radius 8·벨과 간격 24, 팝오버(썸네일·메일·이름·내 정보·로그아웃) 전 색 DS 토큰. workspace(admin) native·embed 헤더 적용 |
| 2026-05-28 | 토큰 audit + fix: spacing 14곳·radius 3곳·opacity 1곳·typography 2곳·color fallback 1곳 → DS 토큰으로 교체. min-height 65→64px (2px step 정렬). 시각 변화 ≈ 0 |
| 이전 | PageHeader family 신규 (TitleBar/ActionGroup/Toolbar 3 region 분할 + partial 분리 + 65px 통일 + 뒤로가기·refresh 좌측 통일) |