Skip to content

현재 사용법 (2026-09-10, 6fbaee7f): 불편·기능 신고·첨부·답변 확인 · 기능 마스터. 아래는 작성 당시의 변경 설명이며 현재 전체 동작·재측정 결과를 뜻하지 않습니다.

CS 리포트 Phase 1 후속 — 수집·모달·콘솔·보존·자기 등록 ​

오류를 만난 사용자가 오류 안내창의 「리포트 보내기」 버튼으로 화면 스크린샷·최근 로그·실행 환경이 자동 첨부된 신고를 보낼 수 있게 됩니다. 운영자는 system-admin 콘솔의 「CS 리포트」 탭에서 전 엔드포인트의 접수 건을 모아 보고 상태를 처리합니다. PR #608(백엔드 기반) 위에 쌓이는 변경이며, 요구사항 문서 §10 Phase 1 의 나머지 전부(트리아지 에이전트 제외)입니다.

배경 ​

#608 이 접수 API·저장 구조를 만들었지만 세 가지가 비어 있었습니다: ① 클라이언트에는 리포트에 담을 "최근 로그"라는 것이 존재하지 않았고(콘솔 출력뿐), ② 사용자가 접수할 화면(모달)이 없었으며, ③ 운영자가 접수 건을 볼 화면이 없었습니다. 추가로 다중 엔드포인트 배포에서 콘솔 env 등록을 빼먹은 서버의 리포트가 조용히 보이지 않는 구조 문제(§6.7)가 남아 있었습니다.

변경 전

변경 후

개념별 변경 ​

① 클라이언트 수집 계층 (§5.2·§5.4). SDK 에 CsReportCollector 링버퍼(200건, 성공 API 는 20건, 직렬화 256KB 상한)를 신설했습니다. 항목은 넣기 전에 @hereby/core/redact 로 정제되어 민감 값이 메모리에도 원문으로 남지 않습니다. Alpine 플러그인이 전역 error/unhandledrejection 훅과 화면 이동(nav) 기록을 1회 등록하고, HerebyClient fetch 래퍼가 실패한 tRPC 호출의 경로·상태·코드·x-request-id·소요시간을 기록합니다. createAlpinePlugin({ appId }) 로 앱 식별자를 받고, 스토어에 csReport.{snapshotLogs, envContext, lastError} 함수를 노출합니다. 빌드 버전은 scripts/vite-build-version.mjs 가 9개 앱 vite define 에 __HEREBY_BUILD__(버전+git sha+빌드일)로 주입합니다.

② 공용 리포트 모달 (§4.1·§4.2·§5.1). packages/components 의 CsReport 컴포넌트 + cs-report.html(일반 모달 규격). 스크린샷은 DOM 기반 캡처(html-to-image, 동적 import 라 초기 번들 밖)이고, 캡처 직전 [data-cs-mask] 요소를 검은 블록으로 덮는 임시 스타일을 주입하며, 3초 상한·긴 변 1920px 다운스케일·WebP(폴백 PNG)로 저장합니다. 미리보기에서 드래그하면 검은 사각형이 이미지에 구워지고(burn-in) 원본은 전송되지 않습니다. showError 는 다이얼로그가 화면을 덮기 직전 hereby:cs-report-precapture 를 쏘고, 오류 얼럿 푸터에 「리포트 보내기」가 뜹니다(페이지에 리포트 호스트가 있을 때만). 전송은 presigned 업로드(스크린샷·로그 JSON·사용자 파일 ≤3) 후 csReport.create 이며, 완료 화면이 접수번호를 남깁니다. 9개 앱의 index.html include + appId 배선까지 포함합니다.

③ system-admin 「CS 리포트」 탭 (§7). 콘솔 라우트 /api/cs-reports 가 csReport.admin.list 를 전 엔드포인트 fan-out 하고(부분 실패는 상단 경고), 상세·전이·코멘트는 단일 엔드포인트 호출입니다. 상세 모달은 스크린샷·사용자 파일·코멘트 타임라인(내부/회신/에이전트 구분)과 컨텍스트 표·오류 스냅샷(요청 ID 복사)·클라이언트 로그 뷰어(로그 첨부 JSON 을 받아 시간순 렌더), 상태 전이 폼(중복 원본 지정·회신문 포함)을 담습니다.

④ 첨부 90일 보존 워커 (§6.6). 접수 90일 경과한 첨부의 S3 객체를 지우고 행을 soft delete 하는 in-process 워커입니다. Postgres advisory lock 으로 다중 인스턴스 중 하나만 배치를 잡고, 스토리지 삭제가 실패해도 행은 지워 같은 실패를 반복하지 않습니다(경고 로그). 리포트 본문·코멘트는 남습니다.

⑤ 엔드포인트 자기 등록·발견 (§6.7). 방향을 뒤집었습니다: 콘솔이 아는 것만 보는 pull 이 아니라, 엔드포인트 서버가 부팅 시 POST /api/registry/announce 로 스스로를 알리고 60초 heartbeat 로 상태를 확인합니다. 서버 정체성은 최초 부팅에 생성해 platform_settings(마이그레이션 1894)에 영구 저장하는 instanceId — 같은 DB 의 다중 프로세스는 멱등이고, 다른 서버가 같은 코드를 쓰면 콘솔이 충돌로 잡습니다. 인증은 ${ts}.${code}.${instanceId}.${baseUrl} 정본 문자열의 HMAC-SHA256(±5분 창) — baseUrl 을 서명에 넣어 피싱 엔드포인트 주입을 막습니다. 콘솔은 발견 항목을 로컬 JSON 파일 스토어에 담고(원자적 쓰기), 엔드포인트 탭의 「발견된 엔드포인트」 섹션에서 승인/거절합니다. 승인해야 fan-out 대상이 되고, 재기동 후에도 복원되며, env SYSADMIN_ENDPOINTS 항목이 항상 우선합니다. /healthz 에 registry 상태가 노출됩니다.

요구사항과 다르게 간 것 (명시적 결정) ​

  • 부팅 필수 env(§6.7.1)는 옵트인: ENDPOINT_REGISTRY_REQUIRED=true 인 배포만 부팅 실패. 기존 배포 env 를 한 번에 깨지 않기 위한 단계적 적용이며, 전 배포에 env 가 깔린 뒤 기본값을 뒤집습니다.
  • 레지스트리 저장은 SQLite(Q2-1) 대신 JSON 파일: node:sqlite 가 Node 22 에서 실험 단계라, 단일 인스턴스 콘솔에는 파일 저장으로 시작하고 스토어 인터페이스를 유지한 채 나중에 교체합니다.
  • 가림 도구의 키보드 조작(§9 접근성)은 미구현 — 포인터 드래그만. 후속 과제로 남깁니다.
  • 상시 진입점(헤더 도움말·단축키)·내 리포트 목록·직원앱 실패 초안은 Phase 2 범위라 이 PR 에 없습니다.