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에서는 본사가 직접 상태를 바꿀 수 없다(진행 착수·해결 처리는 운영자 전담). 위반 전이 → 409TICKET_INVALID_STATUS_TRANSITION.
- F2 우선순위 = 허용(URGENT 제외): 본사는
LOW/NORMAL/HIGH만 설정 가능.URGENT는 운영자 트리아지 전담(작성 폼 #086 정책과 일관). 본사가 URGENT 요청 시 400HQ_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 분리(미적용 변경 힌트).
- priority select(
- 표 5컬럼 (
HqTicketListItem):- 제목 (font-medium, 좌측 정렬)
- 상태 배지 (
StatusPill4종):OPEN=“신규”(info) ·IN_PROGRESS=“진행 중”(info) ·RESOLVED=“해결됨”(success) ·CLOSED=“종결”(muted). 전수 강제(Record<HqTicketListItemStatus, …>) — 후속 enum 확장 시 컴파일 에러로 누락 노출. - 우선순위 배지 (
StatusPill4종):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.
CreateHqTicketRequestPriorityenum (BE@nullable— 누락 시 BE D1 NORMAL default).
- 제목 input —
- 헤더: 제목 “새 문의 작성” + 부제 + 우측 목록으로.
- 푸터: [취소](목록으로 push) + [작성](submit · disabled when invalid · “등록 중…” +
aria-busy). - 성공 흐름: mutation 응답 201 =
HqTicketDetailResponse→router.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 비허용 전이·400HQ_TICKET_PRIORITY_FORBIDDEN(안전망)· 404 race·401/403/5xx inlineBanner.
- 상태 변경 (
- 본문 섹션 (
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).
- 목록: 시간순(BE 가
- 에러 매핑:
- 404
TICKET_NOT_FOUND→ “문의를 찾을 수 없습니다.” (타 본사 티켓·미존재 모두 은닉, BE D3). - 401/403/5xx/
BACKEND_UNREACHABLE/그 외 — list/comment 와 동일 패턴.
- 404
사이드바
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? }. 응답 201HqTicketDetailResponse.GET /api/v1/hq/tickets/{id}(getHqTicketDetail) — 단건 상세 + 댓글 목록. 타 본사·미존재 404.POST /api/v1/hq/tickets/{id}/comments(addHqTicketComment) — 본사 매니저 댓글 추가. 응답 201HqTicketCommentItem.PATCH /api/v1/hq/tickets/{id}/status(changeHqTicketStatus, #108) — 본사 허용 전이만. 응답 200HqTicketStatusChangeResponse. 비허용 전이 409TICKET_INVALID_STATUS_TRANSITION· 타 본사/미존재 404.PATCH /api/v1/hq/tickets/{id}/priority(changeHqTicketPriority, #108) — LOW/NORMAL/HIGH. 응답 200HqTicketPriorityChangeResponse. URGENT 요청 400HQ_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, markSupportSeen — apps/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_CHANGED2종을 추가해 본사 audit 은 총 11종.