Skip to content

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

CS 리포트 백엔드 기반 (Phase 1 — 접수 파이프라인) ​

앱을 쓰다가 오류를 만난 사용자가 「리포트 보내기」로 신고를 접수하는 기능의 서버 쪽 기반을 추가합니다. 이 PR에는 화면이 없어 사용자가 지금 당장 보는 변화는 없습니다. 화면(리포트 모달·system-admin 탭)이 붙을 수 있도록 접수 API·저장 구조·운영자 처리 API·요청 상관 ID를 먼저 만드는 준비 작업입니다.

배경 ​

요구사항은 docs/internal/cs-report-requirements-2026-09.md(PR #607 머지)에 있습니다. CS 리포트란 사용자가 불편·버그를 겪었을 때 스크린샷·클라이언트 로그·실행 환경을 자동 첨부해 서버로 접수하는 한 건의 신고이고, 운영자(PlatformRole.OPERATOR)가 system-admin 콘솔에서 확인·처리합니다. 외부 작업관리 툴은 쓰지 않으며 cs_reports 테이블이 곧 작업 원장입니다.

지금까지는 이 축에 해당하는 것이 없었습니다. 문의 접수(inquiry.router.ts)는 랜딩의 영업 상담 신청이라 DB 저장 없이 메일만 보내고, 클라이언트 오류는 어디에도 기록되지 않으며, 실패한 API 호출을 서버 로그와 짝지을 상관 키(요청 ID)도 응답에 내려가지 않았습니다.

변경 전

변경 후

개념별 변경 ​

① 민감 정보 리댁션을 공용 모듈로 분리합니다. 리포트에 실리는 로그·설명·컨텍스트는 클라이언트에서 한 번, 서버 저장 직전에 한 번 더 같은 규칙으로 정제해야 합니다(클라이언트 정제를 신뢰하지 않는 원칙). 기존 정제 규칙은 pino 로거(packages/core/src/logger/logger.ts) 안에 있어 브라우저로 가져올 수 없었습니다. 규칙만 packages/core/src/redact/로 분리하고 브라우저 안전 서브패스 @hereby/core/redact(tsup ESM+CJS)로 빌드합니다. 주민등록번호·계좌형 숫자열·토큰 쿼리/JSON·이메일(도메인만 남김) 패턴과 민감 키 목록(isSensitiveKey), 재귀 정제기(redactValue — 깊이 6·문자열 2,000자 상한)를 제공합니다.

② 접수 원장 도메인을 만듭니다. cs_reports(본문·컨텍스트 jsonb·오류 스냅샷·트리아지 제안 자리) · cs_report_attachments(S3 메타) · cs_report_comments(내부 메모/사용자 회신, visibility·authorKind 구분) · cs_report_counters(접수번호 일자별 카운터) 네 테이블을 마이그레이션 1893으로 추가합니다. 상태류 컬럼은 값이 늘어날 수 있는 축이라 enum 타입 대신 varchar를 씁니다(1892 settlementState와 같은 이유). AI 트리아지가 나중에 쓸 fingerprint·triage·triageState·embedding vector(1024) 컬럼을 지금 함께 만듭니다 — 나중에 붙이면 초기 리포트가 에이전트 평가 데이터에서 빠지기 때문입니다. embedding은 TypeORM이 pgvector를 모르므로 raw SQL로만 만들고 엔티티에는 선언하지 않습니다(doc_embeddings 규약).

③ 권한은 전 역할 self scope입니다. 리소스 csReport를 신설하고 csReport.read.self / csReport.write.self(write=접수, delete=취소) 두 권한을 모든 기본 역할(EMPLOYEE 포함)에 시드합니다. 리포트를 못 보내는 역할이 있으면 안 된다는 요구(§6.2)입니다. 조직 관리자용 read.all은 미확정(§11 Q1)이라 만들지 않았고, 운영자 콘솔은 권한 행이 아니라 systemProcedure(OPERATOR)로 가립니다.

④ 접수 API가 신뢰 경계를 지킵니다. csReport.create는 클라이언트가 보낸 값 중 조작 가능한 것(조직·직원·세션 종류)을 버리고 ctx에서 다시 채웁니다. 저장 전 제목·설명·컨텍스트·오류 스냅샷을 ①의 정제기로 한 번 더 통과시키고, 오류 지문(appId + 정규화 route + 오류 code + 스택 상위 3프레임, 줄번호 제거로 빌드가 달라도 동일)을 계산하며, 접수번호 CS-{ENDPOINT_CODE}-YYMMDD-NNNN을 카운터 upsert로 원자 발급합니다. 남용 방어는 계정당 분당 5건·일 50건 rate limit과 같은 제목·화면 60초 중복 접수 시 기존 접수번호 반환(더블 클릭 방어)입니다. 첨부는 prepareUpload가 kind별 상한(스크린샷 1·로그 1·파일 3)과 MIME을 검증해 presigned URL을 발급하고, create가 키 접두({orgId}/cs-reports/)를 재검증합니다. 접수 직후 운영자 메일(CS_REPORT_RECIPIENT_EMAIL)을 트랜잭션 밖 fire-and-forget으로 보냅니다.

⑤ 운영자 API는 전이표가 지킵니다. admin.list/get/transition/comment 네 프로시저를 추가합니다. 상태 전이는 서비스의 허용 전이표(canTransition)가 검증하고, 재오픈(종결 → in_progress)은 사유 코멘트를 요구하며, 종결 전이·사용자 회신은 접수자에게 인앱 알림(NotificationType.SYSTEM)을 보냅니다. 전이 시각(triagedAt/inProgressAt/resolvedAt)을 남겨 처리 시간 지표와 에이전트 평가를 준비합니다.

⑥ 요청 상관 ID를 응답에 내립니다. 지금까지 Fastify req.id는 서버 로그에만 남고 클라이언트는 알 수 없었습니다. 모든 응답에 x-request-id 헤더(CORS exposedHeaders 포함)를 내리고, tRPC errorFormatter의 data.requestId에도 넣습니다. 이후 클라이언트 로그 링버퍼가 실패 호출에 이 키를 담고, 오류 다이얼로그가 「요청 ID」로 표시하며, 운영자·트리아지 에이전트가 이 키로 서버 로그를 곧장 찾습니다. /healthz에는 접수번호에 쓰는 endpointCode를 노출합니다(§6.7.1의 최소 선행 — 자기 등록 프로토콜은 후속).

⑦ 조용히 깨지는 로직에 테스트를 답니다. 리댁션 규칙(주민번호·계좌·토큰·이메일), 지문의 빌드 불변성(줄번호·동적 id 변경에도 동일), 상태 전이표를 Vitest 27건으로 고정합니다.

이 PR에 없는 것 (후속) ​

클라이언트 수집 계층(로그 링버퍼·전역 오류 훅·스크린샷 캡처), 리포트 모달, system-admin 「CS 리포트」 탭, 트리아지 에이전트(Phase 2), 엔드포인트 자기 등록(§6.7), 90일 보존 워커. 요구사항 §10의 Phase 1 항목 1·4·5·10과 2의 서버 절반, 6.5·6.3의 알림·메일까지가 이 PR입니다.