HQ Mode — 본사 감사 (apps/space /admin/audit)
SPEC #067 도입(송출 audit 백본) + #068 확장(라이프사이클 audit) + #077 확장(송출 취소 audit) + #101 확장(ticket audit, #086 F3 마감) + #105 확장(매장 정보 편집 audit, #084 F1 마감). 본사(HQ_MANAGER)가
안내방송을 송출·생성·수정·삭제·송출 취소하거나 CS 티켓 생성·댓글·매장 마스터 정보 편집한 행위를 actor·시각·대상 스냅샷으로 append-only 기록하고
read-only 조회한다. dispatch 백본(V23 announcement_dispatch)을 actor 차원으로 확장한 audit
백본(V27 hq_audit_log)의 UI. SPEC #105 부터 액션 총 8종 · 대상 유형 3종(TTS_ANNOUNCEMENT·SUPPORT_TICKET·STORE).
본사 모드(
apps/space)의 실 HQ_MANAGER 화면이다. 본 표면 범위는 본사 audit 조회 + CSV 내보내기 (read-only · 라이프사이클 4종 · CSV export #070) — 점장 즉시방송 audit·운영사 통합 화면은 범위 밖(후속).
Overview
HQ_MANAGER 가 /admin/audit 에 진입하면 자기 본사(hqId 토큰 주체 도출)의 송출 audit 행을 시간
역순으로 본다 — “누가 언제 어떤 안내방송을 어떤 매장에 송출했는지”. impersonation 컨텍스트
(운영자가 본사로 위장해 송출한 경우) 도 같은 행에 표시한다(원본 운영자 + 위장 본사 매니저 둘 다).
시안 출처: workspace parent dir
design_handoff_linkmusic/briefs/requests/hq-audit-page.md(handoff 요청서 — 시안 부재). 임시 구현은 운영사/audit/actions(SPEC #026·#031-D) idiom 미러 + 본사/admin/announcements(#061) idiom 미러로 atom-grounded 합성. 시안 도착 시 정합 교체.
페이지 (/admin/audit)
apps/space/src/app/admin/audit/page.tsx(server 셸) + hq-audit-client.tsx(client). 본사
/admin/announcements(#061)와 같이 서버사이드 페이지네이션 + client-query
(useListHqAuditDispatches) — from·to·action·actorAccountId·q·page 를 client state(applied* ↔
*Draft)로 보유. 정렬은 서버 고정(occurred_at DESC, id DESC).
- 툴바: 공용
ListToolbar— 검색(대상 라벨·상세 부분 검색, ≤100) + 기간 from/to(dateinput) + 액션 select + 적용/초기화. 행위자 입력은 별도 행으로 분리(아래). - 행위자 필터:
actorAccountIdselect(hq-audit-filter-actor, SPEC #069 F9 ✅). mount 시useListHqAuditActors(GET /api/v1/hq/audit/actors) 1회 호출 → 본사 audit 에 등장한 actor distinct 옵션 목록(HqAuditActorOption[]—accountId·email·role(HqAuditActorOptionRole HQ_MANAGER|OPERATOR_IMPERSONATING)·occurrenceCount, occurrenceCount desc·email asc, 최대 200). 옵션 라벨{email} · {roleLabel} · {occurrenceCount}건(예:manager@hq.example.com · 본사 매니저 · 24건,OPERATOR_IMPERSONATING은 “운영자 위장”). 빈 옵션 “전체 행위자”(값 빈 문자열) 기본. select 값은 backend 가 정의한accountId라 추가 검증 불필요(자유 입력 sanitize·UUID_REGEX 제거됨). audit 없는 신규 본사(옵션 0건)는 select disabled + “감사 로그가 없습니다.” 보조 안내. - 행 (
HqAuditItem): 발생 시각(KST) · 액션 배지 · 대상 · 행위자 · 상세.- 발생 시각:
occurredAt(ISO-8601 UTC) →formatKstDateTime(공용@/lib/format) →YYYY. M. D. HH:mm(KST, 24시간제).font-mono tabular-nums. - 액션 배지:
StatusPill15종 (SPEC #068·#077·#101·#105·#106·#108·#078 반복예약·#144 지역) —HQ_ANNOUNCEMENT_DISPATCHED=“송출”(info),HQ_ANNOUNCEMENT_CREATED=“생성”(info),HQ_ANNOUNCEMENT_UPDATED=“수정”(info),HQ_ANNOUNCEMENT_DELETED=“삭제”(warn),HQ_ANNOUNCEMENT_DISPATCH_CANCELED=“송출 취소”(warn — 부정 종착 액션),HQ_DISPATCH_REVOKED=“송출 원격중단”(warn),HQ_DISPATCH_SCHEDULE_CREATED=“반복 예약 등록”(info),HQ_DISPATCH_SCHEDULE_CANCELED=“반복 예약 취소”(warn),HQ_TICKET_CREATED=“티켓 생성”(info),HQ_TICKET_COMMENT_ADDED=“티켓 댓글”(info — CS 영역의 정상 활동),HQ_TICKET_STATUS_CHANGED=“티켓 상태 변경”(info — #108, detailbefore→after),HQ_TICKET_PRIORITY_CHANGED=“티켓 우선순위 변경”(info — #108, detailbefore→after),HQ_STORE_UPDATED=“매장 정보 수정”(info — 정상 운영 활동),HQ_STORE_CREATED=“매장 등록”(info — 정상 운영 활동, #106),HQ_STORE_REGION_UPDATED=“매장 지역 변경”(info — #144, 정상 운영 활동). 전수 강제(Record<HqAuditItemAction, …>)라 후속 액션 추가 시 컴파일 에러로 누락 노출. 액션 select(hq-audit-filter-action)는Object.values(HqAuditItemAction)으로 자동 노출 — enum 확장 시 UI 도 자동 확장(코드 변경 0).apps/admin운영자 본사 감사 뷰(/audit/hq)도 동일 15종 라벨/톤 매핑(audit-hq-client.tsx). - 대상:
targetLabel(스냅샷 — 안내방송 제목 / CS 티켓 제목 / 매장명,targetType에 따라)을 strong 으로. null 이면—.targetType3종(TTS_ANNOUNCEMENT·SUPPORT_TICKET·STORE, SPEC #105). - 행위자:
actorEmail(font-mono).actorRole === OPERATOR_IMPERSONATING이면 보조 줄에↩ impersonatedByEmail+운영자 위장배지(hq-audit-impersonation-{id},title="운영자가 본사로 위장해 송출한 감사 행"). 직접 송출(HQ_MANAGER)이면 보조 줄 미표시. - 상세:
detail.trim()(없으면—). 예:target=ALL·count=3·storeIds=[s1,s2,s3](1024자 cap).
- 발생 시각:
- 빈 상태:
total=0+ 필터 없음 → “아직 기록된 감사 로그가 없습니다.”(empty). 필터 있음 → “조건에 맞는 감사 로그가 없습니다.” + [필터 초기화].total>0인데 이 페이지가 0 행 → “이 페이지에 표시할 감사 로그가 없습니다.” + [첫 페이지로](page=0 reset). - 페이지네이션: 공용
ListPagination— 총 N건 + 이전/다음(경계 disabled).size=20.effectivePage = min(page+1, totalPages)로 out-of-range 보호. - 에러 매핑:
Banner tone="danger"(hq-audit-list-error):- 401
AUTH_UNAUTHENTICATED·NO_SESSION→ “다시 로그인해주세요.” (mutator 가/login으로 풀 리로드). - 403
PRINCIPAL_SCOPE_MISMATCH·AUTH_FORBIDDEN·FORBIDDEN·ACCESS_DENIED→ “이 페이지를 볼 권한이 없습니다.” - 5xx → “서버에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.”
BACKEND_UNREACHABLE(BFF 네트워크 도달 실패) → “서버에 연결할 수 없습니다.”
- 401
CSV 내보내기 (SPEC #070 — F5 ✅)
페이지 헤더 우측 [CSV 내보내기] 버튼(hq-audit-export-csv, lucide:Download 아이콘) — 현재 적용된
필터(from·to·action·actorAccountId·q) 의 전체 매칭 행을 단일 CSV 파일로 다운로드한다. 화면
페이지네이션(20) 과 무관 — 같은 필터 전체 집합을 한 파일로 받는다.
- endpoint:
GET /api/v1/hq/audit/dispatches/export(operationIdexportHqAuditDispatches,text/csv; charset=utf-8, BOM + RFC4180). query 는 list 와 동일하되page·size만 제외. - 컬럼(한국어 헤더, BE-pure):
발생시각(KST)·액션·대상유형·대상라벨·대상ID·행위자 이메일·행위자 역할·임퍼소네이션 운영자 이메일·상세. - 파일명:
Content-Disposition: attachment; filename="hq-audit-{yyyyMMdd-HHmmss}.csv"; filename*=UTF-8''...(RFC5987filename*우선 파싱·실패 시filename·둘 다 실패하면 클라이언트가 KST 타임스탬프로 fallback 생성). - 상한·잘림 신호: backend EXPORT_MAX = 50,000 행. 초과 시 행 잘림 + 응답 헤더
X-Export-Truncated: true→ FE 가Banner tone="warn"(hq-audit-export-truncated) 으로 노출: “감사 행이 상한(50,000건)을 초과해 일부만 다운로드됐습니다. 필터를 좁힌 뒤 다시 시도해주세요.” - CSV injection 방어:
=·+·-·@·탭·CR 시작 셀에'prefix (BEAuditCsvWriter, SPEC #048 미러). - 클라이언트 흐름 (SPEC #070 §D9): generated orval hook 우회. mutator(
apiFetch) 는 모든 응답을res.text()→ JSON 파싱이라 CSV 부적합. 공용 helper@/lib/csv-export.tsdownloadCsvFromBackend(path, query, fallbackFilename)— 브라우저에서 BFF (/api/backend/v1/hq/audit/dispatches/export?...) 로 직접fetch→res.blob()→URL.createObjectURL→<a download={filename}>(앵커 태그) 클릭 →URL.revokeObjectURL. 401 감지 시/login?next={현재경로}풀 리다이렉트(mutator 동작 미러). BFF catch-all proxy 는arrayBuffer()passthrough +content-type/content-disposition헤더 strip 안 함(SPEC #048 검증 그대로). - 버튼 상태: 클릭 →
downloading=true→ 라벨 “다운로드 중…” +aria-busy="true"+ disabled. 성공·잘림 신호 노출·실패 모두 다운로드 종료 시downloading=false복귀. - 에러 매핑:
Banner tone="danger"(hq-audit-export-error) — “CSV 다운로드에 실패했습니다. 잠시 후 다시 시도해주세요.” (401 은 풀 리다이렉트로 분기되므로 배너 미노출). [닫기] 버튼.
사이드바
HQSidebar 의 “감사” 항목(/admin/audit)이 #067 에서 신규 enabled. icon History(lucide,
시계열 append-only 감사 메타포). 위치: LLM 자동멘트 아래, 설정 위 (handoff HQ_NAV 순서 보존 +
운영자 읽기 항목 가까이).
인가
/api/v1/hq/audit/** → hasRole("HQ_MANAGER") 1차 경계 + service verifyHqScope claim↔DB
재검증(hqId 토큰 주체 도출, 요청 파라미터 없음). 미인증 401 · role/소속 불일치 403
PRINCIPAL_SCOPE_MISMATCH. 보안 불변식: search 쿼리에 WHERE hq_id = :hqId 강제 — impersonate
중인 운영자도 본사 토큰이라 같은 hqId 만 조회(자기가 한 액션 노출, 운영사 책임 추적 자연 폐쇄).
모든 호출은 generated apiFetch 가 BFF catch-all /api/backend/... 경유(토큰 서버 전용).
기록 hook (BE 요약)
기록은 HqAnnouncementDispatchService.dispatch 끝, 같은 트랜잭션 내 HqAuditService.record 1줄
(propagation REQUIRED 자연 합치). dispatch row INSERT 와 audit INSERT 가 한 트랜잭션 — audit 실패
시 전체 롤백(원자성, #026 패턴).
- detail:
target={ALL|STORES}·count=N·storeIds=[...앞 10개...](1024자 cap, STORES 일 때만 storeIds). - 0건 송출: dispatch row 0건이어도 dispatch endpoint 호출 자체는 audit (
count=0으로 기록). - actor 해석:
principal.accountId= HQ_MANAGER 계정.impersonatedBy != null이면actor_role=OPERATOR_IMPERSONATING+impersonated_by_operator_id/email. 둘 다 lookup 실패 시 IllegalStateException → 액션 롤백 (audit actor 미상으로 액션 진행 거부).
자세한 dispatch 백본·점장 ack: TTS Announcements 참조.
Followups
- ✅ #067 송출 audit (백본 + 페이지): HQ_MANAGER actor 차원 추적 — 도착.
- ✅ F1 이력 다이얼로그(#065) actor 컬럼 (#071):
announcement_dispatch.audit_idV28 FK +DispatchHistoryItem.{actorEmail,actorRole,impersonatedByEmail}3 필드 +DispatchHistoryDialog5번째 “행위자” 컬럼(impersonate 보조 줄 + “운영자 위장” 배지) — 도착. 본 페이지의 audit 와 이력 다이얼로그의 actor 가 동일hq_audit_logrow 를 가리킨다. V28 이전 row 는 백필 안 함(actor 셀—). F2 announcement create/update/delete audit✅ SPEC #068 도착 —HqAuditAction4종 확장 완료(HQ_ANNOUNCEMENT_CREATED/UPDATED/DELETED추가). 본 페이지의 액션 select 가 자동으로 늘어났음 (Object.values(HqAuditItemAction)).- ✅ #077 송출 취소 audit:
HqAuditAction5종으로 확장(HQ_ANNOUNCEMENT_DISPATCH_CANCELED추가). 본사 행 inline [취소]가 같은 트랜잭션에 본사 audit 1건을 기록(audit INSERT 실패 시 함께 롤백 — 액션-감사 원자성). 본 페이지 액션 select·라벨 record(ACTION_LABELS·ACTION_TONES)도 자동 5종으로 확장. - ✅ #108 CS 티켓 상태/우선순위 변경 audit:
HqAuditAction11종으로 확장(HQ_TICKET_STATUS_CHANGED·HQ_TICKET_PRIORITY_CHANGED추가). 본사 상세 처리 섹션의 상태/우선순위 변경이 같은 트랜잭션에 본사 audit 1건을 기록(detailbefore→after, 우선순위 동일값 멱등 시 생략). 본 페이지 액션 select·라벨 record 도 자동 11종으로 확장. - F3 점장 즉시방송 audit:
store_audit_log별도 백본(STORE_BROADCAST 액션 actor 추적). - F4 운영사 admin 통합 audit 화면: 본사·점장 audit 통합 조회(권한 모델 결정 후 별도 SPEC).
- ✅ F5 CSV export (#070):
exportHqAuditDispatches+ 헤더 [CSV 내보내기] + 잘림 경고 — 도착. - F6 점장 ack 알림·실시간 audit: 송출→재생 사이클의 실시간 audit feed.
- F7 audit 보존 정책: 90일·익명화 등 GDPR-friendly 보존 정책.
- F8 audit 페이지 시안: workspace
design_handoff_linkmusic/briefs/requests/hq-audit-page.mdhandoff 요청서 후속. - ✅ F9 actor select endpoint (#069):
GET /api/v1/hq/audit/actors(listHqAuditActors→HqAuditActorListResponse{items}, 본사 audit 등장 actor distinct·occurrenceCount desc·email asc·최대 200). FEactorAccountId자유 입력 → select 전환 + UUID_REGEX 제거. 같은 SPEC 에서apps/space인라인formatKstDateTime사본을 공용@/lib/formathelper 로 마이그레이션(정책 일관·24시간제, #066 §Followups 마감).