vignette/apps/web/src/lib/piiDisplay.ts

70 lines
2.8 KiB
TypeScript

const DISPLAY_PLACEHOLDERS: Record<string, string> = {
"[NAME]": "익명 내담자",
"[ORG]": "소속 기관",
"[PHONE]": "연락처",
"[EMAIL]": "이메일",
"[RRN]": "주민등록번호",
"[NUMID]": "식별번호",
"[DATE]": "날짜",
"[MONEY]": "금액",
"[ADDR]": "주소",
"[ADDRESS]": "주소",
};
function hasHangulBatchim(value: string) {
for (let index = value.length - 1; index >= 0; index -= 1) {
const code = value.charCodeAt(index);
if (code >= 0xac00 && code <= 0xd7a3) return (code - 0xac00) % 28 !== 0;
}
return false;
}
function replacePlaceholderWithNaturalParticle(
text: string,
placeholder: string,
label: string,
) {
const batchim = hasHangulBatchim(label);
// `이고`를 단일 조사 `이`보다 먼저 처리하지 않으면
// `[NAME]이고`가 `익명 내담자가고`로 깨진다.
let next = text.split(`${placeholder}이고`).join(`${label}이고`);
for (const [variants, particle] of [
[["으로", "로"], batchim ? "으로" : "로"],
[["은", "는"], batchim ? "은" : "는"],
[["이", "가"], batchim ? "이" : "가"],
[["을", "를"], batchim ? "을" : "를"],
[["과", "와"], batchim ? "과" : "와"],
] as const) {
for (const variant of variants) {
next = next.split(`${placeholder}${variant}`).join(`${label}${particle}`);
}
}
return next.split(placeholder).join(label);
}
/**
* 저장/API의 privacy-proof 토큰은 유지하고, 사람이 읽는 일반 대화 표면에서만
* 토큰을 안전한 설명으로 낮춘다. 근거 인용·내보내기에는 사용하지 않는다.
*/
export function displayPiiSafeText(text: string) {
// 2026-08-09 이전 저장본에서 광범위한 성씨 휴리스틱이 평범한 용언을
// NAME으로 오탐한 두 실관측 문형. 원문은 복원할 수 없으므로 표시층에서만
// 결정적으로 복구하고 저장/API privacy proof는 그대로 둔다.
let next = text.replace(
/\[NAME\]는 건지\s+잘 \[NAME\]는데요/g,
"되는 건지 잘 모르겠는데요",
);
// 평가 문장에 이미 역할 명사가 있으면 이름 토큰을 다시 "익명 내담자"로
// 풀지 않는다. 특히 "내담자 [NAME]을 받을 여백"은 저장 마스킹 뒤
// 사람에게 "내담자 반응을 받을 여백"으로 보여야 한다.
next = next
.replace(/내담자\s+\[NAME\](?:을|를)/g, "내담자 반응을")
.replace(/내담자\s+\[NAME\](?:이|가)/g, "내담자가")
.replace(/내담자\s+\[NAME\](?:은|는)/g, "내담자는")
.replace(/내담자\s+\[NAME\]의/g, "내담자의")
.replace(/내담자\s+\[NAME\]/g, "내담자");
for (const [placeholder, label] of Object.entries(DISPLAY_PLACEHOLDERS)) {
next = replacePlaceholderWithNaturalParticle(next, placeholder, label);
}
return next;
}