워크스페이스 전환 (WorkspaceSwitcher)
워크스페이스 전환 드롭다운의 표시 상태를 확인하고, 사용 기준과 호출 방법을 찾아보세요.
Alpine 컴포넌트 · Web · 진행 중(WIP) · 신규 도입 금지 · 문서 포맷 시범 적용
상태: 워크스페이스(admin) 미사용 (2026-06~)
총무·경영 대시보드가 워크스페이스로 병합되면서 admin에서는 앱 전환 드롭다운이 불필요해져 제거되었고, 자리에는 비클릭 정적 브랜드(.ws-brand — 심볼 자리 + 두 줄 텍스트, 버튼 크롬·클릭 없음)가 들어갔다. 텍스트는 카탈로그 제목이 아니라 $store.hereby.brand(예: hereby Medical)와 조직명이고, 심볼 자리는 병원 로고가 가져갈 수 있다(2026-08 — 아래).
- 이 드롭다운 컴포넌트는 현재 총무(4005)·경영 대시보드(4006) 등 단독 접속 화면에서만 사용 중
- 단독 화면들도 추후 정리 대상(후속 검토) — 신규 화면에 이 드롭다운을 새로 도입하지 말 것
ws-brand는ws-trigger와 같은 배치(높이 44·gap 8)에 배경·테두리·커서만 없는 변형이며, 접힘 시 로고만 남는다
Interactive playground
현재 워크스페이스: 총무 · 드롭다운 닫힘— 트리거를 눌러 열고 항목을 클릭해 전환해 보세요
.ws-* 클래스로 렌더해요 — 트리거를 눌러 드롭다운을 열고 항목을 클릭해 전환해 보세요. `current` 는 카탈로그 id 와 정확히 일치해야 합니다 — 틀리면 조용히 실패해 라벨이 「Hereby」로 폴백되고 어떤 항목도 현재로 표시되지 않아요. 호출 시 인라인 style="padding: 0; min-height: 0;" 을 빼면 헤더가 틀어집니다.<!-- 인라인 style 이 필수다. 빼면 .ds-lnb__header 의 padding 과 겹쳐 헤더가 틀어진다 -->
<div class="ds-lnb__header" style="padding: 0; min-height: 0;"
x-data="workspaceSwitcher({ current: 'general-affairs' })">
<!-- include "../../packages/components/html/workspace-switcher.html" -->
</div>드롭다운 트리거 — 총무가 현재
드롭다운 트리거 — 경영 대시보드가 현재
정적 브랜드(.ws-brand) — admin 이 쓰는 비클릭 변형
LNB 접힘 — 텍스트·chevron 숨김
표시 형태(드롭다운 트리거 · 정적 브랜드) · current · LNB 접힘을 골라 실제 .ws-* 클래스로 렌더된 모습과 호출 코드를 확인할 수 있습니다. 대표 상태 네 가지는 「모아보기」 탭에 있습니다.
⚠️ docs 환경 차이 (실제 앱 아님): 위 데모 항목의 파란 밑줄은 docs(VitePress)가 본문 안의
<a>링크에 자동으로 입히는 스타일입니다. 실제 컴포넌트(workspace-switcher.html)도 항목을<a>로 쓰지만, 앱에는.vp-doc링크 스타일이 없어 밑줄이 생기지 않습니다. → CSS로 억제하면 불필요한 override(강제)가 생기므로, 강제 대신 이 차이를 텍스트로만 명시합니다.
규칙 (강제) · 코딩 에이전트용
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-' + $store.hereby.brandFamily)로 심볼을 그리거든요. 지우면 심볼이 사라집니다.
- ⚠️ 테스트할 때 로고를 등록한 조직으로 보면 안 됩니다. 2026-08부터 병원 로고를 등록한 조직은 그 자리를
<img class="ws-brand__org-logo">가 차지해iconFor를 아예 안 탑니다 — x-data를 지워도 그 조직에서는 멀쩡해 보이고, 로고 없는 조직에서만 깨집니다. - 참고: admin의
.ws-brand라벨은 카탈로그 제목이 아니라$store.hereby.brand(윗줄)와$store.hereby.currentUser?.organizationName(아랫줄)에서 옵니다. 같은.ws-trigger-name클래스라고 데이터 출처가 같다고 가정하지 마세요.
복제본 없음 — 앱 main.css 어디에도 .ws-* 재정의가 없습니다. 정본만 고치면 돼요.
- 다크 모드(2026-10-19) — 트리거(
.ws-trigger*)의 글자 · 테두리 · 틴트는 정적 흰 잉크--ds-ink-white-*(LNB 와 같은 이유 —--ds-glass-*는 다크에서 어두워짐), 드롭다운.ws-dropdown면은--color-bg-elevated(전--ds-bw-white고정).
진행 중 (WorkspaceSwitcher component — work in progress)
- LNB 헤더 영역(
.ds-lnb__header)에 자리 잡는 워크스페이스 전환 드롭다운. 한 곳에서 정의된 워크스페이스 카탈로그를 모든 앱이 공유. - 트리거 자체는 LNB 다크 톤 (글래스 tints) 사용, 드롭다운 패널은 흰 배경 사용.
- 토큰 fix 완료. 단, font-weight, transition 120/150ms, border-radius 7px, 300px width, 1px hairline 정책 결정 대기 — 검토 필요 사항 참고.
구조
- 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 우측으로 펼침.
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" 배지
.ws-brand 의 심볼 자리
admin이 쓰는 비클릭 변형(.ws-brand)의 왼쪽 24px 정사각은 두 가지가 번갈아 들어옵니다.
| 조직 | 들어오는 것 | 어떻게 |
|---|---|---|
| 병원 로고 등록 | 업로드한 로고 | <img class="ws-brand__org-logo"> — 24px 정사각 + object-fit: contain(비율 제각각이라 잘라내지 않습니다) + radius-4 |
| 병원 로고 미등록 | 패밀리 심볼 | iconFor('logo-hereby-' + $store.hereby.brandFamily) |
「hereby 심볼 × 병원 로고」 코브랜딩은 없어졌습니다 (2026-08, PR #591)
둘을 ×로 나란히 놓던 시절에는 208px LNB에서 브랜드 텍스트가 쓸 수 있는 폭이 29px까지 밀려(.ws-brand 안쪽 136 − 심볼 24 − gap 8 − 코브랜딩 블록 75) 「hereby Me…」/「히얼바이 …」로 잘렸습니다. 지금은 심볼 자리 자체를 로고가 대체하고, hereby는 LNB 하단 브랜드 마크가 맡습니다. 나란히 놓는 표기를 되살리지 마세요.
조직 정보(orgInfo)는 앱 부팅 시 불러옵니다. 설정 탭 로더에서만 불러오던 때는 설정을 한 번도 열지 않은 화면에서 로고가 등록돼 있어도 안 나왔습니다. 실패하면 패밀리 심볼로 물러나므로 대시보드를 막지 않습니다.
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), 아이콘 흐림 |
호출 패턴
<div class="ds-lnb__header" x-data="workspaceSwitcher({ current: 'admin' })">
<!-- include "../../packages/components/html/workspace-switcher.html" -->
</div>워크스페이스 카탈로그는 컴포넌트 내부에 정의 (단일 출처). 호출 시엔 current (현재 워크스페이스 id) 만 전달.
API
workspaceSwitcher(opts) Alpine.data factory. 옵션은 단순:
| Prop | 타입 | 설명 |
|---|---|---|
current | string | 현재 워크스페이스 id ('admin' 등) — 카탈로그의 id와 일치해야 함 |
타입
| Type | 필드 |
|---|---|
WorkspaceItem | id, group, title, desc, path, devPort, icon, hidden?, disabled?, badge? |
워크스페이스 카탈로그
WorkspaceSwitcher.ts 내부 ITEMS 배열에서 한 곳 관리.
| 그룹 | id | 제목 | 설명 | 상태 |
|---|---|---|---|---|
| 조직 운영 | admin | Workspace 설정 | 직원·팀·역할 | 표시 |
| 조직 운영 | hr-system | 급여관리 | 월급 명세·수당·공제 | hidden |
| 분석 & 스케쥴 | management-dashboard | 경영 대시보드 | 인건비·청구·인력 분석 | 표시 |
| 분석 & 스케쥴 | team-manager | 내부 결재 | 휴가·초과근무·승인 | hidden |
| 업무 & 자원 | general-affairs | 총무 | 교육·정기업무·공지 | 표시 |
| 업무 & 자원 | inventory | 재고관리 | 물품·유통기한·환불 | disabled |
hidden: true → DOM에 남지만 hidden 속성으로 시각적 제외 (페이지 통합 전이라 잠시 숨김 처리) disabled: true → 표시되지만 클릭 불가, 흐린 색
사용된 토큰 (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) |
단일 출처
| 파트 | 파일 |
|---|---|
| Alpine 컴포넌트 | packages/components/src/components/WorkspaceSwitcher.ts |
| HTML partial | packages/components/html/workspace-switcher.html |
| 스타일 | packages/static/styles/design-system.css (.ws-*) |
검토 필요 사항
토큰 체계가 아직 커버하지 못해 하드코딩으로 남겨둔 항목:
- 🟡 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 |
| 이전 | 단일 워크스페이스 카탈로그 컴포넌트로 통합 — 모든 앱이 동일한 스위처 공유 |