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단계만 조정할 수 있다.

정책 — 상태(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 (본사 다른 목록과 일관).
  • 에러 매핑: Banner tone="danger" (hq-ticket-list-error) — 401/403/5xx/BACKEND_UNREACHABLE/그 외 4xx 분리.

페이지 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 배너.
    • 우선순위 (hq-ticket-priority-section): LOW/NORMAL/HIGH 세그먼트(URGENT 옵션 없음). 현재 값 재선택은 no-op. useChangeHqTicketPriority → 성공 시 detail invalidate. 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).
  • 에러 매핑:
    • 404 TICKET_NOT_FOUND → “문의를 찾을 수 없습니다.” (타 본사 티켓·미존재 모두 은닉, BE D3).
    • 401/403/5xx/BACKEND_UNREACHABLE/그 외 — list/comment 와 동일 패턴.

사이드바

HQSidebar“고객지원” 항목(/admin/support)이 #086 에서 신규 enabled. icon LifeBuoy (고객지원 메타포 — 운영자 /tickets 빈 상태 아이콘과 일관). 위치: 구독·결제 아래, LLM 자동멘트 위 (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 6종 (#086 + #108)

  • 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.

자세한 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 첨부파일 — 운영자 ticket 도메인 후속과 공유.
  • 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종.