WorkspaceSwitcher
진행 중 (WorkspaceSwitcher component — work in progress)
- LNB 헤더 영역(
.ds-lnb__header)에 자리 잡는 워크스페이스 전환 드롭다운. 한 곳에서 정의된 워크스페이스 카탈로그를 모든 앱이 공유. - 트리거 자체는 LNB 다크 톤 (글래스 tints) 사용, 드롭다운 패널은 흰 배경 사용.
- 토큰 fix 완료. 단, font-weight, transition 120/150ms, border-radius 7px, 300px width, 1px hairline 정책 결정 대기 — 검토 필요 사항 참고.
규칙 (강제) · 코딩 에이전트용 — 사람은 접힌 채로 두어도 됩니다
WorkspaceSwitcher 강제 규칙. 상세는 아래 본문.
⚠️ 신규 화면에 이 드롭다운을 새로 도입하지 마세요. 실제 사용처는 general-affairs·management-dashboard 2곳뿐이고, admin은 쓰지 않습니다(admin은 클릭 불가 .ws-brand를 렌더). 이 페이지의 "사용처" 본문이 admin을 포함한다고 적어놨다면 그건 오래된 서술이에요.
호스트 마크업 — 인라인 스타일이 필수입니다
<div class="ds-lnb__header" style="padding: 0; min-height: 0;"
x-data="workspaceSwitcher({ current: 'general-affairs' })">
<!-- include workspace-switcher.html -->
</div>style="padding: 0; min-height: 0;"를 빼먹지 마세요.design-system.css의.ds-lnb__header블록은 자체로padding: 8px 10px+min-height: 36px를 갖는데, 트리거도 자기 padding·min-height(44px)를 갖고 있어서 이중 패딩으로 헤더가 틀어집니다.- ⚠️ 본문의 "호출 패턴" 코드 스니펫에는 이 인라인 스타일이 빠져 있어요. 스니펫을 그대로 복사하지 말고 위 형태를 쓰세요. (실제 호출처 2곳과 파셜 docstring은 전부 이 스타일을 갖고 있습니다.)
config
current는 카탈로그id와 정확히 일치해야 합니다:admin | hr-system | management-dashboard | team-manager | general-affairs | inventory. 틀리면 조용히 실패해요 — 라벨이"Hereby"로 폴백되고 어떤 항목도is-current가 안 됩니다.- 카탈로그(
WorkspaceSwitcher.ts의const ITEMS: WorkspaceItem[])가 단일 출처입니다. 워크스페이스 추가 = 거기 엔트리 추가..ws-item마크업을 손으로 짓지 마세요. workspace-switcher.html의x-for래퍼<div style="display: contents">를 실제 요소로 바꾸지 마세요. Alpine v3에서 중첩x-for가 깨져서 평평한entries구조로 일부러 우회한 거예요 — 바꾸면 드롭다운 flex 레이아웃이 깨집니다.- disabled 항목은 속성 3개가 다 필요해요:
is-disabled클래스 +aria-disabled="true"+tabindex="-1".
⚠️ 트랩 ① 트리거 radius는 8px 선언이지만 실제로는 4px입니다.ws-trigger는 <button>인데 전역 4px !important 강제(design-system.css의 button:not([class*="bg-"]):not(.ds-tab-item)… 규칙)의 :not() 제외 명단에 없습니다. border-radius: var(--radius-8)(8px)를 선언해도 4px로 렌더돼요. 선언을 고쳐도 소용없습니다. (.ws-item은 <a>, .ws-item-icon은 <span>이라 강제를 안 받아요. 트리거만 걸립니다.)
⚠️ 트랩 ② .ws-brand의 x-data를 "죽은 코드"라며 지우지 마세요 admin의 .ws-brand는 클릭이 안 되지만 여전히 workspaceSwitcher x-data가 필요합니다 — iconFor('logo-hereby-medical')로 로고를 그리거든요. 지우면 로고가 사라집니다.
- 참고: admin의
.ws-brand라벨은 카탈로그 제목이 아니라$store.hereby.currentUser?.organizationName에서 옵니다. 같은.ws-trigger-name클래스라고 데이터 출처가 같다고 가정하지 마세요.
복제본 없음 — 앱 main.css 어디에도 .ws-* 재정의가 없습니다. 정본만 고치면 돼요.
상태: 워크스페이스(admin) 미사용 (2026-06~)
총무·경영 대시보드가 워크스페이스로 병합되면서 admin에서는 앱 전환 드롭다운이 불필요해져 제거되었고, 자리에는 비클릭 정적 브랜드(.ws-brand — 그라데이션 로고 + "Hereby Workspace" 텍스트, 버튼 크롬·클릭 없음)가 들어갔다.
- 이 드롭다운 컴포넌트는 현재 총무(4005)·경영 대시보드(4006) 등 단독 접속 화면에서만 사용 중
- 단독 화면들도 추후 정리 대상(후속 검토) — 신규 화면에 이 드롭다운을 새로 도입하지 말 것
ws-brand는ws-trigger와 같은 배치(높이 44·gap 8)에 배경·테두리·커서만 없는 변형이며, 접힘 시 로고만 남는다
라이브 미리보기
아래는 실제로 클릭되는 라이브 미리보기입니다 — 프로덕션과 동일한
.ws-*클래스로 렌더됩니다. 트리거를 눌러 드롭다운을 열고, 항목을 클릭해 현재 워크스페이스를 전환해 보세요. (LNB 다크 헤더 톤 안에 트리거가 놓인 모습)
현재 워크스페이스: Workspace 설정 · 드롭다운 닫힘— 트리거를 눌러 열고 항목을 클릭해 전환해 보세요
⚠️ docs 환경 차이 (실제 앱 아님): 위 데모 항목의 파란 밑줄은 docs(VitePress)가 본문 안의
<a>링크에 자동으로 입히는 스타일입니다. 실제 컴포넌트(workspace-switcher.html)도 항목을<a>로 쓰지만, 앱에는.vp-doc링크 스타일이 없어 밑줄이 생기지 않습니다. → CSS로 억제하면 불필요한 override(강제)가 생기므로, 강제 대신 이 차이를 텍스트로만 명시합니다.
Dropdown 구성
- 폭 300px, 흰 배경, shadow-xl, radius 10
- 그룹별 라벨 (10px upper, neutral-400)
- 현재 워크스페이스 (
ic-workspace): orange-50 배경, 아이콘 박스 orange-600 + 흰 아이콘, 체크 표시 - 일반 항목: 흰 배경, 아이콘 박스 orange-50 + orange-600 아이콘 (
ic-chart-up,ic-clipboard) - 그룹 사이
.ws-divider(1px hairline) - disabled (
ic-cube): cursor not-allowed, 텍스트·아이콘 흐림, "Coming soon" 배지
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/WorkspaceSwitcher.ts |
| HTML partial | packages/components/html/workspace-switcher.html |
| 스타일 | packages/static/styles/design-system.css (.ws-*) |
구조
- trigger — LNB 헤더 안에서 워크스페이스 이름·설명·chevron 노출. dark 톤 (
--ds-glass-04/08/10). - dropdown — 300px width, 흰 배경, 그룹(
group-label)별로 묶인 워크스페이스 카드 리스트. - item — 아이콘 + 제목 + 설명 + (현재면) 체크 마크. orange 50/600/700 톤으로 현재 표시.
- disabled item —
is-disabled또는aria-disabled="true"— hover 무효, 흐릿한 색. - collapsed sidebar에서: trigger의 텍스트·chevron 숨김, 드롭다운은 LNB 우측으로 펼침.
호출 패턴
<div class="ds-lnb__header" x-data="workspaceSwitcher({ current: 'admin' })">
<!-- include "../../packages/components/html/workspace-switcher.html" -->
</div>워크스페이스 카탈로그는 컴포넌트 내부에 정의 (단일 출처). 호출 시엔 current (현재 워크스페이스 id) 만 전달.
워크스페이스 카탈로그
WorkspaceSwitcher.ts 내부 ITEMS 배열에서 한 곳 관리.
| 그룹 | id | 제목 | 설명 | 상태 |
|---|---|---|---|---|
| 조직 운영 | admin | Workspace 설정 | 직원·팀·역할 | 표시 |
| 조직 운영 | hr-system | 급여관리 | 월급 명세·수당·공제 | hidden |
| 분석 & 스케쥴 | management-dashboard | 경영 대시보드 | 인건비·청구·인력 분석 | 표시 |
| 분석 & 스케쥴 | team-manager | 내부 결재 | 휴가·초과근무·승인 | hidden |
| 업무 & 자원 | general-affairs | 총무 | 교육·정기업무·공지 | 표시 |
| 업무 & 자원 | inventory | 재고관리 | 물품·유통기한·환불 | disabled |
hidden: true → DOM에 남지만 hidden 속성으로 시각적 제외 (페이지 통합 전이라 잠시 숨김 처리) disabled: true → 표시되지만 클릭 불가, 흐린 색
API
workspaceSwitcher(opts) Alpine.data factory. 옵션은 단순:
| Prop | 타입 | 설명 |
|---|---|---|
current | string | 현재 워크스페이스 id ('admin' 등) — 카탈로그의 id와 일치해야 함 |
타입
| Type | 필드 |
|---|---|
WorkspaceItem | id, group, title, desc, path, devPort, icon, hidden?, disabled?, badge? |
States
| State | 표현 |
|---|---|
| trigger default | background: var(--ds-glass-04), border var(--ds-glass-08) |
| trigger hover | background: var(--ds-glass-08) |
| trigger open | background: var(--ds-glass-10), border var(--ds-glass-18) |
| item default | color: var(--color-neutral-900), transparent bg |
| item hover | background: var(--color-neutral-50) |
| item current | background: var(--color-orange-50), title var(--color-orange-700), icon bg var(--color-orange-600) + 흰 아이콘 |
| item disabled | cursor: not-allowed, 텍스트 var(--color-label-disable), 아이콘 흐림 |
사용된 토큰 (fix 후)
| 카테고리 | 토큰 |
|---|---|
| Spacing | --ds-spacing-{4, 6, 8, 10, 16, 30, 44} |
| Color | --color-neutral-{50, 100, 400, 500, 900}, --color-orange-{50, 500, 600, 700}, --color-line-normal, --color-label-disable, --color-label-muted |
| Glass tints | --ds-glass-{04, 08, 10, 18, 55, 70} |
| Brand | --ds-bw-white |
| Radius | --radius-8, --ds-radius-sm, --ds-radius-lg, --ds-radius-label-md |
| Typography | --text-b3 (chevron 16px), --text-b5 (title/check 13px), --text-c1 (desc 11px), --text-c2 (label/group/badge 10px) |
| Shadow | --ds-shadow-xl (dropdown) |
| Motion | --duration-fast (item hover transition) |
검토 필요 사항
토큰 체계가 아직 커버하지 못해 하드코딩으로 남겨둔 항목:
- 🟡 width: 300px (dropdown) — spacing 토큰 max 256px 초과. component-level layout 토큰 필요 (
--ds-layout-dropdown-width등) - 🟡 border-radius: 7px (item-icon) — radius 토큰에 7px 없음 (6/8 사이). 6 또는 8로 정렬 vs 컴포넌트 전용 유지
- 🟡 font-weight: 600 (trigger-name, group-label, item-title, item-badge) — weight 토큰 미정의
- 🟡 transition 120ms / 0.15s (trigger / chevron) — motion scale 부족
- 🟡 padding 3px 6px (item-badge) — 2px step 미정렬. 배지 인셋 정책 결정 필요
- 🟡 1px hairline (ws-divider) — spacing 최소 2px. hairline 토큰 정책
- 🟡 margin-top: 1px (item-desc) — 시각 정렬용 microvalue. 토큰화하지 않음 (디자이너 의도적 nudge)
- 🟢 글래스 톤은 정확 매칭만 사용 — 추가 step 필요 없음
사용처
.ds-lnb__header를 가진 모든 앱이 동일하게 사용:
- admin — apps/admin/src/views/dashboard.html
- general-affairs — apps/general-affairs/src/views/dashboard.html
- management-dashboard — apps/management-dashboard/src/views/dashboard.html
- 기타 LNB를 가진 앱
각 앱은 current 만 다르게 전달 (해당 워크스페이스의 id).
목표 & 로드맵
| 항목 | 현재 | 목표 |
|---|---|---|
| 단일 워크스페이스 카탈로그 | ✅ ITEMS in WorkspaceSwitcher.ts | 유지 |
| Spacing 토큰 매핑 | ✅ 완료 | 유지 |
| Color 토큰 매핑 | ✅ 글래스 + 시맨틱 | 유지 |
| Radius 토큰 매핑 | ✅ sm/lg/label-md | 7px 정렬 결정 |
| Font-size 토큰 매핑 | ✅ b3/b5/c1/c2 | 유지 |
| Font-weight 토큰화 | 🔴 미정의 | typography weight scale 도입 후 |
| Motion 토큰 확장 | 🟡 fast/normal만 | 120ms / 150ms 등 추가 |
| Layout 토큰 (dropdown width 등) | 🔴 미정의 | --ds-layout-* 도입 |
| hidden 항목 정리 | 🟡 hr-system, team-manager hidden | 페이지 통합 시점에 노출 |
| 컴포넌트 가이드라인 | 🔴 미작성 | group 분류 규칙, disabled 사용 시점 등 |
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-06-12 | workspace(admin)에서 드롭다운 제거 — 페이지 병합(총무·경영)으로 전환 기능 불요. 정적 브랜드 변형 .ws-brand(비클릭, 로고+텍스트) 신설·적용. 단독 화면(총무·경영 등)은 스위처 유지 |
| 2026-05-28 | 토큰 audit + fix: spacing ~10곳, radius 3곳, font-size 5곳, transition 1곳 → DS 토큰 매핑. 시각 변화 ≈ 0 |
| 이전 | 단일 워크스페이스 카탈로그 컴포넌트로 통합 — 모든 앱이 동일한 스위처 공유 |