Skip to content

fix: 비속어 검출 위치를 원문 기준으로 계산 - #83

Merged
Whale0928 merged 1 commit into
mainfrom
fix/filter-original-offsets
Oct 28, 2026
Merged

Whale0928 merged 1 commit into
mainfrom
fix/filter-original-offsets

Conversation

@Whale0928

Copy link
Copy Markdown
Owner

요약

비속어 검출 결과의 원문 위치를 원문에서 키워드 글자를 indexOf로 다시 찾는 대신, 정리(ProfanityText.clean)하는 시점에 기록한 위치로 계산합니다.

문제

NormalProfanityFilter는 정리된 문자열에서 매칭한 뒤, 원문 위치를 text.indexOf(키워드 글자)로 되찾았습니다. 이 방식에서 두 가지 문제가 생겼습니다.

  1. 과잉 마스킹: 매칭 위치보다 앞에 같은 첫 글자가 있으면 그 위치부터 잘라 냅니다. 이 때문에 정상 단어까지 검출에 포함됐습니다.
  2. 대문자 영문 누락: Trie는 ignoreCase()인데 indexOf는 대소문자를 구분합니다. 공개 문서(overview.md)는 "대소문자는 신경 쓰지 않아도 된다"고 약속하지만 실제 동작이 달랐습니다.
모드 입력 변경 전 변경 후
FILTER 시간 시발 ***** 시간 **
FILTER 바보 시간 시발 ** ***** ** 시간 **
NORMAL FUCK you (사전 fuck) 검출 없음 FUCK (0, 4)
FILTER FUCK you 원문 그대로 **** you
QUICK FUCK you StringIndexOutOfBoundsException FUCK (0, 4)

기호를 끼운 우회 검출(바-보 → ***)은 변경 전과 같습니다.

변경 내용

  • ProfanityText.cleanWithOffsets: clean과 같은 NOT_MATCHABLE 패턴으로 정리하면서, 남은 글자마다 원문 위치를 기록합니다. 정리 규칙은 한 곳에 그대로 둡니다.
  • NormalProfanityFilter: Emit.getStart()/getEnd()(끝은 포함 구간)를 기록한 위치로 원문 구간에 옮깁니다. allMatched와 firstMatched 두 오버로드의 중복 위치 계산을 toFilterWord 하나로 모았습니다.
  • ahocorasick 0.6.3은 ignoreCase일 때 글자 단위(Character.toLowerCase(char))로 비교합니다. 그래서 정리된 문자열과 Emit 좌표가 일치합니다.

테스트

신규 24개를 추가했습니다.

  • NormalProfanityFilterTest.OriginalOffsets: 앞에 같은 첫 글자가 있는 경우, 끼운 기호, 반복 단어, 영문 대소문자 4종(fool/FOOL/Fool/fOoL)을 정확한 위치로 단언합니다.
  • DefaultProfanityHandlerTest: FILTER 모드 마스킹 결과(욕심 많은 욕설 → 욕심 많은 **)를 확인합니다.
  • AllowedWordsFilterTest: 기존 allowsEnglishCaseInsensitively는 허용 여부와 관계없이 FOOL이 검출되지 않아 통과하던 상태였습니다. 허용하지 않은 경우의 검출 테스트를 추가해 의미를 되살리고, 허용 단어를 건너뛴 뒤의 영문 위치도 확인합니다.
  • ProfanityTextTest: cleanWithOffsets가 clean과 같은 결과를 내는지(이모지와 서로게이트 포함), 위치가 원문의 같은 글자를 가리키는지 확인합니다.

로컬 검증 결과입니다.

  • ./gradlew staticCheck: 통과
  • ./gradlew unitTest --continue
    • profanity-domain: 223개 통과 (기존 199개 + 신규 24개)
    • profanity-shared: 13개 통과
    • profanity-api: 126개 중 123개 통과. 실패 3개(AdminPortalMigrationTest, ApiKeyMigrationTest)는 로컬 환경에 Docker가 없어 Testcontainers가 기동하지 않은 것이 원인입니다. main에서도 같은 3개가 같은 이유로 실패합니다.
  • supportTest, apiE2eTest: Docker가 필요해 로컬에서는 실행하지 못했습니다. CI 결과로 확인이 필요합니다.

영향과 주의

  • 응답 형식은 바뀌지 않습니다. 바뀌는 것은 검출 단어(filteredWord)와 위치, 그리고 마스킹 범위입니다.
  • request_filter Caffeine 캐시(TTL 24시간)에는 변경 전 결과가 남아 있을 수 있습니다. 로컬 캐시라 롤링 업데이트로 새 pod가 뜨면 비워집니다.
  • 운영 사전에 영문 단어가 없다면, 대문자 문제는 실제로 영향을 주지 않았을 수 있습니다.

Generated by Claude Code

원문에서 키워드 글자를 indexOf로 다시 찾던 방식을 정리 시점에 기록한
원문 위치로 바꾼다.

- 앞에 같은 첫 글자가 있으면 그 위치부터 잘라 정상 단어까지 마스킹하던
  문제("시간 시발" -> "*****")를 고친다.
- Trie는 대소문자를 무시하는데 indexOf는 구분해서, 대문자 영문이
  NORMAL/FILTER에서 누락되고 QUICK에서 StringIndexOutOfBoundsException이
  나던 문제를 고친다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJgrQc8EvNg9yM7peD7cCF
@Whale0928
Whale0928 merged commit 0760971 into main Oct 28, 2026
3 checks passed
@Whale0928
Whale0928 deleted the fix/filter-original-offsets branch October 28, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants