Impersonation (운영사 → 본사 위장)
SPEC #005 · #013 · #015 정합. ① Binding Constraint — 빨간 배너 항시 + 새 탭 + 감사.
Overview
운영사 OPERATOR 가 본사 (HQ_MANAGER) 모드로 대신 진입하여 IT 비숙련 본사 고객을 대행. 60초 유효 1회용
exchange token + 새 탭 + 빨간 배너 항시 + 모든 backend 호출에 impersonatedBy JWT claim 자동.
도착지는 apps/space /admin/*(진짜 본사 기능 단일 소스) 이다 — apps/admin(운영사 백오피스)이
[전환] 으로 발급만 하고, 다른 origin 인 apps/space 새 탭을 열어 본사 announcements·commercials·
libraries·playlists·stores·support·settings·audit 에 그대로 도달하게 한다. backend 는 토큰 기반 + BFF
프록시라 origin 무관 — 무변경이다.
진입 흐름 (cross-origin handoff)
본사 모드 셸 진입 (apps/space)
교환 성공(ready) 시 새 탭은 apps/space /impersonate-exchange → /admin 으로 router.replace 한다.
임퍼소네이션은 lm_space_impersonation_session 으로 진입한다(실 로그인 lm_space_session 아님).
apps/space middleware 는 /impersonate-exchange·/admin/* 를 bypass 하고, app/admin/layout.tsx
(server component) 가 세션 부재 시 /login 으로 redirect 하는 가드를 단독으로 담당한다(fail-closed).
apps/space/adminlayout 은 이중 세션을 해석한다 —lm_space_impersonation_session(이 흐름, 배너 O)과 실 HQ_MANAGER 의lm_space_session(role=HQ_MANAGER, 배너 X). 둘 다 있으면 임퍼소네이션 우선. 상세는/architecture/auth-flow§3·§4 ·/features/design-system/shells참조.
- 셸 —
apps/space/src/components/hq-shell/:HQShell(배너+TopBar+Sidebar 조립) ·ImpersonationBanner(client) ·HQTopBar(hqName·plan·storeCount) ·HQSidebar(본사 기능 항목). ⚠️@linkmusic/ui가 아니라apps/space소속. (apps/admin의 HQShell 은 실 HQ_MANAGER 전용으로 남고 임퍼소네이션 배너가 없다.) - 배너 데이터 — exchange 응답의
operatorEmail·hqName을 space BFF 가 sealed 세션에 봉인 → layout 이 세션에서 읽어 prop 전달 (페이지마다 backend 호출 0). plan·storeCount 는 sealed 세션에 없어 topbar 는 null/0 폴백으로 렌더(0-backend-call). - 카운트다운 — v1 고정 60분. client 가
session.refreshExpiresAt(절대 ms) 까지 1초 간격 카운트다운(MM:SS). 자동 refresh 없음. 0:00 → 셸이 만료 화면으로 전환. - [운영사 백오피스로 돌아가기]/만료 —
POST /api/auth/impersonation-exit(space) →lm_space_impersonation_sessiondestroy →window.close()시도 + “이 탭을 닫고 원래 탭으로 돌아가세요” 안내(noopener 라 close 실패 가능). - 본사 기능 — 도착 후엔 announcements·commercials·libraries·playlists·stores·support·settings·
audit 등
apps/space의 진짜 본사 기능 page 가 그대로 동작한다(임퍼소네이션 access token 으로).
Spec — Confirm Dialog
packages/ui/Dialog + Banner 사용:
[Warning Icon] 본사 모드로 전환
운영사 → 본사 X (FRANCHISE)
모든 작업은 감사 로그에 기록됩니다.
새 탭에서 본사 모드가 열립니다.
[취소] [전환]Spec — 빨간 배너
임퍼소네이션 전용 빨간 배너 (--imp-alert · --imp-alert-fg 토큰 직접 참조 — Banner atom 의 variant 가 아니라 별도 고정 배너):
[!] 임퍼소네이션 중 — operator@chilloen.com → 본사 X
남은 시간 50:23 [돌아가기]- 모든
apps/space/admin/*페이지 최상단 고정 (app/admin/layout.tsx셸 —HQShell>ImpersonationBanner) - 남은 시간은 클라이언트 카운트다운 (
session.refreshExpiresAt, MM:SS, 1초 간격) - [운영사 백오피스로 돌아가기] 클릭 →
POST /api/auth/impersonation-exit(세션 destroy) →window.close()+ 안내
Backend endpoints (SPEC #005)
| Method | Path | Auth | 동작 |
|---|---|---|---|
| POST | /api/v1/admin/hq/{hqId}/impersonate | OPERATOR-only | ImpersonationToken 발급 (60s 유효, used_at=NULL) |
| POST | /api/v1/auth/impersonate-exchange | public | exchangeToken → 새 JWT (impersonatedBy claim) + refresh 60min |
ImpersonationToken Entity
| 컬럼 | 타입 |
|---|---|
| id | UUID |
| accountId | UUID (원 OPERATOR) |
| targetHqId | UUID (대상 본사) |
| tokenHash | SHA-256 hex |
| expiresAt | Instant (발급 + 60s) |
| usedAt | Instant? (사용 시 채움 — 재사용 차단) |
JWT claims (임퍼소네이션 모드)
{
"sub": "<HQ_MANAGER account id>",
"role": "HQ_MANAGER",
"hqId": "<target hq id>",
"impersonatedBy": "<OPERATOR id>",
"type": "access",
"iat": ...,
"exp": ...
}backend 의 JwtAuthenticationFilter 가 impersonatedBy 가 있으면 감사 로그 (TenantAuditLog) 에 자동
기록 (구현 후속 SPEC, 현재 schema 만).
States & Edge Cases (① Critical)
| 상태 | 처리 |
|---|---|
| FRANCHISE + non-SUSPENDED (ACTIVE·ONBOARDING·UNPAID) | confirm → 새 탭 + 정상 진입 |
| FRANCHISE + SUSPENDED | confirm 안 뜸, 버튼 disabled + “이용 정지된 본사”. 정지된 본사 관리자는 로그인 자체가 차단됨 (SPEC #018) |
| INDEPENDENT (가상) | confirm 안 뜸, disabled + “개인 매장 가상 본사” |
| exchange token 만료 (>60s) | 새 탭에 “토큰 만료” 안내 |
| exchange token 재사용 (used_at != NULL) | 401 |
| 원 OPERATOR 세션 만료 중 exchange | exchange 실패 + 로그인 redirect |
| 임퍼소네이션 세션 만료 (60min) | 빨간 배너 카운트다운 0 → 안내 + 원 탭 복귀 |
Constraints (변경 불가)
- 빨간 배너 항시 고정 — 색조 / 정확한 문구는 ③ 영역, 존재 / 항시성은 ①
- 새 탭 — 원 OPERATOR 세션 유지
- 60초 1회 exchange — 재사용 차단
- 60분 access — 활동 시 rotation (backend 정책)
- 단일 세션만 — 동시 임퍼소네이션 N 개 진입은 v1 불가 (서비스 §13-4)
- FRANCHISE만 활성 — INDEPENDENT / SUSPENDED 차단
Decisions
- 매장 단위 임퍼소네이션 v1 X — 본사 단위만.
- exchange token DB 저장 vs 메모리 — DB 저장 (재기동 안전 + 감사 가능).
- 도착지 = apps/space — 진짜 본사 기능이
apps/space/admin/*에 단일 소스로 있으므로, 도착지를 거기로 둔다. apps/admin 자체 셸로 진입하면 그 기능에 도달 못 한다. 새 탭은NEXT_PUBLIC_SPACE_ORIGIN(apps/admin 의 클라이언트 노출 env)으로 절대 URL 을 조립한다. 미설정 시 [전환] 이 에러를 surface 하고 탭을 열지 않는다. backend 무변경(토큰 + BFF 프록시라 origin 무관). - 새 탭 cookie 분리 —
apps/space의 실 로그인 세션(lm_space_session)과 임퍼소네이션 세션 (lm_space_impersonation_session)을 별도 cookie 이름으로 분리 봉인해 공존(overwrite 회피). admin 원 탭의lm_session은 다른 origin·다른 cookie 라 무관하게 보존된다. fragment(#token=)는 cross-originwindow.open에서도 서버 전송 안 됨 — 읽은 즉시 clearFragment.
Roadmap
- 감사 로그 (
TenantAuditLog) — 모든 impersonatedBy 호출 자동 기록 +/audit/impersonation페이지 - 빨간 배너 카운트다운 + [돌아가기] 흐름 — SPEC #015 에서 완성 (셸·배너·exit·만료 화면)
- HQTopBar plan 배지·산하 매장 수 / HQSidebar 기능 페이지 22개 — 후속 SPEC
- 매장 단위 임퍼소네이션 (v1.1+)
- 활동 자동 갱신(sliding) — 현재 backend refresh TTL 60min 고정 의존
References
- SPEC #005 §2-4 (entity · endpoint)
- handoff
00-design-brief.md§2-2 (빨간 배너 ①) - handoff
10-surface-ops-backoffice.md§3 (임퍼소네이션 ①) - handoff
design/hq-shared.jsx(HQShell · ImpersonationBanner · HQSidebar 시안) linkmusic-frontend-space/apps/admin/src/app/(protected)/hq/impersonation-confirm-dialog.tsx(발급 + space 새 탭 open)linkmusic-frontend-space/apps/space/src/app/impersonate-exchange/page.tsx(도착지 — 토큰 교환)linkmusic-frontend-space/apps/space/src/app/admin/layout.tsx(이중 세션 가드)linkmusic-frontend-space/apps/space/src/components/hq-shell/(HQShell · ImpersonationBanner)linkmusic-frontend-space/apps/space/src/app/api/auth/impersonate-exchange/route.ts·impersonation-exit/route.tslinkmusic-msa-space-was/.../api/admin/ImpersonationController.kt