FeaturesDesign System 컴포넌트Shells (운영사 셸 · AuthShell)

Shells — 운영사 셸 · AuthShell

운영사 셸은 두 군데로 나뉜다:

  • AuthShell@linkmusic/ui 의 인증 화면 셸 (packages/ui/src/components/auth-shell.tsx).
  • 운영사 백오피스 셸@linkmusic/ui 에 단일 OpsShell 컴포넌트로 존재하지 않는다. apps/admin(protected) layout 이 Topbar + Sidebar 를 조합해 구성한다.

운영사 백오피스 셸 (admin (protected) layout)

운영사 백오피스 (Surface 10) 전역 셸 — sticky Topbar + Sidebar + content area. 단일 ui 컴포넌트가 아니라 apps/admin/src/app/(protected)/layout.tsx 가 server-side 에서 /auth/me 를 fetch 한 뒤 다음 admin 셸 컴포넌트들을 조립한다 (apps/admin/src/components/shell/):

컴포넌트파일
Topbartopbar.tsx
Sidebarsidebar.tsx
AccountMenuaccount-menu.tsx
ThemeToggletheme-toggle.tsx
ToastHostProvidertoast-host.tsx@linkmusic/ui 공용 구현의 얇은 재-export (SPEC #163). 실제 구현은 packages/ui/src/components/toast-host.tsx 로 승격돼 운영사·본사·점장이 공유한다. 기존 @/components/shell/toast-host import 경로는 안정 유지(동작·시각 불변)
OpsStepperops-stepper.tsx (등록 화면 stepper)
PageHeaderpage-header.tsx (페이지 상단 헤더)

구조

// apps/admin/src/app/(protected)/layout.tsx (요약)
<ToastHostProvider>
  <div data-surface="ops">
    <Topbar email={email} name={name} />
    <div>
      <Sidebar />
      <main>{children}</main>
    </div>
  </div>
</ToastHostProvider>

사이드바 메뉴 (sidebar.tsx NAV_TOP · NAV_BOTTOM 단일 소스)

상단 9 항목 + 구분선 + 하단 3 항목. ⭐ 표식(우측 도트)은 본사·협약.

항목href비고
대시보드/placeholder
본사 ⭐/hqSPEC #013 도입
매장/storesSPEC #019 도입 (목록). sub-route /stores/new(등록) 도 active 매칭
음원/music후속 SPEC
라이브러리/libraries후속 SPEC
협약 ⭐/contracts후속 SPEC
CS 티켓/tickets후속 SPEC
감사 로그/audit/impersonation후속 SPEC
통계 (하단)/stats후속 SPEC
계정 (하단)/users후속 SPEC
설정 (하단)/settings/policiesSPEC #023 도입 — 약관 게시. sub-route 도 active 매칭. lucide Settings 아이콘 (임시 — 시안 부재)

탑바 (topbar.tsx)

  • 로고 + 구분선 + “운영사 백오피스 · BETA” 라벨 (좌)
  • ThemeToggle (light/dark/system)
  • 계정 메뉴 (AccountMenuemail/name)
  • ⌘K 글로벌 검색·장애벨·동기화 펄스 — 비기능 placeholder(전역검색·incident 도메인·상태 endpoint 부재)라 “준비 중” toast 만 띄우던 UI 를 정직성 원칙으로 제거(감사 B7). 해당 도메인 도착 시 후속 SPEC 재도입.

임퍼소네이션 모드 분기

  • 본사 모드(/admin/*)는 운영사 (protected) 셸이 아니라 별도 HQShell 로 진입한다 (아래 참고).

HQShell — 본사 모드 셸 (admin /admin layout) — SPEC #150 에서 제거

SPEC #150 — apps/admin 의 본사 모드 placeholder 셸(/admin layout·page·components/hq-shell/)은 제거됐다. 운영사 앱은 OPERATOR 전용이고, 본사 기능 단일 소스는 apps/space 다. 과거 admin 에는 실 HQ_MANAGER 직접 로그인용 placeholder 셸(6카드 “준비 중”·로그아웃 없음)이 남아 있었는데, 실 HQ_MANAGER 가 운영사 도메인으로 로그인하면 죽은 placeholder 에 갇히는 문제가 있어 제거했다. 이제 운영사 로그인 폼이 HQ_MANAGER 를 space.linkmusic.io 로 안내·차단하고, (protected) ops 셸 가드도 OPERATOR 외 전부 /login 으로 fail-closed 한다. 본사 모드 셸은 아래 apps/space HQShell 절이 유일한 진실이다. (운영사→본사 임퍼소네이션은 apps/space 핸드오프로 유지 — 영향 없음.)

HQShell — 매장 클라이언트 본사 모드 셸 (apps/space /admin layout)

신규 매장 클라이언트(apps/space · space.linkmusic.io)의 본사 모드(Surface 11) 셸. 진짜 본사 기능이 단일 소스로 있는 곳이며, 운영사→본사 임퍼소네이션의 도착지이기도 하다. apps/space/src/app/admin/ layout.tsx(server)가 두 세션을 해석해 조립한다 (apps/space/src/components/hq-shell/):

컴포넌트파일비고
HQShellhq-shell.tsximpersonation prop 으로 배너 분기, TopBar+Sidebar 조립 (client)
ImpersonationBannerimpersonation-banner.tsx빨간 배너 + 60분 카운트다운 + [운영사 백오피스로 돌아가기] (client, 임퍼소네이션만)
HQTopBarhq-topbar.tsx로고 + 본사명 + PlanBadge(plan) + 산하 매장 수(storeCount) + ThemeToggle + 계정 메뉴(HqAccountMenu, 실 HQ_MANAGER 만) (client)
HqAccountMenuhq-account-menu.tsxuseGetMe() → 공용 ShellAccountMenu(profileHref=/admin/settings·label “설정”). impersonation=false 일 때만 렌더
PlanBadgeplan-badge.tsxAI / TRUST 배지 (시안 hq-shared §PlanBadge). plan=null 이면 생략
HQSidebarhq-sidebar.tsx본사 기능 항목 — 출시된 항목 active

임퍼소네이션(impersonation=true)에서는 HQTopBar 가 계정 메뉴를 숨긴다 — 종료는 배너의 [운영사 백오피스로 돌아가기]가 담당(계정 로그아웃 노출은 sealed 세션 파기 오작동/혼란). 공용 계정 메뉴 상세는 아래 공용 계정 메뉴 참고.

두 진입 모드

HQShellimpersonation: boolean prop 으로 두 모드를 분기한다. layout 이 어느 세션으로 진입했는지에 따라 prop 을 정한다 (우선순위: 임퍼소네이션 우선).

모드세션 cookieimpersonation배너hqName 소스plan·storeCount세션 관리
임퍼소네이션lm_space_impersonation_sessiontrueO (빨강 + 60분 카운트다운)sealed 세션null·0 폴백(0-backend-call)v1 고정, refresh 없음
실 HQ_MANAGERlm_space_session role=HQ_MANAGERfalseXHqMeResponse.namegetHqMerefresh-aware

진입·가드

  • apps/space middleware.ts/impersonate-exchange(임퍼소네이션 새 탭 도착지)·/admin/* 를 bypass 한다 — /admin 은 두 세션 중 하나로 진입할 수 있어 단일 cookie presence 검사로는 임퍼소네이션 진입이 부당하게 튕긴다. layout 이 단독 가드(두 세션 모두 검사·둘 다 없으면 server-side /login, fail-closed):
    1. lm_space_impersonation_session 존재 → 임퍼소네이션 모드 (배너 O · plan/storeCount 폴백 · pmc/재동의 체크 불필요).
    2. lm_space_session role=HQ_MANAGER → loadMeRefreshAware(pmc 가드) → loadHqMeRefreshAwareGET /api/v1/hq/me(HqMeResponse) → HQShell 에 hqName·plan·storeCount 주입.
    3. role=STORE_MANAGER → /store, role≠HQ_MANAGER(OPERATOR·누락) → /login (fail-closed).
    4. 세션 없음 → /login.
  • 임퍼소네이션 배너 컨텍스트(operatorEmail·hqName)는 exchange 시 sealed 세션에 봉인 → layout 이 prop 전달 (페이지마다 backend 호출 0). 실 HQ_MANAGER topbar 값은 getHqMe 단일 호출에서 온다 (data=null 이면 폴백값 — 본사 / null plan / 0개).
  • data-surface="hq" HQ scale(comfortable · 15px)은 apps/space/src/app/globals.css 가 선언 (packages/ui 무변경 — 시안 design/tokens.css §HQ MODE 기준).

임퍼소네이션 배너 (① Binding Constraint · impersonation=true 일 때만)

  • 모든 apps/space /admin/* 최상단 고정, --imp-alert·--imp-alert-fg 토큰(@linkmusic/ui) 직접 참조.
  • 세션 잔여 카운트다운 (MM:SS, 1초) — session.refreshExpiresAt(절대 ms) 기준. v1 고정 60분, 자동 refresh 없음.
  • [운영사 백오피스로 돌아가기] → POST /api/auth/impersonation-exit(space) (세션 destroy) → window.close() + 안내.
  • 0:00 도달 → 셸이 만료 화면(danger 배너 + 탭 닫기 안내)으로 전환.

공용 계정 메뉴 (ShellAccountMenu — 점장·본사)

apps/space/src/components/shell-account-menu.tsx — 점장 셸과 본사 셸이 공유하는 계정 드롭다운 atom. 운영사(apps/admin) AccountMenu(OrgAvatar + 이름 트리거 → 이름·이메일 라벨 + 항목)를 미러하되, apps/admin 은 별 앱이라 컴포넌트를 직접 공유할 수 없어 apps/space 안에 같은 시각·동작을 재현한 presentational atom 으로 둔다(atom-grounded — 새 시각 창작 없이 운영사 패턴·기존 토큰).

항목내용
트리거OrgAvatar(이니셜) + 라벨(name 우선, 없으면 email local part). triggerClassName 으로 셸별 헤더 규격 조정
메뉴 항목계정설정(점장 “프로필”→/store/profile · 본사 “설정”→/admin/settings, profileLabel/profileHref 주입) + 로그아웃
로그아웃운영사 AccountMenu 동일 — POST /api/auth/logout(same-origin·best-effort) → router.replace("/login") + refresh()

me 출처 — generic useGetMe()(/api/v1/auth/meMeResponse). 매니저 계정의 email·name 이 여기서 온다. StoreMeResponse·HqMeResponse 는 매장명·본사명만 있고 매니저 email 이 없어 계정 정체성 표시에는 쓰지 않는다(셸의 매장명/본사명 표기는 그쪽 me 가 담당). 각 셸 wrapper (StoreAccountMenu·HqAccountMenu)가 useGetMe 를 호출해 ShellAccountMenu 에 주입한다.

노출 위치

  • 점장: 메인 player 헤더(store-player-client.tsx) + 모든 서브페이지 공용 헤더 (StoreSubHeader 우측 끝, 페이지 액션 right 슬롯 바깥) — 어느 점장 화면에서나 일관 접근. 기존 평면 [프로필] 링크·[로그아웃] 버튼(StoreLogoutButton, 삭제됨)을 대체.
  • 본사: HQTopBar 우측 끝 — 단 실 HQ_MANAGER 세션만. 임퍼소네이션에서는 숨기고 배너의 [돌아가기]가 종료를 담당.

StoreShell — 점장 모드 (apps/space /store)

apps/space/src/app/store/ — STORE_MANAGER 가드 + player 셸(store-player-client.tsx) + 서브페이지(profile·playlist·support·broadcast 등). 서브페이지 공용 헤더는 StoreSubHeader (뒤로 가기 + 제목 + 페이지 액션 right 슬롯 + 우측 끝 ThemeToggle + 공용 계정 메뉴). 계정 액션은 위 공용 계정 메뉴 참고. 제목 h1truncate (overflow-hidden·ellipsis)로 말줄임하고 전체 제목을 title 속성으로 hover 노출한다(SPEC #164 E3 — 긴 제목이 계정 메뉴를 밀거나 가로 스크롤을 유발하던 오버플로 차단). subtitle 도 truncate.

테마 토글(SPEC #166). 본사 HQTopBar 패턴을 미러해 점장 셸에도 공용 ThemeToggle (light/dark/system)을 노출한다 — 메인 player 헤더(store-player-client.tsx)와 모든 서브페이지 공용 헤더(StoreSubHeader)의 계정 메뉴 옆 형제. 두 경로 모두 정확히 1개씩(중복 없이) 노출되며, 임퍼소네이션 sealed 세션에서도 테마 토글은 노출 유지(계정 메뉴만 숨김 대상).

AuthShell

로그인 · 비밀번호 변경 등 pre-auth 화면 전용. @linkmusic/ui export.

구조

사이드바·topbar 없는 단순 centered layout (강제 비밀번호 변경 동안 다른 곳으로 이동하지 못하도록 제한). 헤더(로고 + “운영사 백오피스 · BETA” 라벨 + 우측 옵셔널 headerAction) + center 정렬 main + 옵셔널 footer.

import { AuthShell } from "@linkmusic/ui";
 
<AuthShell headerAction={<ThemeToggle />} footer={<>지원 · 버전</>}>
  <AuthCard>{/* form */}</AuthCard>
</AuthShell>

사용처

  • /login
  • /onboarding/change-password
  • (impersonate-exchange 는 redirect 만, 시각 없음)

위치

packages/ui/src/components/auth-shell.tsx

Constraints

  • 모두 토큰만 사용 — data-surface="ops" Layer 3 ops compact.
  • 운영사 셸: Server Component 가 /auth/me fetch + Client Component (Topbar/Sidebar) 가 interactive.

Roadmap

  • StoreShell 본구현 (apps/space /store — Surface 12 점장 모드 home·즉시방송 등)
  • apps/space HQShell 기능 페이지 22개 · HQSidebar 활성화 · 검색/알림 (후속 SPEC; 계정 메뉴는 완료 — HqAccountMenu/ShellAccountMenu)
  • HQ TopBar ⌘K 검색 활성화 · 사이드바 collapse / pin
  • auth/BFF 공유 패키지 추출 (admin·space 중복 제거 — SPEC #050 §F4)

References

  • SPEC #006 §2-3·§9 · SPEC #012 PR-A/PR-B · SPEC #015 (admin HQShell placeholder — #150 에서 제거) · SPEC #016 (이중 세션·실 HQ_MANAGER 모드) · SPEC #050 (apps/space 본사 모드 셸·점장 placeholder) · SPEC #150 (운영사 앱 OPERATOR 전용화 · admin HQ 셸 제거)
  • handoff 10-surface-ops-backoffice.md §4 · design/hq-shared.jsx (HQShell·HQTopBar·HQSidebar·PlanBadge 시안)
  • linkmusic-frontend-space/packages/ui/src/components/auth-shell.tsx
  • linkmusic-frontend-space/apps/admin/src/app/(protected)/layout.tsx (OPERATOR 전용 ops 셸 가드)
  • linkmusic-frontend-space/apps/admin/src/components/shell/
  • apps/admin/src/app/admin/layout.tsx · apps/admin/src/components/hq-shell/SPEC #150 에서 삭제
  • linkmusic-frontend-space/apps/space/src/app/admin/layout.tsx · apps/space/src/app/store/
  • linkmusic-frontend-space/apps/space/src/components/hq-shell/