FeaturesHQ (본사)HQ Detail (/hq/:id)

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/ui atom + admin 관례에 매핑했다. 기능·계약·상태 분기는 불변(시각만 교체).

Overview

운영사(OPERATOR)가 /hq 목록에서 본사 행의 [상세] 를 눌러 진입하는 단건 상세. 헤더(본사명 + status/type/plan pill) + 5탭. 결제(정산)·활동(감사로그) 도메인이 미구축이라 개요·매장·계정 3탭은 실데이터, 결제·활동 2탭은 “준비 중” placeholder.

Spec

<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 }) → 200 HqDetailResponse read-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

상태처리
존재하지 않는 idbackend 404 HQ_NOT_FOUNDnotFound() (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.md Page 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)