[GMSS-101] feat: 위험 표현 감지 및 상담 기관 안내 - #13
Open
soyeonLee126 wants to merge 10 commits into
Open
Conversation
…DetectRiskInTextUseCase 추가. CRITICAL은 전송 차단, WARNING은 안내만 하도록 등급을 나눠 원격 사전에서 조정할 수 있게 했다 - RiskTermMatcher는 공백·구두점을 지운 뒤 매칭하되, 두 글자 이하 표현은 어절 경계에서 시작할 때만 인정한다. 공백을 지우면 "과자 살까", "혼자 살아보니" 안에 "자살"이 생겨 평범한 일기가 차단되기 때문 - safePhrases로 "배고파 죽겠다" 류의 관용 표현을 걸러낸다
…fig/risk_lexicon 의 version 이 더 클 때만 채택한다. 조회 결과는 DataStore 에 캐시하고 24시간 동안 재조회하지 않는다 - 원격 조회·캐시 파싱·assets 로딩이 모두 실패해도 예외를 내보내지 않는다. 안전 기능이 네트워크 상태에 좌우되면 안 된다 - terms 나 agencies 가 빈 원격 사전은 채택하지 않는다. 필드명 오타로 감지가 통째로 꺼지는 사고를 막는다 - 원문은 기기 밖으로 나가지 않는다. 사전만 받아와 온디바이스로 검사한다 - firestore.rules 로 app_config 를 인증 사용자 읽기 전용으로 제한
… 제공하는 GamssSupportAgencyDialog 추가. 차단된 경우와 안내만 하는 경우의 문구를 구분한다 - dial() 은 ACTION_DIAL 로 번호만 채우고 발신은 사용자가 누른다. CALL_PHONE 권한을 요구하지 않기 위해서다 - 다이얼러가 없는 기기에서 버튼이 무반응으로 보이지 않도록 dial() 이 성공 여부를 반환한다 - Android 11+ 패키지 가시성 정책에 막히지 않도록 AndroidManifest 에 DIAL queries 선언
…CAL 이면 분석을 실행하지 않는다. 차단해도 입력 state 는 그대로 둬 안내를 닫았을 때 사용자가 쓴 글이 남아 있게 한다 - 사전 로딩이 suspend 지점이라 연타로 중복 실행될 수 있어 isRunning 갱신을 감지 호출 앞으로 옮겼다 - 원문은 노출·기록하지 않는다는 기존 정책을 그대로 따른다 - 앱 시작 시 1회 사전을 갱신한다. 전송마다 네트워크를 타면 감지가 느려지고 읽기 비용이 사용량에 비례해 늘어난다
…ider 로 받도록 변경. 인스턴스 생성 시점에 gRPC 채널과 ConnectivityManager 콜백이 함께 올라오는데, 앱 시작 시 사전 갱신은 캐시가 신선하면 조회를 건너뛰므로 그 경우 초기화 비용을 치를 이유가 없다
… 어절 경계 규칙만 남겼다. 두 글자 이하 표현을 어절 경계로 제한하는 이유는 코드만 봐서는 알 수 없고, 제거하면 "과자 살까" 오탐이 되살아난다
…낫, 죽으면좋겠, 죽는수밖에 9개를 CRITICAL 로 추가. 기존 죽어버리 는 죽어버릴거야 를 잡지 못해 죽어버릴 을 따로 넣었다 - 이 계열은 과장 표현과 겹치는 범위가 넓어 safePhrase 9개를 함께 추가했다. 죽을것같, 죽을거같, 죽을뻔, 죽을각오, 죽을힘, 죽을죄, 너죽을래, 죽을때까지, 죽을고생 - 회귀 테스트에 차단 대상 8개, 통과 대상 9개 문장 추가
…추가했다. CRITICAL 4개(차라리죽, 죽어야편, 죽는게답, 다끝내버리고싶)와 WARNING 5개(죽었으면좋겠, 나만없으면, 태어나지말았, 살아있을이유, 없어지는게낫) - 죽었으면좋겠 는 타인을 향한 발화일 수 있어 차단하지 않고 안내만 하는 WARNING 으로 두었다 - 희망이없 과 짐만되는 은 검증에서 일상 문장에 걸려 제외했다. 회귀 테스트에 해당 문장을 통과 대상으로 고정했다
…ms 와 SupportAgency.url 제거. 소비처가 없는데 matchedTerms 는 UI state 까지 올라가 사용자가 쓴 표현이 상주했다 - 상담 기관 안내 다이얼로그와 전화 유틸을 core:ui 에서 feature:emotion 으로 이동. core:ui 는 domain 을 모르는 순수 컴포저블 모듈로 되돌렸다 - MaterialTheme 대신 GamssTheme 토큰 사용(spacing200, body4Regular, body5Medium, subtitle4) - 다이얼로그의 isBlocking Boolean 플래그를 RiskLevel 로 교체. 등급이 늘면 when 이 컴파일 시점에 누락을 잡는다 - 기관 priority 정렬을 matcher 에서 DTO 매핑으로 이동해 RiskLexicon.agencies 가 항상 정렬돼 있다는 불변식으로 만들었다 - RiskLexiconRepository 에 무예외 계약을 KDoc 으로 명시. 보장이 runSafely 구현 디테일에만 있었다 - MASK 길이 보존 제약 주석 추가. 길이가 바뀌면 wordStarts 인덱스가 밀려 짧은 표현 매칭이 조용히 깨진다 - 사전 회귀 테스트가 첫 실패에서 멈추지 않고 어긋난 문장을 모아 보고하도록 변경
soyeonLee126
marked this pull request as ready for review
August 6, 2026 05:59
seunghee17
requested changes
Aug 7, 2026
| name = it.string(FIELD_NAME), | ||
| description = it.string(FIELD_DESCRIPTION), | ||
| phoneNumber = it[FIELD_PHONE_NUMBER] as? String, | ||
| url = it[FIELD_URL] as? String, |
Collaborator
There was a problem hiding this comment.
FIELD_URL을 읽어 SupportAgencyDto.url에 채우고 toDomain()에서 버리는 죽은 데이터 경로인데 혹시 나중에 쓸 계획 있나요? 아니라면 FIELD_URL/SupportAgencyDto.url도 같이 제거하는 게 깔끔할 것 같습니다!
| firestoreProvider.get() | ||
| .collection(COLLECTION) | ||
| .document(DOCUMENT) | ||
| .get() |
Collaborator
There was a problem hiding this comment.
Source 미지정 시 기본값(Source.DEFAULT)은 서버 접속 실패 시 예외 대신 Firestore SDK 자체 오프라인 캐시의 낡은 스냅샷을 조용히 반환합니다. adopt()는 fetch()가 non-null이기만 하면 버전 비교 전에 markFetched(now())부터 호출해 24시간 TTL을 리셋하므로, 오프라인 상태에서 낡은 캐시를 받아도 "방금 서버 확인 완료"로 잘못 기록됩니다 —>실제로는 신규 원격 사전을 놓친 채로 하루를 더 기다리게 됩니다.
.get(Source.SERVER)로 바꾸면 서버 접속 실패 시 확실히 예외가 발생하게 될것 같습니다!
Collaborator
1. 차단 정책 — 동의2. 번개탄 / 죽었으면좋겠 → WARNING — 둘 다 동의
3. 기관 정보 — 3개면 충분, 다만 텍스트 상담 채널 부재지금 다 전화만 있는데 뭔가 이런 위험 신호를 내뱉는 사용자 특성상 바로 전화가 어려울 수도 있을거 같다는 생각이 들었습니다..! 텍스트 상담을 선호할 수도 있을 거 같아서 (관련 상담소 채팅 서비스가 있다면) 문자, 카톡 상담 URL 추가를 고려하면 어떨까 합니다! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
작업 개요
사용자가 일기를 저장할 때 원문에서 자살·자해 위험 표현을 감지해, 전문 상담 기관을 즉시 안내합니다.
위험도가 CRITICAL이면 감정 분석 실행을 차단하고, WARNING이면 안내만 하고 진행합니다.
감지 사전은 앱에 내장된 JSON이 기본이고, Firestore
app_config/risk_lexicon문서의version이 더 크면 그쪽을 채택한다. 앱 배포 없이 단어와 등급을 조정할 수 있게 하려는 목적입니다.원문은 기기 밖으로 나가지 않습니다. 사전만 원격에서 받아오고 검사는 전부 온디바이스에서 진행합니다.
작업 유형
변경 사항
domain/safety위험 표현 감지 도메인.RiskTermMatcher(매칭),DetectRiskInTextUseCase,RefreshRiskLexiconUseCasedata/safety내장 assets JSON → DataStore 캐시 → Firestore 오버라이드 3단 폴백. 원격 조회는 앱 시작 시 1회, 캐시 TTL 24시간core/uiGamssSupportAgencyDialog(레포 최초 다이얼로그),dial()전화 연결 유틸,safety_` 문자열 리소스feature/emotiononAnalyze()에서 감정 분석 전에 원문 검사. CRITICAL이면 분석을 실행하지 않되 입력 state는 유지firestore.rules,firebase.jsonapp_config를 인증 사용자 읽기 전용으로 제한, 쓰기 전면 차단firebase-firestore추가 (기존firebase-bom,kotlinx-coroutines-play-services재사용)매칭 규칙에서 짚어둘 점
공백과 구두점을 지운 뒤 부분 문자열로 찾되, 두 글자 이하 표현은 어절 경계에서 시작할 때만 인정합니다.
공백을 지우면
"과자 살까 말까"와"혼자 살아보니"안에"자살"이 생겨 평범한 일기가 차단됨.초기 구현에서 실제로 이 문제가 있었고, 어절 경계 조건으로 해결.
세 글자 이상은
"죽고 싶다"처럼 어절이 갈려도 잡아야 하므로 위치를 보지 않습니다.오탐 방지를 위해 사전에
safePhrases를 둔다."배고파 죽겠다","죽을 것 같아","죽을 뻔","너 죽을래?","유서 깊은","자살골"같은 관용 표현·합성어를 마스킹합니다.사전 구성
terms 41개(CRITICAL 28 / WARNING 13), safePhrases 29개.
어휘는 보건복지부·자살예방센터가 정리한 언어적 경고신호를 기준으로 뽑았습니다.
공개된 한국어 자살위험 어휘 사전은 존재하지 않아, 관련 연구와 평가도구는 문항 형태라 텍스트 매칭에 쓸 수 없었음.
검증 과정에서
희망이없,짐만되는은"이 프로젝트는 희망이 없다","걔 진짜 짐만 되는 스타일이야"같은 일상 문장에 걸려 제외. 그 문장들을 회귀 테스트의 통과 대상으로 고정. 대가로"이제 희망이 없어"류의 신호는 놓침..안전 설계
terms나agencies가 빈 원격 사전은 채택하지 않는다. 필드명 오타로 감지가 통째로 꺼지는 사고를 막음WARNING으로 내린다. 운영 오타가 과차단이 되면 안됨관련 이슈
관련 작업 (Notion)
스크린샷 / 동작 화면
죽고싶다입력 → 상담 기관 안내 다이얼로그 노출, 감정 분석 실행되지 않음배고파죽겠네입력 → 다이얼로그 없이 정상 감정 분석, 캐릭터 결과 노출과자 살까 말까입력 → 다이얼로그 없음 (어절 경계 규칙 확인)version: 2문서를 올린 뒤 앱이 그것을 채택하는 것을 DataStore 캐시에서 확인체크리스트
develop으로 설정feat:,fix:등)을 따른다리뷰 요청 사항
1. 차단 정책의 적정성
CRITICAL은 전송을 차단한다. 감정 일기 앱에서 사용자가 감정을 적는 통로를 막는 셈이라 트레이드오프가 있습니다.
WARNING 등급을 따로 두고, 등급을 원격 사전에서 조정할 수 있게 했다. 차단 범위가 적절한지 봐주시면 감사하겠습니다
2. 내장 사전의 단어·등급 구성
data/src/main/assets/risk_lexicon.json의 terms 41개, safePhrases 29개. 오탐과 미탐 둘 다 비용이 큰 영역이라 리뷰가 필요합니다.등급을 내린 판단 두 건을 특히 봐주시면 감사하겠습니다.
번개탄: 자살 수단 신호이지만 캠핑·구이 용례가 흔해서 WARNING죽었으면좋겠:"걔 죽었으면 좋겠어"처럼 타인을 향한 발화일 수 있어 WARNING3. 기관 정보
자살예방 상담전화는 2024년 1월부터
1393에서109로 통합됨.1577-0199(정신건강),1388(청소년)은 각자 번호를 계속 운영. 이 세 개만 넣었습니다.테스트
신규 단위 테스트 28개. CI가 게이트하는
detekt/lint/test/assembleDebug/assembleRelease모두 통과RiskTermMatcherTest(14) 등급 판정, safePhrase 마스킹, 어절 경계, 띄어쓰기·구두점 우회DetectRiskInTextUseCaseTest(3) 기관 priority 정렬, 빈 사전RiskLexiconRepositoryImplTest(7) Firestore 실패 폴백, version 비교, 캐시 손상, TTLBundledRiskLexiconContentTest(4) 실제 assets JSON을 읽어 검증. 차단 대상 20문장, 안내 대상 8문장, 걸리면 안 되는 일상 문장 30문장을 고정