FeaturesHQ (본사)Impersonation (운영사 → 본사 위장)

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 /admin layout 은 이중 세션을 해석한다 — 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_session destroy → 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)

MethodPathAuth동작
POST/api/v1/admin/hq/{hqId}/impersonateOPERATOR-onlyImpersonationToken 발급 (60s 유효, used_at=NULL)
POST/api/v1/auth/impersonate-exchangepublicexchangeToken → 새 JWT (impersonatedBy claim) + refresh 60min

ImpersonationToken Entity

컬럼타입
idUUID
accountIdUUID (원 OPERATOR)
targetHqIdUUID (대상 본사)
tokenHashSHA-256 hex
expiresAtInstant (발급 + 60s)
usedAtInstant? (사용 시 채움 — 재사용 차단)

JWT claims (임퍼소네이션 모드)

{
  "sub": "<HQ_MANAGER account id>",
  "role": "HQ_MANAGER",
  "hqId": "<target hq id>",
  "impersonatedBy": "<OPERATOR id>",
  "type": "access",
  "iat": ...,
  "exp": ...
}

backend 의 JwtAuthenticationFilterimpersonatedBy 가 있으면 감사 로그 (TenantAuditLog) 에 자동 기록 (구현 후속 SPEC, 현재 schema 만).

States & Edge Cases (① Critical)

상태처리
FRANCHISE + non-SUSPENDED (ACTIVE·ONBOARDING·UNPAID)confirm → 새 탭 + 정상 진입
FRANCHISE + SUSPENDEDconfirm 안 뜸, 버튼 disabled + “이용 정지된 본사”. 정지된 본사 관리자는 로그인 자체가 차단됨 (SPEC #018)
INDEPENDENT (가상)confirm 안 뜸, disabled + “개인 매장 가상 본사”
exchange token 만료 (>60s)새 탭에 “토큰 만료” 안내
exchange token 재사용 (used_at != NULL)401
원 OPERATOR 세션 만료 중 exchangeexchange 실패 + 로그인 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-origin window.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.ts
  • linkmusic-msa-space-was/.../api/admin/ImpersonationController.kt