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 — @linkmusic/ui 공용 구현의 얇은 재-export (SPEC #163). 실제 구현은 packages/ui/src/components/toast-host.tsx 로 승격돼 운영사·본사·점장이 공유한다. 기존 @/components/shell/toast-host import 경로는 안정 유지(동작·시각 불변) |
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 |
| CS 티켓 | /tickets | 후속 SPEC |
| 감사 로그 | /audit/impersonation | 후속 SPEC |
| 통계 (하단) | /stats | 후속 SPEC |
| 계정 (하단) | /users | 후속 SPEC |
| 설정 (하단) | /settings/policies | SPEC #023 도입 — 약관 게시. sub-route 도 active 매칭. lucide Settings 아이콘 (임시 — 시안 부재) |
탑바 (topbar.tsx)
- 로고 + 구분선 + “운영사 백오피스 · BETA” 라벨 (좌)
ThemeToggle(light/dark/system)- 계정 메뉴 (
AccountMenu—email/name) ⌘K 글로벌 검색·장애벨·동기화 펄스— 비기능 placeholder(전역검색·incident 도메인·상태 endpoint 부재)라 “준비 중” toast 만 띄우던 UI 를 정직성 원칙으로 제거(감사 B7). 해당 도메인 도착 시 후속 SPEC 재도입.
임퍼소네이션 모드 분기
- 본사 모드(
/admin/*)는 운영사(protected)셸이 아니라 별도HQShell로 진입한다 (아래 참고).
HQShell — 본사 모드 셸 (admin /admin layout) — SPEC #150 에서 제거
/admin layout)SPEC #150 —
apps/admin의 본사 모드 placeholder 셸(/adminlayout·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/spaceHQShell 절이 유일한 진실이다. (운영사→본사 임퍼소네이션은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/):
| 컴포넌트 | 파일 | 비고 |
|---|---|---|
HQShell | hq-shell.tsx | impersonation prop 으로 배너 분기, TopBar+Sidebar 조립 (client) |
ImpersonationBanner | impersonation-banner.tsx | 빨간 배너 + 60분 카운트다운 + [운영사 백오피스로 돌아가기] (client, 임퍼소네이션만) |
HQTopBar | hq-topbar.tsx | 로고 + 본사명 + PlanBadge(plan) + 산하 매장 수(storeCount) + ThemeToggle + 계정 메뉴(HqAccountMenu, 실 HQ_MANAGER 만) (client) |
HqAccountMenu | hq-account-menu.tsx | useGetMe() → 공용 ShellAccountMenu(profileHref=/admin/settings·label “설정”). impersonation=false 일 때만 렌더 |
PlanBadge | plan-badge.tsx | AI / TRUST 배지 (시안 hq-shared §PlanBadge). plan=null 이면 생략 |
HQSidebar | hq-sidebar.tsx | 본사 기능 항목 — 출시된 항목 active |
임퍼소네이션(
impersonation=true)에서는 HQTopBar 가 계정 메뉴를 숨긴다 — 종료는 배너의 [운영사 백오피스로 돌아가기]가 담당(계정 로그아웃 노출은 sealed 세션 파기 오작동/혼란). 공용 계정 메뉴 상세는 아래 공용 계정 메뉴 참고.
두 진입 모드
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 배너 + 탭 닫기 안내)으로 전환.
공용 계정 메뉴 (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/me → MeResponse). 매니저 계정의 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 + 공용 계정 메뉴). 계정 액션은 위
공용 계정 메뉴 참고. 제목 h1 은 truncate
(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/mefetch + Client Component (Topbar/Sidebar) 가 interactive.
Roadmap
- StoreShell 본구현 (
apps/space/store — Surface 12 점장 모드 home·즉시방송 등) apps/spaceHQShell 기능 페이지 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.tsxlinkmusic-frontend-space/apps/admin/src/app/(protected)/layout.tsx(OPERATOR 전용 ops 셸 가드)linkmusic-frontend-space/apps/admin/src/components/shell/— SPEC #150 에서 삭제apps/admin/src/app/admin/layout.tsx·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/