개선관리 요구사항과 Google 로그인을 완료

This commit is contained in:
Yun Chan 2026-08-28 16:07:09 +09:00
parent cc0a15b7c6
commit 2a39636163
112 changed files with 10166 additions and 527 deletions

View file

@ -363,6 +363,89 @@ def mask_synthetic_generated_pii(text: str) -> MaskResult:
)
_IDENTITY_SEGMENT_RE = re.compile(r"\s*[·|,/]\s*", re.UNICODE)
_IDENTITY_UUID_RE = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
re.IGNORECASE,
)
_IDENTITY_KO_PARTICLE_LOOKAHEAD = (
r"(?:은|는|이|가|을|를|와|과|의|도|에게|께|랑|하고|님|씨)"
)
def _known_identity_variants(identity: str | None) -> list[str]:
"""Return conservative display-name variants that are safe to role-tokenize."""
raw = str(identity or "").strip()
if not raw or "@" in raw or _IDENTITY_UUID_RE.fullmatch(raw):
return []
first_segment = _IDENTITY_SEGMENT_RE.split(raw, maxsplit=1)[0].strip()
first_segment = re.sub(r"\s*\(가명\)\s*$", "", first_segment).strip()
variants: list[str] = []
for candidate in (raw, first_segment):
if candidate in variants:
continue
letters = re.sub(r"[^A-Za-z가-힣]", "", candidate)
if len(letters) < 2:
continue
variants.append(candidate)
return sorted(variants, key=len, reverse=True)
def mask_role_identities(
text: str,
*,
counselor_identity: str | None = None,
client_identity: str | None = None,
synthetic_generated: bool = False,
) -> MaskResult:
"""Mask known session identities with role tokens, then apply the normal PII gate.
Only identities already owned by the authenticated session are role-tokenized.
Unknown third-party names keep the generic ``[NAME]`` token so the UI cannot
incorrectly present every person as the client.
"""
role_values = (
("ROLE_COUNSELOR", "[COUNSELOR]", counselor_identity),
("ROLE_CLIENT", "[CLIENT]", client_identity),
)
redacted = text
role_entities: list[str] = []
claimed_variants: set[str] = set()
for entity, placeholder, identity in role_values:
for variant in _known_identity_variants(identity):
normalized = variant.casefold()
if normalized in claimed_variants:
continue
pattern = (
rf"(?<![A-Za-z가-힣]){re.escape(variant)}"
rf"(?=$|[^A-Za-z가-힣]|{_IDENTITY_KO_PARTICLE_LOOKAHEAD})"
)
replaced, count = re.subn(
pattern,
placeholder,
redacted,
flags=re.IGNORECASE,
)
if count:
redacted = replaced
role_entities.append(entity)
claimed_variants.add(normalized)
masked = (
mask_synthetic_generated_pii(redacted)
if synthetic_generated
else mask_pii(redacted)
)
return MaskResult(
text_masked=masked.text_masked,
entities=sorted(set(masked.entities + role_entities)),
used_presidio=masked.used_presidio,
used_ko_recognizer=masked.used_ko_recognizer,
)
# ════════════════════════════════════════════════════════════════════════════
# 2. 위기 분류 (입력 — 실제위기 vs 페르소나 연기 구분, R8)
# ════════════════════════════════════════════════════════════════════════════
@ -438,6 +521,8 @@ _MEANS_TERMS = [
"커터", "면도날", "손목 긋", "깊게 그으", "라이터로 지",
]
_DISPLAY_PLACEHOLDER_LABELS = {
"COUNSELOR": "상담자",
"CLIENT": "내담자",
"NAME": "그 이름",
"ORG": "그 기관",
"PHONE": "연락처",
@ -449,7 +534,7 @@ _DISPLAY_PLACEHOLDER_LABELS = {
"ADDR": "그 주소",
}
_DISPLAY_PLACEHOLDER_RE = re.compile(
r"\[(?P<label>NAME|ORG|PHONE|EMAIL|RRN|NUMID|DATE|MONEY|ADDR)\]"
r"\[(?P<label>COUNSELOR|CLIENT|NAME|ORG|PHONE|EMAIL|RRN|NUMID|DATE|MONEY|ADDR)\]"
r"(?P<particle>[은는이가을를와과])?"
)
_DISPLAY_PLACEHOLDER_STREAM_TAIL = 16
@ -625,6 +710,7 @@ __all__ = [
"PiiEntitySpan",
"set_ko_pii_recognizer",
"mask_pii",
"mask_role_identities",
"mask_synthetic_generated_pii",
"CrisisKind",
"CrisisResult",