Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d627f7f
[feat]: Firebase Cloud Messaging 추가 및 설정 (#135)
rbqks529 Sep 11, 2025
381ed36
[feat]: Firebase Cloud Messaging 서비스 구현 (일단 알림을 탭하면 메인 엑티비티로 이동) (#135)
rbqks529 Sep 11, 2025
7300b20
[feat]: FCM 토큰 등록 request 구현 (#131)
rbqks529 Sep 11, 2025
0230ab9
[feat]: FCM 토큰 등록 service, repository 구현 (#135)
rbqks529 Sep 11, 2025
8db0e02
[feat]: FCM 토큰 관리 매니저 구현 (#135)
rbqks529 Sep 11, 2025
8539a5d
[feat]: FCM 토큰 관리 매니저 수정 (#135)
rbqks529 Sep 11, 2025
e8bfcc7
[feat]: FCM 토큰 관리 매니저 수정 (#135)
rbqks529 Sep 11, 2025
66e1319
[feat]: FCM 토큰 등록 API 화면 연결 (#135)
rbqks529 Sep 11, 2025
b8be3b7
[feat]: 푸시 알림 수정 페이지 날짜 로직 수정 (#135)
rbqks529 Sep 11, 2025
7bde62e
[feat]: FCM 토큰 관리 매니저 중복 로직 삭제 (#135)
rbqks529 Sep 11, 2025
8ce347d
[feat]: Firebase 서비스 위치 변경 (#135)
rbqks529 Sep 12, 2025
392b15f
[chore]: 주석 변경 및 줄 바꿈 수정 (#135)
rbqks529 Sep 12, 2025
e206951
[refactor]: 회원 탈퇴 문구 수정 (#135)
rbqks529 Sep 14, 2025
530aeba
[refactor]: 내 띱 목록이 없을때 화면 컴포넌트 패딩 수정 (#135)
rbqks529 Sep 14, 2025
513e726
[feat]: 사용자 푸시알림 수신여부 조회 API Response, Service, Repository 구현 (#135)
rbqks529 Sep 14, 2025
256fc22
[feat]: 사용자 푸시알림 수신여부 조회 API ViewModel 구현 (#135)
rbqks529 Sep 14, 2025
28404be
[feat]: 사용자 푸시알림 수신여부 조회 API 화면 연결 (#135)
rbqks529 Sep 14, 2025
bcbccd2
[refactor]: DeviceId 확장 확장 함수 구현 및 기존 코드 변경 (#135)
rbqks529 Sep 14, 2025
057ea94
[feat]: 푸시 알림 수신 여부 설정 DTO 구현 및 기존 Response와 통일 (#135)
rbqks529 Sep 14, 2025
a179a34
[feat]: 푸시 알림 수신 여부 설정 service, repository 구현 (#135)
rbqks529 Sep 14, 2025
c132fbd
[feat]: 푸시 알림 수신 여부 설정 viewModel 구현 및 API 구현 완료 (#135)
rbqks529 Sep 14, 2025
eb78a97
[feat]: 푸시 알림 화면 content로 분리 완료 (#135)
rbqks529 Sep 14, 2025
9bde90d
[feat]: 알림 권한 요청 로직 추가 (#135)
rbqks529 Sep 14, 2025
50cda6e
[feat]: Context 생성자 주입 로직 번경 (#135)
rbqks529 Sep 14, 2025
02cc000
[feat]: Firebase 토큰 가져오기 로직 수정(코드 래빗) (#135)
rbqks529 Sep 14, 2025
5a9a01b
[feat]: 피드 작성 시 이미지 올리기 로직 수정 (#135)
rbqks529 Sep 14, 2025
4cc7b3b
[refactor]: 검색 필드 수정 (#135)
rbqks529 Sep 14, 2025
9009d24
[refactor]: registerFcmToken 수정 (#135)
rbqks529 Sep 14, 2025
4cf17db
[refactor]: DeviceId를 코드 리뷰에 맞게 수정 (#135)
rbqks529 Sep 14, 2025
9f23504
[feat]: FCM 토큰 삭제 Request, Service, Repository 구현 (#135)
rbqks529 Sep 14, 2025
02fda02
[feat]: 디바이스 관련 정보 삭제 함수 추가 (#135)
rbqks529 Sep 14, 2025
f578aef
[feat]: 회원 탈퇴시 FCM 토큰 삭제 및 디바이스 정보를 삭제하도록 수정 (#135)
rbqks529 Sep 14, 2025
1dcbf42
[feat]: 회원 탈퇴시 FCM 토큰 삭제 및 디바이스 정보를 삭제하도록 수정 (#135)
rbqks529 Sep 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .idea/appInsightsSettings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ dependencies {
implementation(libs.foundation)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.firebase.messaging)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".ThipApplication"
Expand Down Expand Up @@ -47,5 +48,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".service.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
19 changes: 18 additions & 1 deletion app/src/main/java/com/texthip/thip/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.navigation.compose.NavHost
Expand All @@ -14,6 +15,7 @@ import com.texthip.thip.data.manager.TokenManager
import com.texthip.thip.ui.navigator.navigations.authNavigation
import com.texthip.thip.ui.navigator.routes.CommonRoutes
import com.texthip.thip.ui.theme.ThipTheme
import com.texthip.thip.utils.permission.NotificationPermissionUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
Expand All @@ -22,19 +24,34 @@ import javax.inject.Inject
class MainActivity : ComponentActivity() {
@Inject
lateinit var tokenManager: TokenManager

@Inject
lateinit var authStateManager: AuthStateManager

private val notificationPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) {}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()

// 앱 시작 시 알림 권한 요청
requestNotificationPermissionIfNeeded()

setContent {
ThipTheme {
RootNavHost(authStateManager)
}
}
// getKakaoKeyHash(this)
}

private fun requestNotificationPermissionIfNeeded() {
if (NotificationPermissionUtils.shouldRequestNotificationPermission(this)) {
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
}
}
}

@Composable
Expand All @@ -48,7 +65,7 @@ fun RootNavHost(authStateManager: AuthStateManager) {
}
}
}

NavHost(
navController = navController,
startDestination = CommonRoutes.Splash
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/texthip/thip/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fun MainScreen(
MainTabRoutes.Feed -> {
feedReselectionTrigger += 1
}

else -> {
// 다른 탭들은 향후 확장 가능
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/texthip/thip/ThipApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import javax.inject.Inject


@HiltAndroidApp
class ThipApplication : Application(){
class ThipApplication : Application() {
@Inject
lateinit var tokenManager: TokenManager

Expand All @@ -18,7 +18,7 @@ class ThipApplication : Application(){
// 카카오 SDK 초기화
try {
KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY)
}catch (e: Exception){
} catch (e: Exception) {
e.printStackTrace()
}
}
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/com/texthip/thip/data/di/ServiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.texthip.thip.data.service.CommentsService
import com.texthip.thip.data.service.FeedService
import com.texthip.thip.data.service.RoomsService
import com.texthip.thip.data.service.UserService
import com.texthip.thip.data.service.NotificationService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -56,4 +57,9 @@ object ServiceModule {
@Singleton
fun provideFeedService(retrofit: Retrofit): FeedService =
retrofit.create(FeedService::class.java)

@Provides
@Singleton
fun provideNotificationService(retrofit: Retrofit): NotificationService =
retrofit.create(NotificationService::class.java)
}
111 changes: 111 additions & 0 deletions app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.texthip.thip.data.manager

import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.google.firebase.messaging.FirebaseMessaging
import com.texthip.thip.data.repository.NotificationRepository
import com.texthip.thip.utils.auth.getAppScopeDeviceId
import com.texthip.thip.utils.permission.NotificationPermissionUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class FcmTokenManager @Inject constructor(
private val dataStore: DataStore<Preferences>,
private val notificationRepository: NotificationRepository,
@param:ApplicationContext private val context: Context
) {

companion object {
private val FCM_TOKEN_KEY = stringPreferencesKey("fcm_token")
}

suspend fun handleNewToken(newToken: String) {
val storedToken = getFcmTokenOnce()

if (storedToken != newToken) {
Log.d("FCM", "Token updated")

saveFcmToken(newToken)
sendTokenToServer(newToken)
}
}

suspend fun sendCurrentTokenIfExists() {
val storedFcmToken = getFcmTokenOnce()

if (storedFcmToken != null) {
sendTokenToServer(storedFcmToken)
} else {
// 저장된 토큰이 없으면 Firebase에서 직접 가져와서 저장하고 전송
try {
val token = fetchCurrentToken()
saveFcmToken(token)
sendTokenToServer(token)
} catch (e: Exception) {
Log.e("FCM", "Failed to fetch and send current token", e)
}
}
}

private suspend fun fetchCurrentToken(): String = suspendCancellableCoroutine { continuation ->
try {
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
when {
task.isSuccessful -> {
val token = task.result
if (token != null) {
continuation.resume(token)
} else {
continuation.resumeWithException(IllegalStateException("FCM token is null"))
}
}
else -> {
val exception = task.exception ?: Exception("Unknown error fetching FCM token")
Log.w("FCM", "Failed to fetch token", exception)
continuation.resumeWithException(exception)
}
}
}
} catch (e: Exception) {
Log.e("FCM", "Error fetching FCM token", e)
continuation.resumeWithException(e)
}
}

// FCM 토큰 로컬 저장 관리
private suspend fun saveFcmToken(token: String) {
dataStore.edit { prefs -> prefs[FCM_TOKEN_KEY] = token }
}

private suspend fun getFcmTokenOnce(): String? {
return dataStore.data.map { prefs -> prefs[FCM_TOKEN_KEY] }.first()
}

private suspend fun sendTokenToServer(token: String) {
// 알림 권한이 없으면 토큰을 서버에 전송하지 않음
if (!NotificationPermissionUtils.isNotificationPermissionGranted(context)) {
Log.w("FCM", "Notification permission not granted, skipping token registration")
return
}

val deviceId = context.getAppScopeDeviceId()
notificationRepository.registerFcmToken(deviceId, token)
.onSuccess {
Log.d("FCM", "Token sent successfully")
}
.onFailure { exception ->
Log.e("FCM", "Failed to send token", exception)
}
}
}
4 changes: 1 addition & 3 deletions app/src/main/java/com/texthip/thip/data/manager/Genre.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.texthip.thip.data.manager

/**
* 도서 장르를 나타내는 enum class
*/

Copy link

Copilot AI Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The multi-line comment explaining the enum class was removed. Consider keeping documentation for public APIs and data models to maintain code clarity.

Suggested change
/**
* Represents the genre category for content in the application.
*
* @property displayKey The key used for display and identification.
* @property apiCategory The category name used in the API.
* @property networkApiCategory The category name used for network API calls (defaults to [apiCategory]).
*
* Enum values:
* - LITERATURE: Literature genre.
* - SCIENCE_IT: Science and IT genre.
* - SOCIAL_SCIENCE: Social science genre.
* - HUMANITIES: Humanities genre.
* - ART: Art genre.
*/

Copilot uses AI. Check for mistakes.
enum class Genre(
val displayKey: String,
val apiCategory: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TokenManager @Inject constructor(
companion object {
private val APP_TOKEN_KEY = stringPreferencesKey("app_token") // 정식 액세스토큰
private val TEMP_TOKEN_KEY = stringPreferencesKey("temp_token") // 임시 토큰
private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
//private val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
Copy link

Copilot AI Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of commenting out the refresh token functionality, consider removing it entirely if it's not being used, or add a TODO comment explaining when it will be implemented.

Copilot uses AI. Check for mistakes.
}

// ====== 정식 토큰 ======
Expand Down Expand Up @@ -54,13 +54,13 @@ class TokenManager @Inject constructor(
}

// ====== Refresh 토큰 (추후 확장용) ======
suspend fun saveRefreshToken(token: String) {
/*suspend fun saveRefreshToken(token: String) {
dataStore.edit { prefs -> prefs[REFRESH_TOKEN_KEY] = token }
}

suspend fun getRefreshTokenOnce(): String? {
return dataStore.data.map { prefs -> prefs[REFRESH_TOKEN_KEY] }.first()
}
}*/

suspend fun clearTokens() {
dataStore.edit { prefs -> prefs.clear() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.texthip.thip.data.model.notification.request

import kotlinx.serialization.Serializable

@Serializable
data class FcmTokenDeleteRequest(
val deviceId: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.texthip.thip.data.model.notification.request

import kotlinx.serialization.Serializable

@Serializable
data class FcmTokenRequest(
val deviceId: String,
val fcmToken: String,
val platformType: String = "ANDROID"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.texthip.thip.data.model.notification.request

import kotlinx.serialization.Serializable

@Serializable
data class NotificationEnabledRequest(
val enable: Boolean,
val deviceId: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.texthip.thip.data.model.notification.response

import kotlinx.serialization.Serializable

@Serializable
data class NotificationEnabledResponse(
val isEnabled: Boolean
)
Loading