PII 표시 토큰 누출 보정

This commit is contained in:
Yun Chan 2026-08-09 21:04:29 +09:00
parent 61a41d1f08
commit 7cece90e33
19 changed files with 284 additions and 197 deletions

View file

@ -0,0 +1,30 @@
const DISPLAY_PLACEHOLDERS: Record<string, string> = {
"[NAME]": "익명 내담자",
"[ORG]": "소속 기관",
"[PHONE]": "연락처",
"[EMAIL]": "이메일",
"[RRN]": "주민등록번호",
"[NUMID]": "식별번호",
"[DATE]": "날짜",
"[MONEY]": "금액",
"[ADDR]": "주소",
"[ADDRESS]": "주소",
};
/**
* /API의 privacy-proof ,
* . · .
*/
export function displayPiiSafeText(text: string) {
// 2026-08-09 이전 저장본에서 광범위한 성씨 휴리스틱이 평범한 용언을
// NAME으로 오탐한 두 실관측 문형. 원문은 복원할 수 없으므로 표시층에서만
// 결정적으로 복구하고 저장/API privacy proof는 그대로 둔다.
let next = text.replace(
/\[NAME\]는 건지\s+잘 \[NAME\]는데요/g,
"되는 건지 잘 모르겠는데요",
);
for (const [placeholder, label] of Object.entries(DISPLAY_PLACEHOLDERS)) {
next = next.split(placeholder).join(label);
}
return next;
}

View file

@ -43,6 +43,7 @@ import {
} from "../lib/api";
import { useAuth } from "../lib/auth";
import { formatElapsed, formatTimecode, clamp01 } from "../lib/format";
import { displayPiiSafeText } from "../lib/piiDisplay";
import {
parseVoicePracticeContext,
voicePracticeSearch,
@ -1001,7 +1002,7 @@ export default function Session() {
(detail.turns ?? []).map((turn) => ({
id: nextId(),
speaker: turn.speaker === "client" ? "client" : "learner",
text: turn.text,
text: displayPiiSafeText(turn.text),
turnSeq: turn.turn_seq,
at: secondsBetween(detail.started_at, turn.created_at),
})),

View file

@ -4,6 +4,7 @@ import type {
UserPrepostMeasureItem,
UserPrepostMeasuresResponse,
} from "../../lib/api";
import { displayPiiSafeText } from "../../lib/piiDisplay";
export type PrepostMeasureName = UserPrepostMeasureItem["measure_name"];
export type PrepostTimepoint = UserPrepostMeasureItem["timepoint"];
@ -132,23 +133,8 @@ export function displayEvaluationRetryError(message: string) {
return "AI 평가 재시도를 완료하지 못했습니다. 잠시 뒤 다시 실행해 주세요.";
}
const REVIEW_DISPLAY_PLACEHOLDERS: Record<string, string> = {
"[NAME]": "익명 내담자",
"[ORG]": "소속 기관",
"[PHONE]": "연락처",
"[EMAIL]": "이메일",
"[RRN]": "주민등록번호",
"[ADDRESS]": "주소",
};
export function displayTranscriptText(text: string) {
let next = text;
for (const [placeholder, label] of Object.entries(
REVIEW_DISPLAY_PLACEHOLDERS,
)) {
next = next.split(placeholder).join(label);
}
return next;
return displayPiiSafeText(text);
}
export function shouldPollReviewReady(data: SessionReviewResponse) {