Spaces:
Runtime error
Runtime error
Download validators.py from VioletWizard/Talk-Trace-AI: direct link, hf CLI and curl.
- Browser
- Download file 9.16 kB
-
https://huggingface.co/spaces/VioletWizard/Talk-Trace-AI/resolve/main/validators.py
- Command line
-
hf download hf://spaces/VioletWizard/Talk-Trace-AI/validators.py
-
curl -L -o validators.py https://huggingface.co/spaces/VioletWizard/Talk-Trace-AI/resolve/main/validators.py
9.16 kB
| import re | |
| # ══════════════════════════════════════════════ | |
| # [1] 에러 코드 상수 | |
| # ══════════════════════════════════════════════ | |
| class ValidationError: | |
| EMPTY = "VALIDATION_EMPTY" | |
| TOO_LONG = "VALIDATION_TOO_LONG" | |
| INVALID_CHARS = "VALIDATION_INVALID_CHARS" | |
| FORBIDDEN = "VALIDATION_FORBIDDEN" | |
| # ── 에러 코드 → 사용자 메시지 매핑 ── | |
| VALIDATION_MESSAGES = { | |
| ValidationError.EMPTY: "{field}을(를) 입력해 주세요.", | |
| ValidationError.TOO_LONG: "{field}은(는) {max_len}자 이하여야 합니다.", | |
| ValidationError.INVALID_CHARS: "{field}에 허용되지 않은 문자가 포함되어 있습니다.", | |
| ValidationError.FORBIDDEN: "{field}에 사용할 수 없는 단어가 포함되어 있습니다.", | |
| } | |
| # ══════════════════════════════════════════════ | |
| # [2] 텍스트 정규화 유틸리티 | |
| # ══════════════════════════════════════════════ | |
| def normalize_user_text(raw_text, max_len=500): | |
| """앞뒤 공백 제거 후 max_len까지 자릅니다.""" | |
| text = (raw_text or "").strip() | |
| return text[:max_len] if text else "" | |
| def normalize_room_name(raw_text, max_len=60): | |
| """방 이름 전용: 연속 공백을 단일 공백으로 압축합니다.""" | |
| text = normalize_user_text(raw_text, max_len=max_len) | |
| return re.sub(r"\s+", " ", text).strip() if text else "" | |
| # ══════════════════════════════════════════════ | |
| # [3] IP 마스킹 | |
| # ══════════════════════════════════════════════ | |
| def mask_ip_for_teacher(ip_text): | |
| """ | |
| 교사 화면에 표시할 IP를 마스킹합니다. | |
| IPv4 예: 1.XXX.XXX.4 | |
| IPv6 예: 2001:db8:XXXX:XXXX:1 | |
| """ | |
| ip = str(ip_text or "").strip() | |
| if not ip: | |
| return "" | |
| ipv4_parts = ip.split(".") | |
| if len(ipv4_parts) == 4 and all(part.isdigit() for part in ipv4_parts): | |
| return f"{ipv4_parts[0]}.XXX.XXX.{ipv4_parts[3]}" | |
| if ":" in ip: | |
| ipv6_parts = ip.split(":") | |
| if len(ipv6_parts) >= 3: | |
| return f"{ipv6_parts[0]}:{ipv6_parts[1]}:XXXX:XXXX:{ipv6_parts[-1]}" | |
| return ip | |
| # ══════════════════════════════════════════════ | |
| # [4] author_role fallback | |
| # ══════════════════════════════════════════════ | |
| def with_fallback_author_role(df): | |
| """ | |
| author_role 컬럼이 없거나 비어 있는 레거시 데이터를 | |
| student_name 기반으로 역할을 추정해 채웁니다. | |
| - '교사' / '선생님' 포함 → '교사' | |
| - 그 외 → '학생' | |
| """ | |
| if df.empty: | |
| return df | |
| fixed = df.copy() | |
| if "author_role" not in fixed.columns: | |
| fixed["author_role"] = "학생" | |
| return fixed | |
| fixed["author_role"] = fixed["author_role"].fillna("").astype(str).str.strip() | |
| teacher_name_hint = fixed["student_name"].fillna("").astype(str).str.contains( | |
| "교사|선생님", regex=True | |
| ) | |
| fixed.loc[(fixed["author_role"] == "") & teacher_name_hint, "author_role"] = "교사" | |
| fixed.loc[fixed["author_role"] == "", "author_role"] = "학생" | |
| return fixed | |
| # ══════════════════════════════════════════════ | |
| # [5] 허용 문자 패턴 | |
| # ══════════════════════════════════════════════ | |
| # 일반 텍스트 필드: 한글, 영문, 숫자, 기본 특수문자 허용 | |
| ALLOWED_TEXT_PATTERN = re.compile( | |
| r"^[0-9A-Za-z가-힣ㄱ-ㅎㅏ-ㅣ \_.,!?():;@#&/\-]*$" | |
| ) | |
| # 입장 암호: 일반 텍스트보다 넓은 특수문자 허용 | |
| ALLOWED_ENTRY_CODE_PATTERN = re.compile( | |
| r"^[0-9A-Za-z가-힣ㄱ-ㅎㅏ-ㅣ \_!@#$%^&*()+=.?/:;\-]*$" | |
| ) | |
| # 교사 ID/PW: 영문·숫자·일부 특수문자만 허용 (공백 불허) | |
| ALLOWED_CREDENTIAL_PATTERN = re.compile( | |
| r"^[0-9A-Za-z\_.@\-]+$" | |
| ) | |
| # ══════════════════════════════════════════════ | |
| # [6] 금지어 검사 | |
| # ══════════════════════════════════════════════ | |
| def _contains_forbidden_word(text, forbidden_words=None): | |
| """forbidden_words 중 하나라도 포함되면 True를 반환합니다.""" | |
| words = forbidden_words or [] | |
| lowered = str(text or "").lower() | |
| return any(str(word).lower() in lowered for word in words) | |
| # ══════════════════════════════════════════════ | |
| # [7] 공통 검증 함수 | |
| # ══════════════════════════════════════════════ | |
| def _validate_text_field( | |
| raw_text, | |
| *, | |
| field_name, | |
| max_len, | |
| allow_empty=False, | |
| forbidden_words=None, | |
| allowed_pattern=ALLOWED_TEXT_PATTERN, | |
| ): | |
| """ | |
| 텍스트 필드 공통 검증 로직. | |
| Returns | |
| ------- | |
| (ok: bool, safe_text: str, error_code: str | None, error_message: str | None) | |
| """ | |
| stripped = (raw_text or "").strip() | |
| if not stripped: | |
| if allow_empty: | |
| return True, "", None, None | |
| return ( | |
| False, "", | |
| ValidationError.EMPTY, | |
| VALIDATION_MESSAGES[ValidationError.EMPTY].format(field=field_name), | |
| ) | |
| if len(stripped) > max_len: | |
| return ( | |
| False, "", | |
| ValidationError.TOO_LONG, | |
| VALIDATION_MESSAGES[ValidationError.TOO_LONG].format( | |
| field=field_name, max_len=max_len | |
| ), | |
| ) | |
| normalized = stripped | |
| if allowed_pattern and not allowed_pattern.match(normalized): | |
| return ( | |
| False, "", | |
| ValidationError.INVALID_CHARS, | |
| VALIDATION_MESSAGES[ValidationError.INVALID_CHARS].format(field=field_name), | |
| ) | |
| if _contains_forbidden_word(normalized, forbidden_words=forbidden_words): | |
| return ( | |
| False, "", | |
| ValidationError.FORBIDDEN, | |
| VALIDATION_MESSAGES[ValidationError.FORBIDDEN].format(field=field_name), | |
| ) | |
| return True, normalized, None, None | |
| # ══════════════════════════════════════════════ | |
| # [8] 필드별 검증 함수 | |
| # ══════════════════════════════════════════════ | |
| def validate_room_name(raw_text, max_len=60): | |
| """ | |
| 방 이름 검증. | |
| - 연속 공백 압축 후 검사 | |
| - '관리자', '운영자' 금지 | |
| """ | |
| collapsed = normalize_room_name(raw_text, max_len=max_len) | |
| return _validate_text_field( | |
| collapsed, | |
| field_name="방 이름", | |
| max_len=max_len, | |
| allow_empty=False, | |
| forbidden_words=["관리자", "운영자"], | |
| ) | |
| def validate_student_name(raw_text, max_len=30): | |
| """ | |
| 학생 이름/학번 검증. | |
| - '관리자', '운영자', 'admin' 금지 | |
| """ | |
| return _validate_text_field( | |
| raw_text, | |
| field_name="학생 이름/학번", | |
| max_len=max_len, | |
| allow_empty=False, | |
| forbidden_words=["관리자", "운영자", "admin"], | |
| ) | |
| def validate_opinion_content(raw_text, max_len=700): | |
| """ | |
| 의견 내용 검증. | |
| - 최대 700자 | |
| - 길이만 체크 (특수문자·이모지 허용) | |
| """ | |
| return _validate_text_field( | |
| raw_text, | |
| field_name="의견 내용", | |
| max_len=max_len, | |
| allow_empty=False, | |
| allowed_pattern=None, | |
| ) | |
| def validate_entry_code(raw_text, max_len=60): | |
| """ | |
| 방 입장 암호 검증. | |
| - 비어 있어도 허용 (공개방) | |
| - 일반 텍스트보다 넓은 특수문자 허용 | |
| """ | |
| return _validate_text_field( | |
| raw_text, | |
| field_name="입장 암호", | |
| max_len=max_len, | |
| allow_empty=True, | |
| allowed_pattern=ALLOWED_ENTRY_CODE_PATTERN, | |
| ) | |
| def validate_teacher_credential(raw_text, *, field_name, max_len=60): | |
| """ | |
| 교사 ID / PW 검증. | |
| - 공백 불허 | |
| - 영문, 숫자, _.@- 만 허용 | |
| """ | |
| return _validate_text_field( | |
| raw_text, | |
| field_name=field_name, | |
| max_len=max_len, | |
| allow_empty=False, | |
| forbidden_words=[" "], | |
| allowed_pattern=ALLOWED_CREDENTIAL_PATTERN, | |
| ) | |