FeaturesHQ (본사)HQ Mode 하위 매장 CS (apps/space /admin/store-support)

HQ Mode — 하위 매장 CS (apps/space /admin/store-support)

SPEC #173 도입. 본사(HQ_MANAGER)가 산하 매장 점장이 올린 CS(#112, storeId≠null)를 조회하고 처리한다. 본사↔운영사 CS(/admin/support, #086)와는 별도 화면·별도 endpoint(/api/v1/hq/store-tickets/*)다.

배경(과노출 차단 정책 변경): #112 에서 점장 CS 는 hqId=null 로 저장돼 본사 화면에서 격리됐다 (운영사만 양쪽 조회). #173 에서 점장 티켓에 소속 본사 hqId 를 부여(신규는 생성 시, 기존은 V56 백필)해 본사가 하위 매장 CS 를 볼 수 있게 하되, 기존 본사↔운영사 CS 목록엔 storeId IS NULL 가드를 추가해 매장 티켓이 섞이지 않도록 격리를 보존한다(D4).

처리 정책 (D1)

본사는 하위 매장 CS 의 실질 처리 주체다.

  • 조회 + 답변(REPLY) + 상태 변경(표준 전이 전체) 가능: OPEN→IN_PROGRESS→RESOLVED→CLOSED + RESOLVED/CLOSED→IN_PROGRESS(재개). 운영자와 동일한 표준 전이.
  • assign(담당자 배정)·우선순위 조정은 운영사 전담 — 본사 화면엔 해당 컨트롤이 없다(endpoint·DTO 자체 없음).
  • 전 카테고리 처리(D2 — PLAYBACK·BROADCAST·BILLING·ACCOUNT·OTHER 모두). 카테고리 가드는 후속 여지(M2).
  • 운영사는 현행대로 전부 조회·처리·재오픈·assign 유지(변경 없음).

격리 (D4)

/api/v1/hq/store-tickets/**hasRole("HQ_MANAGER") + verifyHqScope + ticket.storeId 의 소속 hqId == 주체 hqId 재검증. 목록은 WHERE storeId≠null AND store.hq_id=:hqId. 상세/댓글/상태는 hqId 격리 원자적 조회·UPDATE. 타 본사·타 매장·미존재 전부 404 TICKET_NOT_FOUND(존재 흘림 차단).

페이지 1 — 목록 (/admin/store-support)

apps/space/src/app/admin/store-support/page.tsx(server 셸) + hq-store-ticket-list-client.tsx(client). /admin/support(#086) 목록 패턴 미러 — 서버사이드 페이지네이션 + useListHqStoreTickets(q·status· priority·category·page·size 를 draft↔applied client state). 정렬 서버 고정(created_at DESC, id ASC).

  • 헤더: 제목 “매장 문의” + 부제(산하 매장 문의 N건 · 점장이 올린 CS). [새 문의 작성] 없음 (본사는 조회·처리만 — 작성은 점장 채널).
  • 툴바: 공용 ListToolbar — 제목 검색(≤100) + status/priority/category select + 적용/초기화.
  • 표 5컬럼 (HqStoreTicketListItem):
    • 매장 (storeName, font-medium) + 제목(부제, truncate)
    • 카테고리 (category nullable → StatusPill, 미분류 시 ”—”). 라벨은 점장 CS(#112)와 동일.
    • 상태 배지: OPEN=“신규” · IN_PROGRESS=“진행 중” · RESOLVED=“해결됨” · CLOSED=“종결”.
    • 우선순위 배지: URGENT=“긴급” · HIGH=“높음” · NORMAL=“보통” · LOW=“낮음”(조회만).
    • 작성 시각 (createdAt, formatKstDateTime, 우측 정렬).
  • 행 동작: 클릭/Enter/Space → /admin/store-support/{id}. tabIndex=0 + aria-label + focus-visible ring.
  • 빈 상태: total=0 + 필터 없음 → “산하 매장 문의가 없습니다.” · 필터 있음 → “조건에 맞는 문의가 없습니다.” + [필터 초기화]. total>0 인데 이 페이지 0 행 → “이 페이지에 표시할 문의가 없습니다.” + [첫 페이지로].
  • 에러 매핑: 공용 ErrorState(hq-store-ticket-list-error, [다시 시도]=refetch) — 401/403/5xx/BACKEND_UNREACHABLE 분리.

페이지 2 — 상세 (/admin/store-support/[id])

apps/space/src/app/admin/store-support/[id]/page.tsx(server 셸) + hq-store-ticket-detail-client.tsx. useGetHqStoreTicketDetail·useAddHqStoreTicketComment·useChangeHqStoreTicketStatus 직접 사용.

  • 헤더: [목록으로] + 매장명 + 제목(hq-store-ticket-detail-title, truncate) + 상태/우선순위/카테고리 배지 + 댓글 N건.
  • 좌측: 본문 카드(pre-wrap) + 채팅 스레드(3-role 좌우 말풍선) + 하단 고정 답변 작성폼.
    • 스레드 시각: STORE_MANAGER=“점장”(좌·surface — 문의 주체) · OPERATOR=“운영사”(좌·success 틴트) · HQ_MANAGER=“본사”(우·primary-soft — 나=처리 주체). BE 가 createdAt asc 보장. 운영자 INTERNAL 메모 제외.
  • 우측 처리 사이드:
    • 상태 변경(hq-store-ticket-status-section, D1 표준 전이): 현재 status 의 허용 전이 버튼만 — OPEN→[처리 시작] · IN_PROGRESS→[해결 처리] · RESOLVED→[종료]·[재개] · CLOSED→[재개]. useChangeHqStoreTicketStatus → 성공 시 detail invalidate + success 토스트 “상태가 변경되었습니다.”. 에러(hq-store-ticket-action-error): 409 비허용 전이·404 race·401/403/5xx inline Banner.
    • 메타: 매장 · 작성 점장 이메일 · 작성/수정 시각(KST).
    • 안내: “담당자 배정·우선순위 조정은 운영사가 담당합니다. 본사는 조회·답변·상태 변경만 가능합니다.”
  • 첨부파일 섹션(hq-store-ticket-attachments, 본문 아래 · BE PR #324·#325): 4 채널 공유 TicketAttachments(@linkmusic/ui, surface="ops")에 하위 매장 채널 (useUploadTicketAttachmentFile("HQ_STORE"))만 주입. 점장이 올린 화면 캡처·PDF 를 같은 목록에서 내려받고(같은 출처 <a download> — 인증 프록시 경로), 본사도 자료를 올릴 수 있다 — 4MB 이하는 POST /api/v1/hq/store-tickets/{id}/attachments(multipart 파트 file), 초과는 업로드 티켓 (TICKET_ATTACHMENT_HQ_STORE → 백엔드 직접) 경로다. 성공 시 getGetHqStoreTicketDetailQueryKey(id) invalidate. 실패는 공유 mapTicketAttachmentError (code 우선 + status 폴백 · UPLOAD_NOT_CONFIGURED 전용 문구 포함) → hq-store-ticket-attachment-error 인라인 Banner. 제약은 이미지 4종·PDF · 파일당 10MB(전 구간 업로드 가능) · 티켓당 5개 (+ BE 매직바이트 검증). 가시성 배지는 운영자 채널 전용이라 이 화면엔 없다.
  • 답변 작성폼(hq-store-ticket-comment-form): textarea(≤5000, AddHqStoreTicketCommentRequest.body) + [답변 등록]. 성공 시 getGetHqStoreTicketDetailQueryKey(id) invalidate + 입력 초기화. 실패 시 inline Banner.
  • 에러 매핑: 404 TICKET_NOT_FOUND → “문의를 찾을 수 없습니다.” (타 본사·타 매장·미존재 은닉).

사이드바 — “매장 문의” 항목 + unread 배지

HQSidebar“매장 문의” 항목(/admin/store-support, icon Inbox)을 “고객지원” 아래 신규 추가. getHqStoreSupportUnreadSignal 60초 폴링(refetchInterval:60s · refetchIntervalInBackground:false — 탭 비활성 중단·focus 복귀 시 즉시):

  • 미해결 수 count 배지(hq-nav-count-store-support): openOrInProgressCount(>0일 때 노출, 99 초과 “99+”).
  • 새 활동 판정: latestStoreActivityAt(점장 신규 티켓·신규 답글 중 최신)이 localStorage lastSeen(lm.support.lastSeen.hq-store.<hqId>)보다 크면 “새 활동”. me·signal 로딩/에러여도 보수적으로 미표시.
    • 신호 중복 방지: 새 활동이면 count 배지가 있을 땐 배지 자체를 primary 톤으로 강조(별도 dot 없음).
    • 독립 dot(hq-nav-dot-store-support)은 count=0 인데 새 활동만 있을 때만 뜬다(예: RESOLVED 티켓에 점장 재답글 — 미해결 수엔 안 잡히지만 확인 필요). 즉 showStoreDot = storeSupportDot && !showStoreCount.
  • 목록 진입(mount) 시 markSupportSeen("hq-store", hqId) 로 lastSeen=now 갱신 → dot 해소.

네비바 storeCount — 임퍼소네이션 실값 (D5)

/admin layout 의 임퍼소네이션(운영사→본사 위장) 세션은 과거 sealed 세션에 storeCount 가 없어 네비바 “산하 매장 N개”를 하드코딩 0으로 렌더했다. #173 D5 에서 임퍼소네이션 access token 이 HQ 스코프임을 이용해 layout 이 /hq/me(backendHqMe)를 1회 직접 조회해 실 storeCount 를 주입한다(BE 변경 0). 임퍼소네이션 토큰은 60분 고정이라 refresh 가 무의미하므로 refresh-aware 로더 대신 직접 호출하고, 실패(401 만료·5xx·네트워크) 시 0 폴백(네비바만 영향·셸 렌더 유지). 만료 세션은 middleware 가 앞단에서 /impersonation-expired 로 차단.

BE endpoint 7종 (#173 + 첨부 BE PR #324)

  • GET /api/v1/hq/store-tickets (listHqStoreTickets) — 목록. 필터 q/status/priority/category/storeId/page/size + 매장명.
  • GET /api/v1/hq/store-tickets/{id} (getHqStoreTicketDetail) — 상세 + REPLY 댓글(3-role). 타 본사·타 매장·미존재 404.
  • POST /api/v1/hq/store-tickets/{id}/comments (addHqStoreTicketComment) — 본사 답변(HQ_MANAGER REPLY, ≤5000).
  • PATCH /api/v1/hq/store-tickets/{id}/status (changeHqStoreTicketStatus) — 표준 전이. 비허용 409.
  • GET /api/v1/hq/store-support/unread-signal (getHqStoreSupportUnreadSignal) — 미해결 수 + 최신 점장 활동 시각.
  • POST /api/v1/hq/store-tickets/{id}/attachments (uploadHqStoreTicketAttachment, BE PR #324) — 첨부 업로드(multipart 파트 file + query commentId?). 201 TicketAttachmentUploadResponse.
  • GET /api/v1/hq/store-tickets/{id}/attachments/{attId} (downloadHqStoreTicketAttachment, BE PR #324) — 서버 stream 다운로드. 404 TICKET_ATTACHMENT_NOT_FOUND.

자세한 endpoint 규약은 Endpoints · DTO 는 DTOs 참조.

시안 출처: 본 페이지 전용 시안 부재 — [[feedback_design_only_from_handoff]] 준수. 본사 /admin/support (#086) idiom 미러(공용 ListToolbar/ListPagination + 2-컬럼 상세 + 채팅형 스레드)로 atom-grounded 합성. 시안 도착 시 정합 교체.

Followups

  • M1 상태 동시 변경 정책 — 본사·운영사 동시 변경 시 현재 last-write + 감사(필요 시 잠금).
  • M2 카테고리 가드 — 결제·계정도 본사 상태 변경 가능(오종료 위험 감수). 후속에 카테고리별 처리 제한 여지.
  • 매장→운영사 직통(본사 비노출) — 매장이 본사에 대한 불만을 올리는 케이스는 현재 전 노출(D2). 후속 플래그 여지.