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/):
| 컴포넌트 | 파일 |
|---|---|
Topbar | topbar.tsx |
Sidebar | sidebar.tsx |
AccountMenu | account-menu.tsx |
ThemeToggle | theme-toggle.tsx |
ToastHostProvider | toast-host.tsx |
OpsStepper | ops-stepper.tsx (등록 화면 stepper) |
PageHeader | page-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 |
| 본사 ⭐ | /hq | SPEC #013 도입 |
| 매장 | /stores | SPEC #019 도입 (목록). sub-route /stores/new(등록) 도 active 매칭 |
| 음원 | /music | 후속 SPEC |
| 라이브러리 | /libraries | 후속 SPEC |
| 협약 ⭐ | /contracts | 후속 SPEC |
| 정산 | /billing | 후속 SPEC |
| CS 티켓 | /tickets | 후속 SPEC |
| 감사 로그 | /audit/impersonation | 후속 SPEC |
| 통계 (하단) | /stats | 후속 SPEC |
| 계정 (하단) | /users | 후속 SPEC |
| 설정 (하단) | /settings/policies | SPEC #023 도입 — 약관 게시. sub-route 도 active 매칭. lucide Settings 아이콘 (임시 — 시안 부재) |
탑바 (topbar.tsx)
- 로고 + 구분선 + “운영사 백오피스 · BETA” 라벨 (좌)
- ⌘K 글로벌 검색 (placeholder — 클릭 시 “준비 중” toast)
ThemeToggle(light/dark/system)- 장애벨 (
incidentCountcount badge — incident 도메인 미존재라 기본 0, 배지 숨김. SPEC #017 §3) 동기화 펄스— 가짜 “12초 전” 펄스는 backend 상태 endpoint 가 없어 제거(SPEC #017)- 계정 메뉴 (
AccountMenu—email/name)
임퍼소네이션 모드 분기
- 본사 모드(
/admin/*)는 운영사(protected)셸이 아니라 별도HQShell로 진입한다 (아래 참고).
HQShell — 본사 모드 셸 (admin /admin layout · 실 HQ_MANAGER 전용)
이 절은
apps/admin의 본사 모드 셸이다. 운영사→본사 임퍼소네이션의 도착지가apps/space/admin/*(진짜 본사 기능)로 이전되면서, admin HQShell 은 실 HQ_MANAGER 직접 로그인 전용으로 단순화됐다 — 임퍼소네이션 배너·카운트다운·만료 화면이 없다. 임퍼소네이션 표면은 아래apps/spaceHQShell 절 참고.
본사 모드 (Surface 11) 전역 셸 — HQTopBar + HQSidebar + content area. OpsShell 과 동일하게 단일
ui 컴포넌트가 아니라 apps/admin 소속이며, apps/admin/src/app/admin/layout.tsx(server
component) 가 세션을 읽어 다음 컴포넌트들을 조립한다 (apps/admin/src/components/hq-shell/):
| 컴포넌트 | 파일 | 비고 |
|---|---|---|
HQShell | hq-shell.tsx | TopBar+Sidebar 조립 (server component · 배너 없음) |
HQTopBar | hq-topbar.tsx | 로고 + 본사명 + ThemeToggle (plan·storeCount 후속) |
HQSidebar | hq-sidebar.tsx | 10항목 — 대시보드만 active, 나머지 disabled(“준비 중”) |
ImpersonationBanner(impersonation-banner.tsx)는 apps/admin 에서 제거됐다 —apps/space로 이전.
진입·가드
/admin/*는middleware.ts가 bypass 하고admin/layout.tsx가 유일한 가드:- 세션 없음(
lm_session) →/login. lm_sessionrole=HQ_MANAGER → 실 HQ_MANAGER 모드 (refresh-aware,passwordMustChange→ change-password, 약관/개인정보 재동의 → reagree).lm_sessionrole=OPERATOR →redirect("/")(교차 리디렉션).
- 세션 없음(
- hqName 은
/auth/me응답. 세션 만료는 layout 의 refresh-aware(loadMeRefreshAware)가 처리한다 (클라이언트 카운트다운/만료 화면 없음).
사이드바 메뉴 (hq-sidebar.tsx HQ_NAV 단일 소스)
대시보드 · 안내방송 · CM송 · 플레이리스트 · 라이브러리 · 매장 · 구독·결제 · 고객지원 · LLM 자동멘트 · 설정.
대시보드(/admin) 만 링크 활성, 나머지 9 항목은 aria-disabled + “준비 중” (후속 SPEC).
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/):
| 컴포넌트 | 파일 | 비고 |
|---|---|---|
HQShell | hq-shell.tsx | impersonation prop 으로 배너 분기, TopBar+Sidebar 조립 (client) |
ImpersonationBanner | impersonation-banner.tsx | 빨간 배너 + 60분 카운트다운 + [운영사 백오피스로 돌아가기] (client, 임퍼소네이션만) |
HQTopBar | hq-topbar.tsx | 로고 + 본사명 + PlanBadge(plan) + 산하 매장 수(storeCount) + ThemeToggle (client) |
PlanBadge | plan-badge.tsx | AI / TRUST 배지 (시안 hq-shared §PlanBadge). plan=null 이면 생략 |
HQSidebar | hq-sidebar.tsx | 본사 기능 항목 — 출시된 항목 active |
두 진입 모드
HQShell 은 impersonation: boolean prop 으로 두 모드를 분기한다. layout 이 어느 세션으로 진입했는지에
따라 prop 을 정한다 (우선순위: 임퍼소네이션 우선).
| 모드 | 세션 cookie | impersonation | 배너 | hqName 소스 | plan·storeCount | 세션 관리 |
|---|---|---|---|---|---|---|
| 임퍼소네이션 | lm_space_impersonation_session | true | O (빨강 + 60분 카운트다운) | sealed 세션 | null·0 폴백(0-backend-call) | v1 고정, refresh 없음 |
| 실 HQ_MANAGER | lm_space_session role=HQ_MANAGER | false | X | HqMeResponse.name | getHqMe | refresh-aware |
진입·가드
apps/spacemiddleware.ts는/impersonate-exchange(임퍼소네이션 새 탭 도착지)·/admin/*를 bypass 한다 —/admin은 두 세션 중 하나로 진입할 수 있어 단일 cookie presence 검사로는 임퍼소네이션 진입이 부당하게 튕긴다. layout 이 단독 가드(두 세션 모두 검사·둘 다 없으면 server-side/login, fail-closed):lm_space_impersonation_session존재 → 임퍼소네이션 모드 (배너 O · plan/storeCount 폴백 · pmc/재동의 체크 불필요).lm_space_sessionrole=HQ_MANAGER →loadMeRefreshAware(pmc 가드) →loadHqMeRefreshAware→GET /api/v1/hq/me(HqMeResponse) → HQShell 에hqName·plan·storeCount주입.- role=STORE_MANAGER →
/store, role≠HQ_MANAGER(OPERATOR·누락) →/login(fail-closed). - 세션 없음 →
/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 배너 + 탭 닫기 안내)으로 전환.
StoreShell — 점장 모드 (apps/space /store · placeholder · SPEC #050)
apps/space/src/app/store/ — STORE_MANAGER 가드 + placeholder. 셸·화면(home·즉시방송 등)은 후속.
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/mefetch + Client Component (Topbar/Sidebar) 가 interactive.
Roadmap
- StoreShell 본구현 (
apps/space/store — Surface 12 점장 모드 home·즉시방송 등) apps/spaceHQShell 기능 페이지 22개 · HQSidebar 활성화 · 검색/알림/계정 메뉴 (후속 SPEC)- 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) · SPEC #016 (이중 세션·실 HQ_MANAGER 모드) · SPEC #050 (apps/space 본사 모드 셸·점장 placeholder)
- handoff
10-surface-ops-backoffice.md§4 ·design/hq-shared.jsx(HQShell·HQTopBar·HQSidebar·PlanBadge 시안) linkmusic-frontend-space/packages/ui/src/components/auth-shell.tsxlinkmusic-frontend-space/apps/admin/src/app/(protected)/layout.tsxlinkmusic-frontend-space/apps/admin/src/components/shell/linkmusic-frontend-space/apps/admin/src/app/admin/layout.tsxlinkmusic-frontend-space/apps/admin/src/components/hq-shell/linkmusic-frontend-space/apps/space/src/app/admin/layout.tsx·apps/space/src/app/store/linkmusic-frontend-space/apps/space/src/components/hq-shell/