FeaturesHQ (본사)HQ Mode 고객지원 (apps/space /admin/support)

HQ Mode — 본사 CS 티켓 (apps/space /admin/support)

SPEC #086 도입(본사 CS 티켓 백본 — 작성·조회·상세·댓글). SPEC #108 확장(상태 F1·우선순위 F2 변경). 본사(HQ_MANAGER)가 운영사에 문의·요청·이슈를 보고하는 채널. 운영자 ticket 도메인(#027)의 백본·service 를 재사용하되 본사 view 는 별도 endpoint·DTO 로 분리(운영사 assignee 등 과노출 회피, BE D5).

본사 모드(apps/space)의 실 HQ_MANAGER 화면이다. 본 표면 범위는 본사 본인 본사 티켓의 작성· 조회·상세·댓글 + 제한적 상태/우선순위 변경(read+create+comment + F1·F2). 상태 변경은 운영자 처리가 끝난 뒤의 확인·재개에 한정되고(아래 정책), 우선순위는 URGENT 를 제외한 3단계만 조정할 수 있다.

⚠️ 이 페이지(/admin/support)는 본사↔운영사 CS(본사가 운영사에 문의하는 채널)다. 본사가 산하 매장 점장이 올린 CS 를 조회·처리하는 화면은 별도다 → HQ Mode 하위 매장 CS (/admin/store-support, #173). #173 에서 이 목록(listHqTickets 등)에는 storeId IS NULL 가드가 추가돼(D4) 백필 후에도 매장 티켓이 섞이지 않는다(본사발만 노출).

정책 — 상태(F1)·우선순위(F2) 변경 (SPEC #108)

본사 상태/우선순위 변경은 운영자 도메인(#027/#046)의 부분집합으로, BE 정책 검증이 단일 방어선이고 FE 는 허용 옵션만 노출하는 보조 가드다.

  • F1 상태 = 제한적 허용 (운영자 전이표의 부분집합):
    • RESOLVED → CLOSED(확인 종료) · RESOLVED → IN_PROGRESS(재개)
    • CLOSED → IN_PROGRESS(재개)
    • OPEN/IN_PROGRESS 에서는 본사가 직접 상태를 바꿀 수 없다(진행 착수·해결 처리는 운영자 전담). 위반 전이 → 409 TICKET_INVALID_STATUS_TRANSITION.
  • F2 우선순위 = 허용(URGENT 제외): 본사는 LOW/NORMAL/HIGH 만 설정 가능. URGENT 는 운영자 트리아지 전담(작성 폼 #086 정책과 일관). 본사가 URGENT 요청 시 400 HQ_TICKET_PRIORITY_FORBIDDEN. 현재 priority 가 URGENT(운영자 분류)인 티켓도 본사는 3단계로만 조정한다.
  • audit: 변경 시 HQ_TICKET_STATUS_CHANGED·HQ_TICKET_PRIORITY_CHANGED(detail=before→after)를 같은 트랜잭션 hook 으로 기록(hq_audit_log). 우선순위 동일값 멱등 변경은 audit 생략.

Overview

HQ_MANAGER 가 /admin/support 에 진입하면 자기 본사(hqId 토큰 주체 도출)가 작성한 CS 티켓을 시간 역순으로 본다. [새 문의 작성]으로 신규 티켓을 만들고, 행을 클릭해 상세에서 운영사 댓글 스레드를 확인·자기 댓글을 덧붙인다. 운영자 ticket detail 의 INTERNAL 메모는 본사에 노출되지 않는다(BE D5 — 별도 DTO 로 분리).

시안 출처: 본 페이지 전용 시안 부재 — [[feedback_design_only_from_handoff]] 준수. 임시 구현은 본사 /admin/audit(#067) idiom 미러(공용 ListToolbar/ListPagination + client state) + 운영자 /tickets(#027) idiom 미러(상태·우선순위 배지·채팅형 댓글 스레드)로 atom-grounded 합성. 시안 도착 시 정합 교체.

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

apps/space/src/app/admin/support/page.tsx(server 셸) + hq-ticket-list-client.tsx(client). 본사 /admin/audit(#067)·/admin/announcements(#061)와 같이 서버사이드 페이지네이션 + client-query (useListHqTickets) — q·status·priority·page 를 client state(applied**Draft)로 보유. 정렬은 서버 고정(created_at DESC, id ASC — SPEC D7).

  • 헤더: 페이지 제목 “고객지원” + 부제(본사 문의 N건 · 운영사와의 CS 채널) + 우측 [새 문의 작성] 버튼(hq-ticket-create-open, lucide:Plus) → /admin/support/new 네비.
  • 툴바: 공용 ListToolbar — 제목 검색(≤100, OpenAPI 일치) + status select(OPEN/IN_PROGRESS/RESOLVED/CLOSED)
    • priority select(URGENT/HIGH/NORMAL/LOW) + 적용/초기화. draft↔applied 분리(미적용 변경 힌트).
  • 표 5컬럼 (HqTicketListItem):
    • 제목 (font-medium, 좌측 정렬)
    • 상태 배지 (StatusPill 4종): OPEN=“신규”(info) · IN_PROGRESS=“진행 중”(info) · RESOLVED=“해결됨”(success) · CLOSED=“종결”(muted). 전수 강제(Record<HqTicketListItemStatus, …>) — 후속 enum 확장 시 컴파일 에러로 누락 노출.
    • 우선순위 배지 (StatusPill 4종): URGENT=“긴급”(danger) · HIGH=“높음”(warn) · NORMAL=“보통”(info) · LOW=“낮음”(muted). 운영자 /tickets(#027) 의미 매핑과 일관.
    • 댓글 수 (commentCount, font-mono tabular-nums, 우측 정렬)
    • 작성 시각 (createdAt, formatKstDateTime, 우측 정렬)
  • 행 동작: 행 클릭/Enter/Space → /admin/support/{id} 상세. tabIndex=0 + aria-label + focus-visible ring (운영자 /tickets 패턴 — table semantics 보존).
  • 빈 상태: total=0 + 필터 없음 → “문의가 없습니다.” · 필터 있음 → “조건에 맞는 문의가 없습니다.” + [필터 초기화]. total>0 인데 이 페이지 0 행 → “이 페이지에 표시할 문의가 없습니다.” + [첫 페이지로].
  • 페이지네이션: 공용 ListPagination — 총 N건 + 이전/다음. size=20 (본사 다른 목록과 일관).
  • 에러 매핑: 공용 ErrorState ([다시 시도]=refetch, hq-ticket-list-error) — 401/403/5xx/BACKEND_UNREACHABLE/그 외 4xx 분리. 에러 시 툴바·표 숨기고 재시도만(SPEC #154).

페이지 2 — 작성 (/admin/support/new)

apps/space/src/app/admin/support/new/page.tsx(server 셸) + hq-ticket-new-client.tsx(client). [새 문의 작성] 진입점. 별도 페이지로 두는 이유는 본문 5000자가 다이얼로그에 좁아서(운영자 ticket-create-dialog 와 분리). submitter·hqId 는 토큰 주체 도출(BE D6) — 요청 본문에 직접 지정할 수 없다(타 본사 격리).

  • 폼 3 필드 (CreateHqTicketRequest):
    • 제목 input — @maxLength 200 · NotBlank. 빈/blank → submit disabled + “제목을 입력해 주세요.” · 201자 이상 → disabled + “제목은 200자 이하로 입력해 주세요.” · 우측 카운터 N / 200 (tabular-nums).
    • 본문 textarea — @maxLength 5000 · NotBlank. 동일 검증·카운터. rows={12} resize-y. 줄바꿈/공백 보존.
    • 우선순위 select — LOW/NORMAL/HIGH 3종 노출(URGENT 는 운영자 판단, F 후속). 기본 NORMAL. CreateHqTicketRequestPriority enum (BE @nullable — 누락 시 BE D1 NORMAL default).
  • 헤더: 제목 “새 문의 작성” + 부제 + 우측 목록으로.
  • 푸터: [취소](목록으로 push) + [작성](submit · disabled when invalid · “등록 중…” + aria-busy).
  • 성공 흐름: mutation 응답 201 = HqTicketDetailResponserouter.replace(/admin/support/{id}) 로 상세로 이동(뒤로가기 시 작성 폼 재진입 방지 — 중복 등록 차단).
  • 에러 매핑 (mapCreateError): Banner tone="danger" (hq-ticket-new-error) — 401/403/5xx/BACKEND_UNREACHABLE/그 외 4xx 분리.

페이지 3 — 상세 (/admin/support/[id])

apps/space/src/app/admin/support/[id]/page.tsx(server 셸, Next 15 params: Promise) + hq-ticket-detail-client.tsx(client). 상세 조회·댓글 추가는 generated 훅 직접 사용 (useGetHqTicketDetail·useAddHqTicketComment).

  • 헤더: 좌측 [목록으로] + 제목(hq-ticket-detail-title, truncate + title attribute) + 상태/우선순위 배지(StatusPill, list 와 동일 톤 매핑) + 댓글 N건 카운트.
  • 메타 섹션 (hq-ticket-detail-meta, DescCard 3행): 작성자 이메일(font-mono) · 작성 시각 · 수정 시각(KST 24시간제).
  • 처리 섹션 (hq-ticket-detail-actions, SPEC #108 — 운영자 tickets/[id] ActionCard idiom 미러):
    • 상태 변경 (hq-ticket-status-section): 현재 status 의 본사 허용 전이만 버튼 노출 — RESOLVED확인 종료·재개, CLOSED → [재개]. OPEN/IN_PROGRESS 면 버튼 없이 “운영자가 처리 중입니다” 안내 (hq-ticket-status-locked). useChangeHqTicketStatus → 성공 시 detail invalidate + success 토스트 “상태가 변경되었습니다.”(SPEC #163 — 인라인 배너 대신 공용 ToastHost).
    • 우선순위 (hq-ticket-priority-section): LOW/NORMAL/HIGH 세그먼트(URGENT 옵션 없음). 현재 값 재선택은 no-op. useChangeHqTicketPriority → 성공 시 detail invalidate + success 토스트 “우선순위가 변경되었습니다.”(SPEC #163). priority 가 URGENT 인 티켓은 안내 문구 + 3단계 세그먼트.
    • 에러 (hq-ticket-action-error): 409 비허용 전이·400 HQ_TICKET_PRIORITY_FORBIDDEN(안전망)· 404 race·401/403/5xx inline Banner(에러는 인라인 유지).
  • 본문 섹션 (hq-ticket-detail-body): pre-wrap 텍스트 카드 — BE 본문 그대로(공백·줄바꿈 보존).
  • 댓글 스레드 (hq-ticket-detail-comments):
    • 목록: 시간순(BE 가 createdAt asc 보장). 각 댓글에 authorRole 배지(StatusPill):
      • HQ_MANAGER=“내 매장”(info — 본사 본인 매니저)
      • OPERATOR=“운영사”(success — 운영사 응답 도착의 긍정 신호)
      • authorEmail(font-mono) + KST 시각 + body(pre-wrap). 0건이면 “아직 댓글이 없습니다.”.
    • 추가 form (hq-ticket-comment-form): textarea(≤5000, AddHqTicketCommentRequest.body) + [댓글 추가](disabled when invalid · “등록 중…”). 성공 시 getGetHqTicketDetailQueryKey(id) invalidate → 스레드 자동 갱신 + 입력 초기화. 실패 시 인라인 Banner tone="danger" (hq-ticket-comment-error).
  • 첨부파일 섹션(hq-ticket-attachments, 본문 아래 · BE PR #324·#325): 4 채널 공유 TicketAttachments(@linkmusic/ui, surface="ops")에 본사 채널(useUploadTicketAttachmentFile("HQ"))만 주입. [파일 첨부] → 클라 사전 검증(이미지 4종·PDF · 10MB · 5개) → 4MB 이하는 POST /api/v1/hq/tickets/{id}/attachments(multipart 파트 file), 초과는 업로드 티켓(TICKET_ATTACHMENT_HQ → 백엔드 직접 cross-origin) → 성공 시 getGetHqTicketDetailQueryKey(id) invalidate. 10MB 전 구간 업로드 가능(예전 4.5MB 프록시 벽 해소). 실패는 공유 mapTicketAttachmentError(code 우선 + status 폴백 · UPLOAD_NOT_CONFIGURED 전용 문구 포함)로 섹션 인라인 Banner(hq-ticket-attachment-error). 다운로드는 downloadUrl + BFF prefix 의 같은 출처 <a download>(인증 프록시 경로 — public blob URL 아님). 썸네일 없음. 가시성 배지는 운영자 채널 전용이라 이 화면엔 없다(본사 응답에 visibility 필드 자체가 없다 — 존재 은닉).
  • 에러 매핑:
    • 404 TICKET_NOT_FOUND → “문의를 찾을 수 없습니다.” (타 본사 티켓·미존재 모두 은닉, BE D3).
    • 401/403/5xx/BACKEND_UNREACHABLE/그 외 — list/comment 와 동일 패턴.

사이드바

HQSidebar“고객지원” 항목(/admin/support)이 #086 에서 신규 enabled. icon LifeBuoy (고객지원 메타포 — 운영자 /tickets 빈 상태 아이콘과 일관). 위치: 매장 아래, LLM 자동멘트 위 (감사 B3/B6 로 구독·결제 항목 제거 후 매장이 바로 위 — handoff HQ_NAV 순서 보존).

인가

/api/v1/hq/tickets/**hasRole("HQ_MANAGER") 1차 경계 + service verifyHqScope claim↔DB 재검증(hqId 토큰 주체 도출, 요청 파라미터 없음). 미인증 401 · role/소속 불일치 403 PRINCIPAL_SCOPE_MISMATCH. 보안 불변식: 모든 endpoint 에 WHERE ticket.hqId = :hqId 강제 — 타 본사 티켓 id 도 404 로 은닉(존재 흘림 차단, BE D3).

모든 호출은 generated apiFetch 가 BFF catch-all /api/backend/... 경유(토큰 서버 전용).

BE endpoint 8종 (#086 + #108 + 첨부 BE PR #324)

  • GET /api/v1/hq/tickets (listHqTickets) — 본사 본인 티켓 목록. 필터 q/status/priority/page/size.
  • POST /api/v1/hq/tickets (createHqTicket) — 작성. body { title, body, priority? }. 응답 201 HqTicketDetailResponse.
  • GET /api/v1/hq/tickets/{id} (getHqTicketDetail) — 단건 상세 + 댓글 목록. 타 본사·미존재 404.
  • POST /api/v1/hq/tickets/{id}/comments (addHqTicketComment) — 본사 매니저 댓글 추가. 응답 201 HqTicketCommentItem.
  • PATCH /api/v1/hq/tickets/{id}/status (changeHqTicketStatus, #108) — 본사 허용 전이만. 응답 200 HqTicketStatusChangeResponse. 비허용 전이 409 TICKET_INVALID_STATUS_TRANSITION · 타 본사/미존재 404.
  • PATCH /api/v1/hq/tickets/{id}/priority (changeHqTicketPriority, #108) — LOW/NORMAL/HIGH. 응답 200 HqTicketPriorityChangeResponse. URGENT 요청 400 HQ_TICKET_PRIORITY_FORBIDDEN · 타 본사/미존재 404.
  • POST /api/v1/hq/tickets/{id}/attachments (uploadHqTicketAttachment, BE PR #324) — 첨부 업로드(multipart 파트 file + query commentId?). 201 TicketAttachmentUploadResponse. ≤10MB · 티켓당 5개 · MIME 5종 + 매직바이트 · 분당 10회.
  • GET /api/v1/hq/tickets/{id}/attachments/{attId} (downloadHqTicketAttachment, BE PR #324) — 서버 stream 다운로드. 404 TICKET_ATTACHMENT_NOT_FOUND.

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

CS 새 답변 dot (SPEC #119 F4 · D2)

HQSidebar/admin/support(고객지원) 항목에 새 운영자 답변 dot 을 붙인다. getHqSupportUnreadSignal(HqSupportUnreadSignalResponse.latestOperatorReplyAt)을 60초 폴링 (refetchInterval:60s·refetchIntervalInBackground:false — 탭 비활성 중단, focus 복귀 시 즉시) 하고, localStorage lastSeen(lm.support.lastSeen.hq.<hqId>useGetHqMe().id)보다 최신 운영자 REPLY 가 있으면 항목 우측에 primary 도트(hq-nav-dot-support)를 표시한다(boolean dot — 카운트 아님, D2). openOrInProgressCount(미해결 수)는 보조 필드. /admin/support 목록 진입(mount) 시 lastSeen=now 로 갱신해 dot 을 해소한다(D5, markSupportSeenapps/space/src/lib/support-last-seen.ts). localStorage 실패 시 보수적으로 dot 유지(깨지지 않음, D1 리스크 1 — 기기 간 미동기·시크릿모드 초기화는 다시 뜸). dot 시각은 atom-grounded(전용 시안 부재 — sidebar-star 도트 패턴과 동형, design-debt 등재).

Followups

  • F1 본사 상태 변경SPEC #108 완료. 제한적 허용(RESOLVED→CLOSED·IN_PROGRESS, CLOSED→IN_PROGRESS). 위 정책 섹션 참조.
  • F2 본사 우선순위 변경SPEC #108 완료. LOW/NORMAL/HIGH(URGENT 제외).
  • F4 첨부파일BE PR #324 완료(4 채널 동시). 이미지 4종·PDF · 파일당 10MB · 티켓당 5개 · 공유 TicketAttachments 본체. 위 상세 섹션 참조.
  • F5 SLA·고객 알림 — 운영자 ticket 도메인 후속과 공유.
  • F6 본사 CS 페이지 시안 — 전용 시안 도착 시 atom-grounded 임시 레이아웃 정합 교체.

F3(본사 ticket audit 백본 — HQ_TICKET_CREATED·HQ_TICKET_COMMENT_ADDED)은 #101 에서 완료됐고, #108 이 HQ_TICKET_STATUS_CHANGED·HQ_TICKET_PRIORITY_CHANGED 2종을 추가해 본사 audit 은 총 11종.