-
Notifications
You must be signed in to change notification settings - Fork 2
[feat]: FCM api 연동 #19
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
Merged
Minsu-Lee
merged 30 commits into
DDD-Community:develop
from
Minsu-Lee:feature/fcm-push-notification
Apr 24, 2026
Merged
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
2ace5d6
feat(notification): Firebase FCM 푸시 알림 연동 - 인프라 구성
Minsu-Lee b5e62f3
feat(notification): splash/login 화면 FCM 토큰 서버 동기화 연동
Minsu-Lee d8de4d5
chore(app): allowBackup 비활성화
Minsu-Lee a404b2a
style(notification): onDestroy 메서드를 클래스 하단으로 이동
Minsu-Lee 2f92e18
refactor(notification): FCM 토큰 데이터소스 역할과 sync 상태 API 정리
Minsu-Lee 5f15186
fix(notification): UpdateFcmTokenUseCase가 FCM 등록 결과를 반환하도록 수정
Minsu-Lee d23d351
refactor(notification): FCM 동기화 실패 로그 처리 공용 함수로 통합
Minsu-Lee 8b8034c
refactor(notification): FCM 토큰 제공자를 DIP 기반으로 추상화
Minsu-Lee 372cc5d
test(notification): 테스트 공용 Fake 통합 및 커버리지 보완
Minsu-Lee 60d14f2
refactor(notification): FcmTokenProvider를 repository 패키지로 이동
Minsu-Lee dfbde29
refactor(notification): 채널 초기화 로직을 NotificationChannelInitializer로 캡슐화
Minsu-Lee 09bf0e1
style(notification): FcmTokenErrorMapper 중첩 when을 if/else로 개선
Minsu-Lee f6f91dd
fix(notification): RegisterFcmTokenRequest에 @SerializedName 명시
Minsu-Lee 61cba5d
test(notification): FakeTokenRepository를 testutil로 이동 및 CancellationE…
Minsu-Lee a7ab3db
test(notification): SyncFcmTokenUseCase provider 예외/취소 케이스 추가
Minsu-Lee e569eff
refactor(notification): FcmTokenRepository API를 의미 단위로 정리
Minsu-Lee f5b7e29
fix(notification): runCatching을 try/catch로 교체해 CancellationException 재전파
Minsu-Lee f772ad5
style(gradle): Firebase version catalog 키를 camelCase로 통일
Minsu-Lee 2c6970b
fix(notification): 로그아웃/탈퇴 시 FCM 데이터 초기화 누락 수정
Minsu-Lee ad1cd8c
refactor(notification): FCM 데이터 초기화 책임을 UseCase 레이어로 이동
Minsu-Lee 69d0195
style(notification): FcmTokenRepository 불필요한 주석 제거
Minsu-Lee 44ad508
fix(notification): Splash 알림 권한 요청 및 notify 권한 검사 추가
Minsu-Lee c8fbaa6
refactor(notification): 미사용 saveFcmToken 제거
Minsu-Lee 725a506
refactor(notification): remove unnecessary context property
Minsu-Lee ce09ee1
fix(auth): 로그아웃 후 로컬 정리 실패 무시
Minsu-Lee 74176ec
fix(ci): GitHub CI 빌드를 위해 google-services 설정 포함
Minsu-Lee d2d1fbb
fix(main): 로그아웃 UseCase 디버그 인자 전달
Minsu-Lee b88dc31
refactor(notification): NeveraMessagingService 메서드 분리
Minsu-Lee 47094dd
refactor(notification): NotificationType 가시성을 internal로 제한
Minsu-Lee 82d1cdc
move(notification): NotificationRemoteDataSource를 FcmTokenRemoteDataS…
Minsu-Lee 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
Some comments aren't visible on the classic Files Changed page.
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
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
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,37 @@ | ||
| plugins { | ||
| alias(libs.plugins.android.library) | ||
| alias(libs.plugins.hilt) | ||
| alias(libs.plugins.ksp) | ||
| } | ||
|
|
||
| android { | ||
| namespace = "com.anddd.nevera.core.notification" | ||
| compileSdk = 36 | ||
|
|
||
| defaultConfig { | ||
| minSdk = 30 | ||
| consumerProguardFiles("consumer-rules.pro") | ||
| } | ||
|
|
||
| buildFeatures { | ||
| // AGP 8.0부터는 BuildConfig가 기본 비활성화 상태 | ||
| buildConfig = true | ||
| } | ||
|
|
||
| compileOptions { | ||
| sourceCompatibility = JavaVersion.VERSION_17 | ||
| targetCompatibility = JavaVersion.VERSION_17 | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| dependencies { | ||
| implementation(project(":domain")) | ||
| implementation(project(":core:common")) | ||
|
|
||
| implementation(libs.hilt.android) | ||
| ksp(libs.hilt.compiler) | ||
| implementation(libs.androidx.core.ktx) | ||
|
|
||
| implementation(platform(libs.firebase.bom)) | ||
| implementation(libs.firebase.messaging) | ||
| } | ||
Empty file.
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,18 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| <application> | ||
| <service | ||
| android:name=".NeveraMessagingService" | ||
| android:exported="false"> | ||
| <intent-filter> | ||
| <action android:name="com.google.firebase.MESSAGING_EVENT" /> | ||
| </intent-filter> | ||
| </service> | ||
|
|
||
| <meta-data | ||
| android:name="com.google.firebase.messaging.default_notification_channel_id" | ||
| android:value="@string/default_notification_channel_id" /> | ||
| </application> | ||
|
|
||
| </manifest> |
116 changes: 116 additions & 0 deletions
116
...notification/src/main/kotlin/com/anddd/nevera/core/notification/NeveraMessagingService.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,116 @@ | ||
| package com.anddd.nevera.core.notification | ||
|
|
||
| import android.app.NotificationManager | ||
| import android.app.PendingIntent | ||
| import android.content.Intent | ||
| import android.util.Log | ||
| import androidx.core.app.NotificationCompat | ||
| import androidx.core.net.toUri | ||
| import com.anddd.nevera.domain.model.notification.logFcmSyncFailure | ||
| import com.anddd.nevera.domain.usecase.notification.UpdateFcmTokenUseCase | ||
| import com.google.firebase.messaging.FirebaseMessagingService | ||
| import com.google.firebase.messaging.RemoteMessage | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.SupervisorJob | ||
| import kotlinx.coroutines.cancel | ||
| import kotlinx.coroutines.launch | ||
| import javax.inject.Inject | ||
|
|
||
| @AndroidEntryPoint | ||
| class NeveraMessagingService : FirebaseMessagingService() { | ||
|
|
||
| @Inject lateinit var updateFcmTokenUseCase: UpdateFcmTokenUseCase | ||
|
|
||
| private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) | ||
|
|
||
| override fun onNewToken(token: String) { | ||
| super.onNewToken(token) | ||
| serviceScope.launch { | ||
| runCatching { | ||
| updateFcmTokenUseCase(token) | ||
| }.onSuccess { result -> | ||
| result.logFcmSyncFailure(TAG, BuildConfig.DEBUG, Log::w) | ||
| }.onFailure { | ||
| if (BuildConfig.DEBUG) { | ||
| Log.e(TAG, "FCM 토큰 업데이트 실패", it) | ||
| } | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Minsu-Lee marked this conversation as resolved.
|
||
| } | ||
|
|
||
| override fun onMessageReceived(remoteMessage: RemoteMessage) { | ||
| super.onMessageReceived(remoteMessage) | ||
| var type = NotificationType.from(remoteMessage.data[NOTIFICATION_TYPE]) | ||
| val title = remoteMessage.data[NOTIFICATION_TITLE] | ||
| val body = remoteMessage.data[NOTIFICATION_BODY] | ||
| val deepLink = remoteMessage.data[NOTIFICATION_DEEPLINK] ?: DEFAULT_DEEP_LINK | ||
|
|
||
| // TODO :: 현재 type과 deepLink가 전달되고 있지 않은 상황, 임시로 type을 default로 설정합니다. | ||
| type = NotificationType.DEFAULT | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| when (type) { | ||
| NotificationType.DEFAULT -> { | ||
| showNotification( | ||
| title = title, | ||
| body = body, | ||
| type = type, | ||
| deepLink = deepLink, | ||
| ) | ||
| } | ||
| NotificationType.UNKNOWN -> { | ||
| if (BuildConfig.DEBUG) { | ||
| Log.e(TAG, "unknown type, $remoteMessage") | ||
| } | ||
| } | ||
| } | ||
|
Minsu-Lee marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private fun showNotification( | ||
| title: String?, | ||
| body: String?, | ||
| type: NotificationType, | ||
| deepLink: String, | ||
| ) { | ||
| val intent = Intent(Intent.ACTION_VIEW, deepLink.toUri()).apply { | ||
| addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) | ||
| } | ||
|
|
||
| val pendingIntent = PendingIntent.getActivity( | ||
| this, | ||
| type.ordinal, | ||
| intent, | ||
| PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, | ||
| ) | ||
|
Minsu-Lee marked this conversation as resolved.
|
||
|
|
||
| val notificationBuilder = NotificationCompat.Builder( | ||
| this, | ||
| getString(R.string.default_notification_channel_id) | ||
| ) | ||
|
|
||
| val notification = notificationBuilder.setContentTitle(title) | ||
| .setContentText(body) | ||
| .setSmallIcon(applicationInfo.icon) | ||
| .setAutoCancel(true) | ||
| .setContentIntent(pendingIntent) | ||
| .build() | ||
|
|
||
| val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager | ||
| notificationManager.notify(type.ordinal, notification) | ||
|
Minsu-Lee marked this conversation as resolved.
Outdated
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| override fun onDestroy() { | ||
| super.onDestroy() | ||
| serviceScope.cancel() | ||
| } | ||
|
|
||
| companion object { | ||
| private const val TAG = "NeveraMessagingService" | ||
| private const val NOTIFICATION_TYPE = "type" | ||
| private const val NOTIFICATION_TITLE = "title" | ||
| private const val NOTIFICATION_BODY = "body" | ||
| private const val NOTIFICATION_DEEPLINK = "deepLink" | ||
| private const val DEFAULT_DEEP_LINK = "nevera://" | ||
| } | ||
| } | ||
11 changes: 11 additions & 0 deletions
11
core/notification/src/main/kotlin/com/anddd/nevera/core/notification/NotificationType.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,11 @@ | ||||||
| package com.anddd.nevera.core.notification | ||||||
|
|
||||||
| enum class NotificationType(val value: 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.
Suggested change
외부에서 참조할 이유가 없다면 internal를 쓰면 좋을 것 같아요
Collaborator
Author
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. @Jooman-Lee |
||||||
| DEFAULT("default"), | ||||||
| UNKNOWN("unknown"); | ||||||
|
|
||||||
| companion object { | ||||||
| fun from(value: String?): NotificationType = | ||||||
| entries.find { it.value == value } ?: UNKNOWN | ||||||
| } | ||||||
| } | ||||||
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,5 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <string name="default_notification_channel_name" translatable="false">Nevera</string> | ||
| <string name="default_notification_channel_id" translatable="false">nevera_default</string> | ||
| </resources> |
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
15 changes: 15 additions & 0 deletions
15
data/src/main/kotlin/com/anddd/nevera/data/api/NotificationApi.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,15 @@ | ||
| package com.anddd.nevera.data.api | ||
|
|
||
| import com.anddd.nevera.core.network.model.ApiResponse | ||
| import com.anddd.nevera.data.model.notification.RegisterFcmTokenRequest | ||
| import com.anddd.nevera.data.model.notification.RegisterFcmTokenResponse | ||
| import retrofit2.http.Body | ||
| import retrofit2.http.POST | ||
|
|
||
| internal interface NotificationApi { | ||
|
|
||
| @POST("api/v1/notification/token") | ||
| suspend fun registerFcmToken( | ||
| @Body body: RegisterFcmTokenRequest | ||
| ): ApiResponse<RegisterFcmTokenResponse> | ||
| } |
9 changes: 9 additions & 0 deletions
9
data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSource.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,9 @@ | ||
| package com.anddd.nevera.data.datasource | ||
|
|
||
| internal interface FcmTokenLocalDataSource { | ||
| suspend fun getFcmToken(): String? | ||
| suspend fun saveFcmToken(token: String) | ||
| suspend fun isSyncNeeded(): Boolean | ||
| suspend fun setNeedsSync(value: Boolean) | ||
| suspend fun markTokenForSync(token: String) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
50 changes: 50 additions & 0 deletions
50
data/src/main/kotlin/com/anddd/nevera/data/datasource/FcmTokenLocalDataSourceImpl.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,50 @@ | ||
| package com.anddd.nevera.data.datasource | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.core.booleanPreferencesKey | ||
| import androidx.datastore.preferences.core.edit | ||
| import androidx.datastore.preferences.core.stringPreferencesKey | ||
| import androidx.datastore.preferences.preferencesDataStore | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.flow.first | ||
| import javax.inject.Inject | ||
|
|
||
| private val Context.pushTokenDataStore: DataStore<Preferences> by preferencesDataStore(name = "push_token") | ||
|
|
||
| internal class FcmTokenLocalDataSourceImpl @Inject constructor( | ||
| @param:ApplicationContext private val context: Context, | ||
| ) : FcmTokenLocalDataSource { | ||
|
|
||
| private val dataStore = context.pushTokenDataStore | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| override suspend fun getFcmToken(): String? = | ||
| dataStore.data.first()[KEY_FCM_TOKEN] | ||
|
|
||
| override suspend fun saveFcmToken(token: String) { | ||
| dataStore.edit { it[KEY_FCM_TOKEN] = token } | ||
| } | ||
|
|
||
| override suspend fun isSyncNeeded(): Boolean = | ||
| dataStore.data.first()[KEY_NEEDS_SYNC] ?: false | ||
|
|
||
| override suspend fun setNeedsSync(value: Boolean) { | ||
| dataStore.edit { it[KEY_NEEDS_SYNC] = value } | ||
| } | ||
|
|
||
| override suspend fun markTokenForSync(token: String) { | ||
| // DataStore.edit는 atomic transaction으로 동작한다. | ||
| // FCM 토큰 저장과 서버 동기화 필요 상태를 동시에 업데이트하여 | ||
| // 토큰과 sync 상태 간의 불일치가 발생하지 않도록 한다. | ||
| dataStore.edit { | ||
| it[KEY_FCM_TOKEN] = token | ||
| it[KEY_NEEDS_SYNC] = true | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| private val KEY_FCM_TOKEN = stringPreferencesKey("fcm_token") | ||
| private val KEY_NEEDS_SYNC = booleanPreferencesKey("fcm_token_needs_sync") | ||
| } | ||
| } | ||
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.
Uh oh!
There was an error while loading. Please reload this page.