Skip to content

Commit f280150

Browse files
authored
Merge pull request #136 from rbqks529/feat/#135_API_Notification_FCM
[FEAT] FCM Token 발급, 등록, 푸시알림 기능 구현 및 기타 QA수정
2 parents e8141b0 + 1dcbf42 commit f280150

38 files changed

+941
-155
lines changed

.idea/appInsightsSettings.xml

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ dependencies {
103103
implementation(libs.foundation)
104104
implementation(libs.androidx.lifecycle.runtime.compose)
105105
implementation(libs.androidx.datastore.preferences)
106+
implementation(libs.firebase.messaging)
106107
testImplementation(libs.junit)
107108
androidTestImplementation(libs.androidx.junit)
108109
androidTestImplementation(libs.androidx.espresso.core)

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
67

78
<application
89
android:name=".ThipApplication"
@@ -47,5 +48,13 @@
4748
<category android:name="android.intent.category.LAUNCHER" />
4849
</intent-filter>
4950
</activity>
51+
52+
<service
53+
android:name=".service.MyFirebaseMessagingService"
54+
android:exported="false">
55+
<intent-filter>
56+
<action android:name="com.google.firebase.MESSAGING_EVENT" />
57+
</intent-filter>
58+
</service>
5059
</application>
5160
</manifest>

app/src/main/java/com/texthip/thip/MainActivity.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
7+
import androidx.activity.result.contract.ActivityResultContracts
78
import androidx.compose.runtime.Composable
89
import androidx.compose.runtime.LaunchedEffect
910
import androidx.navigation.compose.NavHost
@@ -14,6 +15,7 @@ import com.texthip.thip.data.manager.TokenManager
1415
import com.texthip.thip.ui.navigator.navigations.authNavigation
1516
import com.texthip.thip.ui.navigator.routes.CommonRoutes
1617
import com.texthip.thip.ui.theme.ThipTheme
18+
import com.texthip.thip.utils.permission.NotificationPermissionUtils
1719
import dagger.hilt.android.AndroidEntryPoint
1820
import kotlinx.coroutines.flow.collectLatest
1921
import javax.inject.Inject
@@ -22,19 +24,34 @@ import javax.inject.Inject
2224
class MainActivity : ComponentActivity() {
2325
@Inject
2426
lateinit var tokenManager: TokenManager
27+
2528
@Inject
2629
lateinit var authStateManager: AuthStateManager
2730

31+
private val notificationPermissionLauncher = registerForActivityResult(
32+
ActivityResultContracts.RequestPermission()
33+
) {}
34+
2835
override fun onCreate(savedInstanceState: Bundle?) {
2936
super.onCreate(savedInstanceState)
3037
enableEdgeToEdge()
38+
39+
// 앱 시작 시 알림 권한 요청
40+
requestNotificationPermissionIfNeeded()
41+
3142
setContent {
3243
ThipTheme {
3344
RootNavHost(authStateManager)
3445
}
3546
}
3647
// getKakaoKeyHash(this)
3748
}
49+
50+
private fun requestNotificationPermissionIfNeeded() {
51+
if (NotificationPermissionUtils.shouldRequestNotificationPermission(this)) {
52+
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
53+
}
54+
}
3855
}
3956

4057
@Composable
@@ -48,7 +65,7 @@ fun RootNavHost(authStateManager: AuthStateManager) {
4865
}
4966
}
5067
}
51-
68+
5269
NavHost(
5370
navController = navController,
5471
startDestination = CommonRoutes.Splash

app/src/main/java/com/texthip/thip/MainScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ fun MainScreen(
3838
MainTabRoutes.Feed -> {
3939
feedReselectionTrigger += 1
4040
}
41+
4142
else -> {
4243
// 다른 탭들은 향후 확장 가능
4344
}

app/src/main/java/com/texthip/thip/ThipApplication.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import javax.inject.Inject
88

99

1010
@HiltAndroidApp
11-
class ThipApplication : Application(){
11+
class ThipApplication : Application() {
1212
@Inject
1313
lateinit var tokenManager: TokenManager
1414

@@ -18,7 +18,7 @@ class ThipApplication : Application(){
1818
// 카카오 SDK 초기화
1919
try {
2020
KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY)
21-
}catch (e: Exception){
21+
} catch (e: Exception) {
2222
e.printStackTrace()
2323
}
2424
}

app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.texthip.thip.data.service.CommentsService
77
import com.texthip.thip.data.service.FeedService
88
import com.texthip.thip.data.service.RoomsService
99
import com.texthip.thip.data.service.UserService
10+
import com.texthip.thip.data.service.NotificationService
1011
import dagger.Module
1112
import dagger.Provides
1213
import dagger.hilt.InstallIn
@@ -56,4 +57,9 @@ object ServiceModule {
5657
@Singleton
5758
fun provideFeedService(retrofit: Retrofit): FeedService =
5859
retrofit.create(FeedService::class.java)
60+
61+
@Provides
62+
@Singleton
63+
fun provideNotificationService(retrofit: Retrofit): NotificationService =
64+
retrofit.create(NotificationService::class.java)
5965
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.texthip.thip.data.manager
2+
3+
import android.content.Context
4+
import android.util.Log
5+
import androidx.datastore.core.DataStore
6+
import androidx.datastore.preferences.core.Preferences
7+
import androidx.datastore.preferences.core.edit
8+
import androidx.datastore.preferences.core.stringPreferencesKey
9+
import com.google.firebase.messaging.FirebaseMessaging
10+
import com.texthip.thip.data.repository.NotificationRepository
11+
import com.texthip.thip.utils.auth.getAppScopeDeviceId
12+
import com.texthip.thip.utils.permission.NotificationPermissionUtils
13+
import dagger.hilt.android.qualifiers.ApplicationContext
14+
import kotlinx.coroutines.flow.first
15+
import kotlinx.coroutines.flow.map
16+
import kotlinx.coroutines.suspendCancellableCoroutine
17+
import kotlin.coroutines.resume
18+
import kotlin.coroutines.resumeWithException
19+
import javax.inject.Inject
20+
import javax.inject.Singleton
21+
22+
@Singleton
23+
class FcmTokenManager @Inject constructor(
24+
private val dataStore: DataStore<Preferences>,
25+
private val notificationRepository: NotificationRepository,
26+
@param:ApplicationContext private val context: Context
27+
) {
28+
29+
companion object {
30+
private val FCM_TOKEN_KEY = stringPreferencesKey("fcm_token")
31+
}
32+
33+
suspend fun handleNewToken(newToken: String) {
34+
val storedToken = getFcmTokenOnce()
35+
36+
if (storedToken != newToken) {
37+
Log.d("FCM", "Token updated")
38+
39+
saveFcmToken(newToken)
40+
sendTokenToServer(newToken)
41+
}
42+
}
43+
44+
suspend fun sendCurrentTokenIfExists() {
45+
val storedFcmToken = getFcmTokenOnce()
46+
47+
if (storedFcmToken != null) {
48+
sendTokenToServer(storedFcmToken)
49+
} else {
50+
// 저장된 토큰이 없으면 Firebase에서 직접 가져와서 저장하고 전송
51+
try {
52+
val token = fetchCurrentToken()
53+
saveFcmToken(token)
54+
sendTokenToServer(token)
55+
} catch (e: Exception) {
56+
Log.e("FCM", "Failed to fetch and send current token", e)
57+
}
58+
}
59+
}
60+
61+
private suspend fun fetchCurrentToken(): String = suspendCancellableCoroutine { continuation ->
62+
try {
63+
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
64+
when {
65+
task.isSuccessful -> {
66+
val token = task.result
67+
if (token != null) {
68+
continuation.resume(token)
69+
} else {
70+
continuation.resumeWithException(IllegalStateException("FCM token is null"))
71+
}
72+
}
73+
else -> {
74+
val exception = task.exception ?: Exception("Unknown error fetching FCM token")
75+
Log.w("FCM", "Failed to fetch token", exception)
76+
continuation.resumeWithException(exception)
77+
}
78+
}
79+
}
80+
} catch (e: Exception) {
81+
Log.e("FCM", "Error fetching FCM token", e)
82+
continuation.resumeWithException(e)
83+
}
84+
}
85+
86+
// FCM 토큰 로컬 저장 관리
87+
private suspend fun saveFcmToken(token: String) {
88+
dataStore.edit { prefs -> prefs[FCM_TOKEN_KEY] = token }
89+
}
90+
91+
private suspend fun getFcmTokenOnce(): String? {
92+
return dataStore.data.map { prefs -> prefs[FCM_TOKEN_KEY] }.first()
93+
}
94+
95+
private suspend fun sendTokenToServer(token: String) {
96+
// 알림 권한이 없으면 토큰을 서버에 전송하지 않음
97+
if (!NotificationPermissionUtils.isNotificationPermissionGranted(context)) {
98+
Log.w("FCM", "Notification permission not granted, skipping token registration")
99+
return
100+
}
101+
102+
val deviceId = context.getAppScopeDeviceId()
103+
notificationRepository.registerFcmToken(deviceId, token)
104+
.onSuccess {
105+
Log.d("FCM", "Token sent successfully")
106+
}
107+
.onFailure { exception ->
108+
Log.e("FCM", "Failed to send token", exception)
109+
}
110+
}
111+
}

app/src/main/java/com/texthip/thip/data/manager/Genre.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.texthip.thip.data.manager
22

3-
/**
4-
* 도서 장르를 나타내는 enum class
5-
*/
3+
64
enum class Genre(
75
val displayKey: String,
86
val apiCategory: String,

app/src/main/java/com/texthip/thip/data/manager/TokenManager.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class TokenManager @Inject constructor(
1818
companion object {
1919
private val APP_TOKEN_KEY = stringPreferencesKey("app_token") // 정식 액세스토큰
2020
private val TEMP_TOKEN_KEY = stringPreferencesKey("temp_token") // 임시 토큰
21-
private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
21+
//private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
2222
}
2323

2424
// ====== 정식 토큰 ======
@@ -54,13 +54,13 @@ class TokenManager @Inject constructor(
5454
}
5555

5656
// ====== Refresh 토큰 (추후 확장용) ======
57-
suspend fun saveRefreshToken(token: String) {
57+
/*suspend fun saveRefreshToken(token: String) {
5858
dataStore.edit { prefs -> prefs[REFRESH_TOKEN_KEY] = token }
5959
}
6060
6161
suspend fun getRefreshTokenOnce(): String? {
6262
return dataStore.data.map { prefs -> prefs[REFRESH_TOKEN_KEY] }.first()
63-
}
63+
}*/
6464

6565
suspend fun clearTokens() {
6666
dataStore.edit { prefs -> prefs.clear() }

0 commit comments

Comments
 (0)