Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f909028
[feat]: 알림센터 조회 Response 구현 (#140)
rbqks529 Sep 23, 2025
3fda293
[feat]: 알림센터 조회 Type enum class 구현 (#140)
rbqks529 Sep 23, 2025
9d1e3bc
[ui]: 알림 필터 row 수정 (#140)
rbqks529 Sep 23, 2025
18079db
[feat]: 알림센터용 UiState 구현 (#140)
rbqks529 Sep 23, 2025
d140dfb
[feat]: 알림센터 조회 API Service, Repository 구현 (#140)
rbqks529 Sep 23, 2025
bdbf183
[feat]: 알림센터 조회 viewModel 수정 (#140)
rbqks529 Sep 23, 2025
32376cd
[feat]: 알림센터 조회 Screen 구현 (#140)
rbqks529 Sep 23, 2025
0c7a691
[feat]: Common 네비게이션 수정 (#140)
rbqks529 Sep 23, 2025
c91ad6a
[feat]: 알림센터 기준으로 탑바 알림 아이콘 수정 (#140)
rbqks529 Sep 23, 2025
45f541c
[refactor]: branch 최신화 (#140)
rbqks529 Sep 24, 2025
42f0b01
[feat]: 알림 읽기 API Request, Response 구현 (#140)
rbqks529 Sep 25, 2025
7580e37
[feat]: 알림 읽기 API Notification, Service 구현 (#140)
rbqks529 Sep 25, 2025
613a333
[feat]: 알림 읽기 viewModel, UiState 수정 및 구현 (#140)
rbqks529 Sep 25, 2025
ee3788c
[feat]: 알림 Screen에 로직 연결 (#140)
rbqks529 Sep 25, 2025
cb9b2a5
[feat]: 알림 네비게이션 확장 함수 생성 (#140)
rbqks529 Sep 25, 2025
f7bd19d
[feat]: 알림 네비게이션에 맞게 기존 네비게이션 파일 수정 (#140)
rbqks529 Sep 25, 2025
28a0187
[feat]: 푸시알림 네비게이션을 위한 MainActivity와 MainScreen 수정 (#140)
rbqks529 Sep 25, 2025
4d53c6c
[feat]: 알림을 읽고 화면으로 왔을때 알림 아이콘 수정을 위한 로직 추가 (#140)
rbqks529 Sep 25, 2025
c1a877c
[feat]: 해당 포스트로 스크롤과 댓글창 열기 로직을 위한 GroupNote 수정 (#140)
rbqks529 Sep 25, 2025
e74142c
[feat]: AlarmScreen의 Navigation 콜백 함수 추가(#140)
rbqks529 Sep 25, 2025
3ed58f2
[feat]: 매니페스트 수정 (#140)
rbqks529 Sep 25, 2025
e0f2111
[feat]: 푸시알림 허용에서 알림 권한이 없다면 권한을 다시 요청하도록 수정 (#140)
rbqks529 Sep 25, 2025
2bd7010
[feat]: 알림 권한이 없어도 FCM토큰을 전송하도록 수정 (#140)
rbqks529 Sep 25, 2025
5864f8b
[refactor]: emit에서 tryEmit으로 수정 (#140)
rbqks529 Sep 25, 2025
0d9f94a
[refactor]: LaunchedEffect 키에fromNotification 포함되게 수정 (#140)
rbqks529 Sep 25, 2025
801e8ba
[refactor]: String 추출 및 PR 반영 (#140)
rbqks529 Sep 25, 2025
bf47fe1
[refactor]: 파라미터 호출 순서 문제 수정 (#140)
rbqks529 Sep 26, 2025
249c517
[refactor]: 코드래빗 리뷰에 맞게 뷰모델 수정 (#140)
rbqks529 Sep 26, 2025
c6f40f7
[chore]: 주석 수정 (#140)
rbqks529 Sep 26, 2025
687c179
[feat]: 피드, 그룹 화면 재진입시 알림 아이콘 초기화화 (#140)
rbqks529 Sep 26, 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
17 changes: 14 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@
android:name="com.kakao.sdk.AppKey"
android:value="${NATIVE_APP_KEY}" />

<!-- FCM 기본 설정 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@mipmap/ic_launcher" />

<!-- FCM 기본 클릭 액션 설정 -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="thip_notifications" />
Comment on lines +25 to +33
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

FCM 기본 알림 아이콘은 단색 drawable로 지정

mipmap/ic_launcher 대신 단색 drawable을 사용해야 합니다. 상태바/티커 표시 품질을 위해 교체해 주세요.

         <meta-data
             android:name="com.google.firebase.messaging.default_notification_icon"
-            android:resource="@mipmap/ic_launcher" />
+            android:resource="@drawable/ic_notification_small" />

ic_notification_small 리소스가 없다면 생성이 필요합니다.

🤖 Prompt for AI Agents
app/src/main/AndroidManifest.xml lines 25-33: the FCM default notification icon
currently points to mipmap/ic_launcher which is not a single-color drawable;
replace the meta-data android:resource to reference a single-color drawable
(e.g., @drawable/ic_notification_small) and if ic_notification_small does not
exist, add a drawable resource (vector or shape) with a single solid foreground
color and transparent background suitable for status bar rendering, then update
resource name in the manifest to that drawable.


<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
Expand All @@ -39,20 +49,21 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize"
android:label="@string/app_name"
android:theme="@style/Theme.Thip">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".service.MyFirebaseMessagingService"
android:exported="false">
<intent-filter>
android:exported="false"
android:directBootAware="true">
<intent-filter android:priority="1000">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Expand Down
95 changes: 90 additions & 5 deletions app/src/main/java/com/texthip/thip/MainActivity.kt
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

클로드의 도움을 받아서 백그라운드 푸시알림일 때 인텐트로 notificationId를 넘기도록 수정했습니다.

Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.texthip.thip

import android.content.Intent
import android.os.Bundle
import android.util.Log
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.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
Expand All @@ -32,21 +37,96 @@ class MainActivity : ComponentActivity() {
ActivityResultContracts.RequestPermission()
) {}

private var notificationData by mutableStateOf<NotificationData?>(null)

data class NotificationData(
val notificationId: String?,
val fromNotification: Boolean
)

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

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


// 푸시 알림에서 온 데이터 처리
handleNotificationIntent(intent)

setContent {
ThipTheme {
RootNavHost(authStateManager)
RootNavHost(
authStateManager = authStateManager,
notificationData = notificationData
)
}
}
// getKakaoKeyHash(this)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)

// 새로운 Intent가 들어올 때 (백그라운드에서 알림 클릭 시)
handleNotificationIntent(intent)
}

private fun handleNotificationIntent(intent: Intent) {
Log.d("MainActivity", "Handling notification intent with extras: ${intent.extras?.keySet()}")

val customNotificationId = intent.getStringExtra("notification_id")
val customFromNotification = intent.getBooleanExtra("from_notification", false)

// FCM 백그라운드 알림에서 온 데이터 확인 (시스템이 자동 생성한 알림의 경우)
val fcmNotificationId = intent.getStringExtra("gcm.notification.data.notificationId")
?: intent.getStringExtra("notificationId")

var newNotificationData: NotificationData? = null

// 커스텀 알림에서 온 경우 (포그라운드에서 생성된 알림)
if (customFromNotification && customNotificationId != null) {
Log.d("MainActivity", "Processing custom notification: $customNotificationId")
newNotificationData = NotificationData(customNotificationId, customFromNotification)

// Intent extras 완전 제거
cleanupNotificationExtras(intent, listOf("notification_id", "from_notification"))
}
// FCM 백그라운드 시스템 알림에서 온 경우
else if (fcmNotificationId != null) {
Log.d("MainActivity", "Processing FCM notification: $fcmNotificationId")
newNotificationData = NotificationData(fcmNotificationId, true)

// Intent extras 완전 제거
cleanupNotificationExtras(intent, listOf(
"gcm.notification.data.notificationId",
"notificationId"
))
}

// 새로운 알림 데이터가 있고, 기존 데이터와 다른 경우에만 업데이트
if (newNotificationData != null && newNotificationData != notificationData) {
Log.d("MainActivity", "Setting new notification data: ${newNotificationData.notificationId}")
notificationData = newNotificationData
} else if (newNotificationData != null) {
Log.d("MainActivity", "Notification data unchanged, skipping update")
}
}

private fun cleanupNotificationExtras(intent: Intent, keys: List<String>) {
keys.forEach { key ->
try {
intent.removeExtra(key)
Log.v("MainActivity", "Removed extra: $key")
} catch (e: Exception) {
Log.w("MainActivity", "Failed to remove extra: $key", e)
}
}

// Intent 플래그도 정리
intent.replaceExtras(intent.extras)
}

private fun requestNotificationPermissionIfNeeded() {
if (NotificationPermissionUtils.shouldRequestNotificationPermission(this)) {
notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
Expand All @@ -55,7 +135,10 @@ class MainActivity : ComponentActivity() {
}

@Composable
fun RootNavHost(authStateManager: AuthStateManager) {
fun RootNavHost(
authStateManager: AuthStateManager,
notificationData: MainActivity.NotificationData? = null
) {
val navController = rememberNavController()

LaunchedEffect(Unit) {
Expand All @@ -66,6 +149,7 @@ fun RootNavHost(authStateManager: AuthStateManager) {
}
}


NavHost(
navController = navController,
startDestination = CommonRoutes.Splash
Expand Down Expand Up @@ -104,7 +188,8 @@ fun RootNavHost(authStateManager: AuthStateManager) {
inclusive = true
}
}
}
},
notificationData = notificationData
)
}
}
Expand Down
68 changes: 67 additions & 1 deletion app/src/main/java/com/texthip/thip/MainScreen.kt
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

MainActivity에서 전달받은 알림 데이터를 네비게이션으로 연결하는 로직을 구현했습니다

Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,94 @@ package com.texthip.thip
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.texthip.thip.data.repository.NotificationRepository
import com.texthip.thip.ui.navigator.BottomNavigationBar
import com.texthip.thip.ui.navigator.MainNavHost
import com.texthip.thip.ui.navigator.extensions.isMainTabRoute
import com.texthip.thip.ui.navigator.extensions.navigateFromNotification
import com.texthip.thip.ui.navigator.routes.MainTabRoutes
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MainScreenEntryPoint {
fun notificationRepository(): NotificationRepository
}

@Composable
fun MainScreen(
onNavigateToLogin: () -> Unit
onNavigateToLogin: () -> Unit,
notificationData: MainActivity.NotificationData? = null
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
var feedReselectionTrigger by remember { mutableStateOf(0) }
val context = LocalContext.current

// 처리된 알림 ID 추적
var processedNotificationId by remember { mutableStateOf<String?>(null) }

// 푸시 알림에서 온 경우 알림 읽기 API 호출 및 네비게이션
LaunchedEffect(notificationData?.notificationId, notificationData?.fromNotification) {
val data = notificationData

// 중복 처리 방지
if (data?.notificationId == processedNotificationId) {
return@LaunchedEffect
}

data?.let { notificationData ->
if (notificationData.fromNotification && notificationData.notificationId != null) {
try {
val entryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
MainScreenEntryPoint::class.java
)
val notificationRepository = entryPoint.notificationRepository()

val notificationId = try {
notificationData.notificationId.toInt()
} catch (e: NumberFormatException) {
Log.e("MainScreen", "Invalid notification ID format: ${notificationData.notificationId}", e)
return@LaunchedEffect
}

val result = notificationRepository.checkNotification(notificationId)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

여기서 알림읽기 요청


result.onSuccess { response ->
if (response != null) {
navController.navigateFromNotification(response)
notificationRepository.onNotificationReceived()
processedNotificationId = notificationData.notificationId
} else {
Log.w("MainScreen", "Notification check returned null response")
}
}.onFailure { exception ->
Log.e("MainScreen", "Failed to check notification: ${notificationData.notificationId}", exception)
}

} catch (e: Exception) {
Log.e("MainScreen", "Unexpected error processing notification: ${notificationData.notificationId}", e)
}
}
}
}

val showBottomBar = currentDestination?.isMainTabRoute() ?: true

Expand Down
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

일단 권한이 없어도 전송을 해야됨

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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
Expand Down Expand Up @@ -93,12 +92,6 @@ class FcmTokenManager @Inject constructor(
}

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 {
Expand Down
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.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class NotificationCheckRequest(
@SerialName("notificationId") val notificationId: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.texthip.thip.data.model.notification.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement

@Serializable
data class NotificationCheckResponse(
@SerialName("route") val route: NotificationRoute,
@SerialName("params") val params: Map<String, JsonElement>
)

@Serializable
enum class NotificationRoute {
@SerialName("FEED_USER")
FEED_USER,

@SerialName("FEED_DETAIL")
FEED_DETAIL,

@SerialName("ROOM_MAIN")
ROOM_MAIN,

@SerialName("ROOM_DETAIL")
ROOM_DETAIL,

@SerialName("ROOM_POST_DETAIL")
ROOM_POST_DETAIL
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.texthip.thip.data.model.notification.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class NotificationListResponse(
@SerialName("notifications") val notifications: List<NotificationResponse>,
@SerialName("nextCursor") val nextCursor: String?,
@SerialName("isLast") val isLast: Boolean
)

@Serializable
data class NotificationResponse(
@SerialName("notificationId") val notificationId: Int,
@SerialName("title") val title: String,
@SerialName("content") val content: String,
@SerialName("isChecked") val isChecked: Boolean,
@SerialName("notificationType") val notificationType: String,
@SerialName("postDate") val postDate: String
)
Loading