-
Notifications
You must be signed in to change notification settings - Fork 0
[GMSS-101] feat: 위험 표현 감지 및 상담 기관 안내 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
soyeonLee126
wants to merge
10
commits into
develop
Choose a base branch
from
feature/risk-keyword-guidance
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
223d732
feat: 위험 표현 감지 도메인 로직 추가 - 원문에서 자살·자해 위험 표현을 찾아 위험도와 안내할 상담 기관을 반환하는 …
soyeonLee126 486f403
feat: 위험 표현 사전 원격 갱신 구현 - 앱 내장 assets JSON을 기본으로 두고 Firestore app_con…
soyeonLee126 9376eb7
feat: 상담 기관 안내 다이얼로그 추가 - 위험 표현이 감지되면 상담 창구를 priority 순으로 노출하고 전화 걸기를…
soyeonLee126 5ea16e3
feat: 감정 분석 화면에 위험 표현 감지 연결 - onAnalyze() 에서 감정 분석 전에 원문을 검사하고, CRITI…
soyeonLee126 1fa2e4d
refactor: Firestore 인스턴스를 실제 조회 시점까지 지연 생성 - FirebaseFirestore 를 Prov…
soyeonLee126 c46ba45
chore: 위험 표현 감지 관련 주석 정리 - 코드에서 유추 가능한 설명 주석을 제거하고, RiskTermMatcher 의…
soyeonLee126 e908c6b
feat: 위험 표현 사전에 죽을거야/죽을래 계열 추가 - 죽을거야, 죽을게, 죽을래, 죽어버릴, 죽어야지, 죽고말, 죽는게…
soyeonLee126 2fc993e
feat: 공식 자살 경고신호 표현을 위험 표현 사전에 반영 - 보건복지부·자살예방센터가 정리한 언어적 경고신호에서 9개를 …
soyeonLee126 c617ec8
Merge remote-tracking branch 'origin/develop' into feature/risk-keywo…
soyeonLee126 34c4208
refactor: 위험 감지 코드 정리 및 designsystem 토큰 적용 - RiskDetection.matchedTer…
soyeonLee126 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
28 changes: 27 additions & 1 deletion
28
app/src/main/kotlin/com/gamss/android/app/GamssApplication.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,33 @@ | ||
| package com.gamss.android.app | ||
|
|
||
| import android.app.Application | ||
| import android.util.Log | ||
| import com.gamss.android.domain.safety.RefreshRiskLexiconUseCase | ||
| import dagger.hilt.android.HiltAndroidApp | ||
| import kotlinx.coroutines.CoroutineExceptionHandler | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.SupervisorJob | ||
| import kotlinx.coroutines.launch | ||
| import javax.inject.Inject | ||
|
|
||
| private const val TAG = "GamssApplication" | ||
|
|
||
| @HiltAndroidApp | ||
| class GamssApplication : Application() | ||
| class GamssApplication : Application() { | ||
|
|
||
| @Inject | ||
| lateinit var refreshRiskLexicon: RefreshRiskLexiconUseCase | ||
|
|
||
| private val applicationScope = CoroutineScope( | ||
| SupervisorJob() + Dispatchers.IO + | ||
| CoroutineExceptionHandler { _, throwable -> | ||
| Log.w(TAG, "application scope task failed", throwable) | ||
| }, | ||
| ) | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
| applicationScope.launch { refreshRiskLexicon() } | ||
| } | ||
| } |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| { | ||
| "version": 3, | ||
| "terms": [ | ||
| { "term": "차라리죽", "level": "CRITICAL" }, | ||
| { "term": "죽어야편", "level": "CRITICAL" }, | ||
| { "term": "죽는게답", "level": "CRITICAL" }, | ||
| { "term": "다끝내버리고싶", "level": "CRITICAL" }, | ||
| { "term": "죽었으면좋겠", "level": "WARNING" }, | ||
| { "term": "나만없으면", "level": "WARNING" }, | ||
| { "term": "태어나지말았", "level": "WARNING" }, | ||
| { "term": "살아있을이유", "level": "WARNING" }, | ||
| { "term": "없어지는게낫", "level": "WARNING" }, | ||
| { "term": "죽을거야", "level": "CRITICAL" }, | ||
| { "term": "죽을게", "level": "CRITICAL" }, | ||
| { "term": "죽을래", "level": "CRITICAL" }, | ||
| { "term": "죽어버릴", "level": "CRITICAL" }, | ||
| { "term": "죽어야지", "level": "CRITICAL" }, | ||
| { "term": "죽고말", "level": "CRITICAL" }, | ||
| { "term": "죽는게낫", "level": "CRITICAL" }, | ||
| { "term": "죽으면좋겠", "level": "CRITICAL" }, | ||
| { "term": "죽는수밖에", "level": "CRITICAL" }, | ||
| { "term": "자살", "level": "CRITICAL" }, | ||
| { "term": "자살방법", "level": "CRITICAL" }, | ||
| { "term": "죽는방법", "level": "CRITICAL" }, | ||
| { "term": "죽고싶", "level": "CRITICAL" }, | ||
| { "term": "죽어버리", "level": "CRITICAL" }, | ||
| { "term": "죽어야겠", "level": "CRITICAL" }, | ||
| { "term": "자해", "level": "CRITICAL" }, | ||
| { "term": "손목그", "level": "CRITICAL" }, | ||
| { "term": "목매달", "level": "CRITICAL" }, | ||
| { "term": "목을매", "level": "CRITICAL" }, | ||
| { "term": "극단적선택", "level": "CRITICAL" }, | ||
| { "term": "살고싶지않", "level": "CRITICAL" }, | ||
| { "term": "유서를", "level": "CRITICAL" }, | ||
| { "term": "유서써", "level": "CRITICAL" }, | ||
| { "term": "유서남", "level": "CRITICAL" }, | ||
| { "term": "번개탄", "level": "WARNING" }, | ||
| { "term": "사라지고싶", "level": "WARNING" }, | ||
| { "term": "없어지고싶", "level": "WARNING" }, | ||
| { "term": "살기싫", "level": "WARNING" }, | ||
| { "term": "살아갈이유", "level": "WARNING" }, | ||
| { "term": "다포기하고싶", "level": "WARNING" }, | ||
| { "term": "아무도날필요", "level": "WARNING" }, | ||
| { "term": "혼자남겨진것같", "level": "WARNING" } | ||
| ], | ||
| "safePhrases": [ | ||
| "죽을것같", | ||
| "죽을거같", | ||
| "죽을뻔", | ||
| "죽을각오", | ||
| "죽을힘", | ||
| "죽을죄", | ||
| "너죽을래", | ||
| "죽을때까지", | ||
| "죽을고생", | ||
| "죽고싶지않", | ||
| "자살예방", | ||
| "자살률", | ||
| "자살골", | ||
| "자살패스", | ||
| "유서깊", | ||
| "배고파죽", | ||
| "웃겨죽", | ||
| "좋아죽", | ||
| "귀여워죽", | ||
| "더워죽", | ||
| "추워죽", | ||
| "졸려죽", | ||
| "힘들어죽", | ||
| "심심해죽", | ||
| "죽을맛", | ||
| "죽을만큼", | ||
| "죽도록", | ||
| "죽여주는", | ||
| "자해공갈" | ||
| ], | ||
| "agencies": [ | ||
| { | ||
| "id": "kr-109", | ||
| "name": "자살예방 상담전화", | ||
| "description": "24시간 무료 전문 상담", | ||
| "phoneNumber": "109", | ||
| "url": null, | ||
| "priority": 1 | ||
| }, | ||
| { | ||
| "id": "kr-mental-health", | ||
| "name": "정신건강 상담전화", | ||
| "description": "정신건강 위기 상담", | ||
| "phoneNumber": "1577-0199", | ||
| "url": null, | ||
| "priority": 2 | ||
| }, | ||
| { | ||
| "id": "kr-youth", | ||
| "name": "청소년 상담전화", | ||
| "description": "청소년 전문 상담", | ||
| "phoneNumber": "1388", | ||
| "url": null, | ||
| "priority": 3 | ||
| } | ||
| ] | ||
| } |
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
40 changes: 40 additions & 0 deletions
40
data/src/main/java/com/gamss/android/data/di/SafetyModule.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.gamss.android.data.di | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.PreferenceDataStoreFactory | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.preferencesDataStoreFile | ||
| import com.gamss.android.data.safety.RiskLexiconDataStore | ||
| import com.gamss.android.data.safety.RiskLexiconRepositoryImpl | ||
| import com.gamss.android.domain.safety.RiskLexiconRepository | ||
| import dagger.Binds | ||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import dagger.hilt.components.SingletonComponent | ||
| import javax.inject.Singleton | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| internal abstract class SafetyModule { | ||
|
|
||
| @Binds | ||
| abstract fun bindRiskLexiconRepository( | ||
| riskLexiconRepositoryImpl: RiskLexiconRepositoryImpl, | ||
| ): RiskLexiconRepository | ||
|
|
||
| companion object { | ||
| @Provides | ||
| @Singleton | ||
| @RiskLexiconDataStore | ||
| fun provideRiskLexiconDataStore( | ||
| @ApplicationContext context: Context, | ||
| ): DataStore<Preferences> = PreferenceDataStoreFactory.create( | ||
| produceFile = { context.preferencesDataStoreFile(RISK_LEXICON_DATA_STORE_NAME) }, | ||
| ) | ||
|
|
||
| private const val RISK_LEXICON_DATA_STORE_NAME = "risk_lexicon" | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
data/src/main/java/com/gamss/android/data/safety/BundledRiskLexiconDataSource.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.gamss.android.data.safety | ||
|
|
||
| import android.content.Context | ||
| import com.gamss.android.data.safety.model.RiskLexiconDto | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.withContext | ||
| import kotlinx.serialization.json.Json | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| internal class BundledRiskLexiconDataSource @Inject constructor( | ||
| @param:ApplicationContext private val context: Context, | ||
| private val json: Json, | ||
| ) { | ||
| suspend fun load(): RiskLexiconDto = withContext(Dispatchers.IO) { | ||
| context.assets.open(ASSET_NAME).bufferedReader().use { reader -> | ||
| json.decodeFromString<RiskLexiconDto>(reader.readText()) | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| const val ASSET_NAME = "risk_lexicon.json" | ||
| } | ||
| } |
66 changes: 66 additions & 0 deletions
66
data/src/main/java/com/gamss/android/data/safety/FirestoreRiskLexiconDataSource.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package com.gamss.android.data.safety | ||
|
|
||
| import com.gamss.android.data.safety.model.RiskLexiconDto | ||
| import com.gamss.android.data.safety.model.RiskTermDto | ||
| import com.gamss.android.data.safety.model.SupportAgencyDto | ||
| import com.google.firebase.firestore.DocumentSnapshot | ||
| import com.google.firebase.firestore.FirebaseFirestore | ||
| import kotlinx.coroutines.tasks.await | ||
| import javax.inject.Inject | ||
| import javax.inject.Provider | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| internal class FirestoreRiskLexiconDataSource @Inject constructor( | ||
| private val firestoreProvider: Provider<FirebaseFirestore>, | ||
| ) { | ||
| suspend fun fetch(): RiskLexiconDto? = | ||
| firestoreProvider.get() | ||
| .collection(COLLECTION) | ||
| .document(DOCUMENT) | ||
| .get() | ||
| .await() | ||
| .takeIf { it.exists() } | ||
| ?.toDto() | ||
|
|
||
| private fun DocumentSnapshot.toDto(): RiskLexiconDto = RiskLexiconDto( | ||
| version = getLong(FIELD_VERSION)?.toInt() ?: 0, | ||
| terms = objectList(FIELD_TERMS).map { | ||
| RiskTermDto(term = it.string(FIELD_TERM), level = it.string(FIELD_LEVEL)) | ||
| }, | ||
| safePhrases = (get(FIELD_SAFE_PHRASES) as? List<*>).orEmpty().filterIsInstance<String>(), | ||
| agencies = objectList(FIELD_AGENCIES).map { | ||
| SupportAgencyDto( | ||
| id = it.string(FIELD_ID), | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FIELD_URL을 읽어 SupportAgencyDto.url에 채우고 toDomain()에서 버리는 죽은 데이터 경로인데 혹시 나중에 쓸 계획 있나요? 아니라면 FIELD_URL/SupportAgencyDto.url도 같이 제거하는 게 깔끔할 것 같습니다! |
||
| priority = (it[FIELD_PRIORITY] as? Number)?.toInt() ?: Int.MAX_VALUE, | ||
| ) | ||
| }, | ||
| ) | ||
|
|
||
| private fun DocumentSnapshot.objectList(field: String): List<Map<*, *>> = | ||
| (get(field) as? List<*>).orEmpty().filterIsInstance<Map<*, *>>() | ||
|
|
||
| private fun Map<*, *>.string(key: String): String = (this[key] as? String).orEmpty() | ||
|
|
||
| private companion object { | ||
| const val COLLECTION = "app_config" | ||
| const val DOCUMENT = "risk_lexicon" | ||
|
|
||
| const val FIELD_VERSION = "version" | ||
| const val FIELD_TERMS = "terms" | ||
| const val FIELD_SAFE_PHRASES = "safePhrases" | ||
| const val FIELD_AGENCIES = "agencies" | ||
| const val FIELD_TERM = "term" | ||
| const val FIELD_LEVEL = "level" | ||
| const val FIELD_ID = "id" | ||
| const val FIELD_NAME = "name" | ||
| const val FIELD_DESCRIPTION = "description" | ||
| const val FIELD_PHONE_NUMBER = "phoneNumber" | ||
| const val FIELD_URL = "url" | ||
| const val FIELD_PRIORITY = "priority" | ||
| } | ||
| } | ||
7 changes: 7 additions & 0 deletions
7
data/src/main/java/com/gamss/android/data/safety/RiskLexiconDataStore.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.gamss.android.data.safety | ||
|
|
||
| import javax.inject.Qualifier | ||
|
|
||
| @Qualifier | ||
| @Retention(AnnotationRetention.BINARY) | ||
| internal annotation class RiskLexiconDataStore |
49 changes: 49 additions & 0 deletions
49
data/src/main/java/com/gamss/android/data/safety/RiskLexiconLocalDataSource.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.gamss.android.data.safety | ||
|
|
||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.core.edit | ||
| import androidx.datastore.preferences.core.longPreferencesKey | ||
| import androidx.datastore.preferences.core.stringPreferencesKey | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.flow.first | ||
| import kotlinx.coroutines.withContext | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| internal data class CachedRiskLexicon( | ||
| val json: String, | ||
| val fetchedAtMillis: Long, | ||
| ) | ||
|
|
||
| @Singleton | ||
| internal class RiskLexiconLocalDataSource @Inject constructor( | ||
| @param:RiskLexiconDataStore private val dataStore: DataStore<Preferences>, | ||
| ) { | ||
| suspend fun read(): CachedRiskLexicon? = withContext(Dispatchers.IO) { | ||
| val preferences = dataStore.data.first() | ||
| val json = preferences[LEXICON_JSON_KEY] ?: return@withContext null | ||
| CachedRiskLexicon( | ||
| json = json, | ||
| fetchedAtMillis = preferences[FETCHED_AT_KEY] ?: 0L, | ||
| ) | ||
| } | ||
|
|
||
| suspend fun write(json: String, fetchedAtMillis: Long): Unit = withContext(Dispatchers.IO) { | ||
| dataStore.edit { preferences -> | ||
| preferences[LEXICON_JSON_KEY] = json | ||
| preferences[FETCHED_AT_KEY] = fetchedAtMillis | ||
| } | ||
| } | ||
|
|
||
| suspend fun markFetched(fetchedAtMillis: Long): Unit = withContext(Dispatchers.IO) { | ||
| dataStore.edit { preferences -> | ||
| preferences[FETCHED_AT_KEY] = fetchedAtMillis | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| val LEXICON_JSON_KEY = stringPreferencesKey("risk_lexicon_json") | ||
| val FETCHED_AT_KEY = longPreferencesKey("risk_lexicon_fetched_at") | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Source 미지정 시 기본값(Source.DEFAULT)은 서버 접속 실패 시 예외 대신 Firestore SDK 자체 오프라인 캐시의 낡은 스냅샷을 조용히 반환합니다. adopt()는 fetch()가 non-null이기만 하면 버전 비교 전에 markFetched(now())부터 호출해 24시간 TTL을 리셋하므로, 오프라인 상태에서 낡은 캐시를 받아도 "방금 서버 확인 완료"로 잘못 기록됩니다 —>실제로는 신규 원격 사전을 놓친 채로 하루를 더 기다리게 됩니다.
.get(Source.SERVER)로 바꾸면 서버 접속 실패 시 확실히 예외가 발생하게 될것 같습니다!