Skip to content

feat: 계정별 허용 단어 그룹 추가 - #81

Merged
Whale0928 merged 7 commits into
mainfrom
feat/whitelist-groups
Oct 20, 2026
Merged

Whale0928 merged 7 commits into
mainfrom
feat/whitelist-groups

Conversation

@Whale0928

@Whale0928 Whale0928 commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

무엇을 바꾸나

계정이 허용 단어 그룹을 만들어 두고, 필터 요청의 whitelistIds에 그룹 ID를 넣으면 그 그룹의 단어는 검출과 마스킹에서 빠진다. 서비스마다 허용할 단어가 다른데 지금은 전역 사전 하나뿐이라 고객 한 곳의 사정으로 사전을 바꿀 수 없던 문제를 푼다. 결정 근거는 ADR 0009.

  • 계정당 그룹 10개, 그룹당 단어 200개, 요청당 그룹 5개
  • 여러 그룹을 넣으면 허용 단어를 합쳐서 적용
  • whitelistIds가 없는 요청은 기존과 완전히 동일

설계에서 짚을 점

  • 검출은 사전 단어로, 응답은 원문 조각으로 나간다. 고객은 응답에서 본 바-보를 등록하므로 허용 단어와 사전 단어를 같은 규칙(ProfanityText.comparisonKey)으로 정리해 비교한다.
  • QUICK 모드는 첫 검출만 돌려주므로 결과를 뒤에서 걸러 내면 허용 단어 뒤의 실제 비속어를 놓친다. 허용 단어가 아닌 첫 검출을 찾도록 구현했다.
  • 잘못된 ID는 조용히 넘기지 않는다. 없는 그룹과 남의 그룹은 같은 4090으로 거절해 남의 그룹 존재를 알 수 없게 하고, 소유 계정이 없는 API Key는 4092.
  • 결과 캐시(request_filter)는 키가 문장과 모드뿐이라 그룹을 지정한 요청은 캐시에서 제외한다. 이 조건을 빼면 한 계정의 결과가 다른 계정에 샌다.

변경 범위

영역 내용
DB V7__whitelists.sql — whitelists 테이블(계정 FK, 단어는 TEXT 한 컬럼)
도메인 Whitelist, WhitelistService, WhitelistResolver, WhitelistReader(그룹 단위 1분 캐시)
필터 ProfanityFilter에 허용 단어를 받는 메서드 추가, ProfanityText로 정리 규칙 통합
API GET/POST/PUT/DELETE /api/v1/dashboard/whitelists, 필터 요청 whitelistIds 필드, 상태 코드 4090~4092
UI /app/whitelists 목록·편집 화면, 계정 메뉴 연결
문서 overview.md, AGENTS.md, CHANGELOG, ADR 0009 승인

검증

  • 단위 27건: 필터 허용 단어 동작, 그룹 규칙(상한·정리·중복), 그룹 검증
  • E2E 30건(WhitelistE2ETest, Testcontainers MySQL): 계정 격리, 관리자도 남의 그룹 접근 불가, 키 재발급·다중 키·과거 키 연결, 동시 생성 상한, 캐시 누수, 세 모드 × JSON/form/비동기
  • 캐시 조건을 일부러 제거하고 E2E를 돌려 7건이 실패하는 것을 확인(테스트가 누수를 실제로 잡음)
  • 임시 MySQL/Redis로 서버를 띄워 실제 HTTP 35건 통과(로그인 → 키 발급 → 그룹 관리 → 필터 → 수정·삭제·재발급 반영)
  • 실제 브라우저로 사용자 동선 QA(Playwright, 로그인부터 그룹 생성·편집·삭제·모바일·다크 모드까지): 첫 동선 16단계, 이탈·되돌리기 등 추가 15건 통과
  • ./gradlew test 전체, UI 타입 검사·빌드·discovery 검증 통과

사용자 QA로 찾아 고친 것 (4da19ba)

스크립트 검증은 다 통과했지만 실제로 화면을 써 보니 문제가 있었다.

  • 단어를 입력하고 추가를 안 누른 채 저장하면 그 단어가 버려짐 → 저장 시 입력창 단어도 함께 담고, 잘못된 단어면 막고 이유를 입력칸 아래에 표시
  • 저장 안 한 변경이 있는데 뒤로가면 경고 없이 사라짐 → 화면 뒤로가기와 브라우저 뒤로가기 모두 확인창, 취소하면 쓰던 내용 유지
  • 편집 중 브라우저 뒤로가기가 사이트 첫 화면으로 튕김 → 편집을 열 때 history 항목을 쌓아 목록으로 돌아오게 함
  • 새 그룹의 요청 예시가 아직 없는 가짜 ID를 표시 → 저장 전에는 안내로 대체
  • 10개가 차면 버튼만 회색이 되고 이유가 없음 → 안내 문구 추가
  • 모바일에서 삭제 버튼 글자가 세로로 깨짐 → 줄바꿈 방지

배포 시 확인

  • V7은 새 테이블 생성이라 기존 데이터에 영향 없음. 롤백은 DROP TABLE whitelists
  • 소유 계정이 연결되지 않은 API Key(2026.09.22 기준 37개 중 30개)는 이 기능을 쓸 수 없음. 발급 이메일로 로그인하면 자동 연결됨
  • 배포는 API와 UI 이미지 둘 다 필요

🤖 Generated with Claude Code

계정이 허용 단어 그룹을 여러 개 두고 필터 요청의 whitelistIds로 골라
조합하는 방식을 ADR 0009로 제안한다. 단어는 통째로 무시하고 검출에서 빼는
것만 다루며, 계정별 차단 용어는 별도 ADR로 넘긴다. 잘못된 그룹 ID는 조용히
무시하지 않고 오류로 응답한다.

AGENTS.md의 필터 결과 캐시 서술을 실제 구성에 맞게 고친다. TTL 24시간에
최대 1,000건이고 사전이 바뀌면 자동으로 비워진다.
계정이 허용 단어 그룹을 최대 10개 두고, 필터 요청의 whitelistIds로 그룹을
골라 조합하면 그 그룹의 단어는 검출과 마스킹에서 빠진다(ADR 0009). 그룹을
지정하지 않은 요청은 기존과 똑같이 동작한다.

검출은 사전 단어로 일어나고 응답에는 원문 조각이 나가므로, 허용 단어와
사전 단어를 같은 규칙(ProfanityText.comparisonKey)으로 정리해 비교한다.
QUICK 모드는 첫 검출만 돌려주므로 결과를 뒤에서 거르지 않고 허용 단어가
아닌 첫 검출을 찾는다.

잘못된 그룹 ID는 조용히 무시하지 않는다. 없는 그룹과 남의 그룹은 같은
코드(4090)로 거절하고, 소유 계정이 없는 API Key는 4092로 거절한다. 그룹을
지정한 요청은 문장과 모드만으로 키를 만드는 결과 캐시에서 제외한다.

V7 마이그레이션으로 whitelists 테이블을 추가하고 대시보드 화면과 API,
공개 문서를 함께 갱신한다.
실제 사용자 동선으로 QA하며 찾은 문제를 고친다.

- 단어를 입력하고 추가를 누르지 않은 채 저장하면 그 단어가 버려졌다.
  저장 시 입력창의 단어도 함께 담고, 잘못된 단어면 저장을 막고 이유를
  입력칸 아래에 보여 준다.
- 저장하지 않은 변경이 있는데 뒤로가기를 누르면 경고 없이 사라졌다.
  화면의 뒤로가기와 브라우저 뒤로가기 모두 확인창을 띄우고, 취소하면
  쓰던 내용을 그대로 돌려준다.
- 편집 화면에서 브라우저 뒤로가기를 누르면 목록이 아니라 사이트 첫
  화면으로 나갔다. 편집을 열 때 history 항목을 쌓아 목록으로 돌아오게
  한다.
- 새 그룹의 요청 예시가 아직 없는 ID를 보여 줬다. 저장 전에는 안내로
  대신한다.
- 그룹이 10개가 차면 버튼만 비활성화되고 이유가 없었다. 안내를 붙인다.
- 모바일 폭에서 삭제 버튼 글자가 세로로 깨졌다.
그룹의 첫 단어로 그룹을 적용하기 전과 후의 마스킹 결과를 나란히 보여 준다.
허용 단어는 검출된 표현과 똑같아야 하며 띄어 쓴 표기는 따로 등록해야 한다는
안내를 화면과 공개 문서에 적는다.
로그인한 사용자에게만 상단 메뉴에 커스텀 항목을 보여 주고, 누르면 아래로
펼쳐진다. 허용 단어 그룹으로 가는 항목과, 아직 없는 기능인 커스텀 필터링
그룹 항목을 비활성으로 둔다. 바깥을 누르거나 Escape로 닫히고 모바일
메뉴 안에서는 목록으로 펼쳐진다.
@Whale0928
Whale0928 merged commit 9202958 into main Oct 20, 2026
3 checks passed
@Whale0928
Whale0928 deleted the feat/whitelist-groups branch October 20, 2026 06:49
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.

1 participant