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) - 카테고리 (
categorynullable →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 메모 제외.
- 스레드 시각: STORE_MANAGER=“점장”(좌·surface — 문의 주체) · OPERATOR=“운영사”(좌·success 틴트) ·
HQ_MANAGER=“본사”(우·primary-soft — 나=처리 주체). BE 가
- 우측 처리 사이드:
- 상태 변경(
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 inlineBanner. - 메타: 매장 · 작성 점장 이메일 · 작성/수정 시각(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 + 입력 초기화. 실패 시 inlineBanner. - 에러 매핑: 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+ querycommentId?). 201TicketAttachmentUploadResponse.GET /api/v1/hq/store-tickets/{id}/attachments/{attId}(downloadHqStoreTicketAttachment, BE PR #324) — 서버 stream 다운로드. 404TICKET_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). 후속 플래그 여지.