HQ Detail — /hq/:id (본사 상세)
SPEC #020 · #024 정합. handoff 10-surface §3 Page 4 (본사 상세: 헤더 + 5탭).
✅ 시안 정합 (SPEC #031-B, 2026-06-04). 시각 레이아웃은 handoff 시안
design/screens/ops-hq-detail.jsx(헤더 status/type/plan pill + 5탭) 정합 — 개요 탭의 DescCard(라벨-값 2컬럼 카드)·매장/계정 탭의 테이블 카드 헤더 바·결제/활동 placeholder(점선 박스 빈상태)·정지 사유 다이얼로그를 기존@linkmusic/uiatom + admin 관례에 매핑했다. 기능·계약·상태 분기는 불변(시각만 교체).
Overview
운영사(OPERATOR)가 /hq 목록에서 본사 행의 [상세] 를 눌러 진입하는 단건 상세. 헤더(본사명 +
status/type/plan pill) + 5탭. 결제(정산)·활동(감사로그) 도메인이 미구축이라 개요·매장·계정
3탭은 실데이터, 결제·활동 2탭은 “준비 중” placeholder.
Spec
Header
<PageHeader> — 제목 = 본사명, 부제 = <StatusPill> status(활성/온보딩/미납/정지) + type(가맹/개인)
- plan(AI/TRUST). 우측 [목록으로] ghost 링크(
/hq).
5탭
| 탭 | 데이터 | 내용 |
|---|---|---|
| 개요 | 실데이터 | 상단 본사명 인라인 편집 섹션(SPEC #123 — 헤더 우측 [편집] 토글 → input → [저장]/[취소], 운영사 전용) + 본사 정보 라벨-값: 사업자번호 · 타입 · 플랜 · 상태 · 정지 사유(SPEC #024 — status 가 SUSPENDED 이고 suspensionReason 이 있을 때만 “상태” 아래 행 추가, 그 외 생략) · 매장 수 · 결제 기준일 · LLM 자동멘트 · LLM 키워드 · LLM 시간당 최대 · 등록일 |
| 매장 | 실데이터 | store admin-list { hqId: id, size: 100 } 결과 테이블 (매장명 · 타입 · 플랜 · 상태 · 담당자 · 주소 · 최근 온라인). 본사당 매장 전체를 한 번에 확보(페이지네이션 UI 없음 — #044 D8). store-list 컬럼 관용구 재사용 + formatRelativeTime(lib/format) 공유. 빈상태 / 에러 격리 배너. |
| 결제 | placeholder | ”준비 중 — 정산 도메인 후속” 점선 박스 빈상태(시안 §OpsEmpty: 아이콘 칩 + 타이틀 + 본문) (Invoice·BillingKey·ContractPlan 미구축) |
| 활동 | placeholder | ”준비 중 — 감사로그 후속” 점선 박스 빈상태(시안 §OpsEmpty) (TenantAuditLog 미구축) |
| 계정 | 실데이터 | managers 목록 테이블 (이메일 · 이름 · 상태 · 비밀번호(passwordMustChange → “변경 필요” 배지) · 최근 로그인 · 등록일). 빈상태 “등록된 매니저가 없습니다.” |
탭 바는 코드베이스 표준 @linkmusic/ui <Tabs>/<Tab>(handoff ops-shared.jsx §OpsTab —
button group + aria-pressed)을 재사용한다. 라우팅 없이 패널만 전환. 시안 ops-hq-detail
탭 바와 동일하게 매장·계정 탭에 count 배지(실데이터 storeCount·managers.length)를 표시한다.
탭 내부 콘텐츠 레이아웃은 시안 정합 — 개요는 DescCard 2컬럼 그리드(좌측 본사 기본 정보 ·
우측 LLM 설정, 정지 사유는 danger highlight 행), 매장/계정은 헤더 바를 단 테이블 카드,
결제/활동은 점선 박스 빈상태(시안 §OpsEmpty: 아이콘 칩 + 타이틀 + 본문)다.
Implementation
Page (server component)
// apps/admin/src/app/(protected)/hq/[id]/page.tsx (server)
// refresh-aware (frontend.md §3): refreshIfNeeded → fetch → 401 catch → forceRefresh 1회.
// 404 HQ_NOT_FOUND → notFound(). 5xx/네트워크 → errorMessage 배너.
const { id } = await params; // Next 15: params 는 Promise (frontend.md §14)
const detail = await backendGetHqDetail(session.accessToken, id);
// #044: backendListStoresAdmin 시그니처가 (token, params) 로 바뀌고 응답이 envelope
// `{ items, page, size, total }` 가 됐다. 본사 상세 매장 탭은 페이지네이션 UI 없이 전체를
// 보여주므로 `{ hqId: id, size: 100 }` 로 본사 매장 전체를 확보하고 `.items` 만 쓴다(D8).
const { items: stores } = await backendListStoresAdmin(session.accessToken, {
hqId: id,
size: 100,
});
return <HqDetailClient detail={detail} stores={stores} />;- HQ 상세는
backendGetHqDetail(BFF/api/v1/admin/hq/{id}server-side, 토큰 서버 전용). - 매장 탭은 detail 성공 후에만 fetch — 매장 fetch 실패는 매장 탭 배너에만 격리(상세 전체 차단 X).
Client (hq-detail-client.tsx)
'use client' — 탭 state(useState<TabId>) + 패널 렌더. 응답 타입은 generated 스키마
재사용(BackendHqDetailResponse·BackendHqManagerItem = lib/backend alias, 수기 정의 X —
frontend.md §15).
본사명 인라인 편집 (hq-name-edit-form.tsx · SPEC #123 / #085 F1)
개요 탭 상단 “본사명” 섹션 헤더 우측 [편집] 토글 → <HqNameEditForm> 인라인 폼(Field+Input 1필드 +
[저장]/[취소]). 운영사 매장 정보 인라인 편집(#105 hq-store-edit-form.tsx) 패턴을 정확 미러한
atom-grounded 구현(전용 시안 부재 — roadmap/design-debt 등재). 인터뷰 결정:
본사명 변경은 운영사만(본사 자기수정 불가).
- 검증: 비-blank · ≤255자(
UpdateHqRequest제약 미러, frontend.md §8). 변경 없음(trim 후 동일)·blank 이면 [저장] disabled(no-op 차단). useUpdateHq(PATCH /api/v1/admin/hq/{id}, body{ name }) → 200HqDetailResponseread-back. 성공 시router.refresh()(page 가 server-fetch prop 기반이라 client query invalidate 대신 server component 재실행으로 갱신) + read 모드 복귀 + 1회성 success Banner.- 에러 매핑: 400(검증) → “입력값을 확인해 주세요.” · 404
HQ_NOT_FOUND→ “본사를 찾을 수 없습니다.” · 401/403 → 로그인/권한 메시지(hq-name-edit-error).
행 클릭 네비게이션 (hq-list)
hq-list-client 의 행 [상세] 버튼을 <Button asChild> + <Link href="/hq/{id}"> 로 변경.
Link 로 분리해 체크박스·[전환]·[정지/복구] 클릭과 이벤트 충돌 없이 키보드 포커스/접근성 유지.
States & Edge Cases
| 상태 | 처리 |
|---|---|
| 존재하지 않는 id | backend 404 HQ_NOT_FOUND → notFound() (Next 404 페이지) |
| 5xx / 네트워크 (detail) | detail=null + errorMessage 배너 (탭 미렌더) |
| 매장 fetch 실패 | 매장 탭에만 에러 배너 격리 — 다른 탭 정상 |
| 매장 0건 | 매장 탭 “이 본사에 등록된 매장이 없습니다.” 빈상태 |
| managers 0건 | 계정 탭 “등록된 매니저가 없습니다.” 빈상태 |
| optional 필드 omit (businessNumber·plan·billingAnchorDay·llmKeywords·lastLoginAt) | ”—” 표기 |
| 가상(INDEPENDENT) 본사 | 상세 조회 허용 — 개요 표시 (status 전이만 #018 에서 차단) |
Roadmap
- 결제 탭 — 정산 도메인(Invoice·BillingKey·ContractPlan) 도착 시 실데이터
- 활동 탭 — 감사로그(TenantAuditLog) 도착 시 실데이터
- ✅ 본사명 인라인 편집 (SPEC #123 — 운영사 전용). plan·기타 필드 일괄 편집은 후속
- STORE_MANAGER 계정 발급 흐름
References
- SPEC #020 · #024 · #031-B · #123 · handoff
10-surface-ops-backoffice.mdPage 4 · 시안design/screens/ops-hq-detail.jsx linkmusic-frontend-space/apps/admin/src/app/(protected)/hq/[id]/(hq-detail-client.tsx·hq-name-edit-form.tsx)