diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 774bc0ad..af348c36 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,16 @@
android:name="com.kakao.sdk.AppKey"
android:value="${NATIVE_APP_KEY}" />
+
+
+
+
+
+
@@ -39,20 +49,21 @@
-
-
+ android:exported="false"
+ android:directBootAware="true">
+
diff --git a/app/src/main/java/com/texthip/thip/MainActivity.kt b/app/src/main/java/com/texthip/thip/MainActivity.kt
index 3d86934d..83d1f2e6 100644
--- a/app/src/main/java/com/texthip/thip/MainActivity.kt
+++ b/app/src/main/java/com/texthip/thip/MainActivity.kt
@@ -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
@@ -32,21 +37,96 @@ class MainActivity : ComponentActivity() {
ActivityResultContracts.RequestPermission()
) {}
+ private var notificationData by mutableStateOf(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) {
+ 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)
@@ -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) {
@@ -66,6 +149,7 @@ fun RootNavHost(authStateManager: AuthStateManager) {
}
}
+
NavHost(
navController = navController,
startDestination = CommonRoutes.Splash
@@ -104,7 +188,8 @@ fun RootNavHost(authStateManager: AuthStateManager) {
inclusive = true
}
}
- }
+ },
+ notificationData = notificationData
)
}
}
diff --git a/app/src/main/java/com/texthip/thip/MainScreen.kt b/app/src/main/java/com/texthip/thip/MainScreen.kt
index 77084060..d0239481 100644
--- a/app/src/main/java/com/texthip/thip/MainScreen.kt
+++ b/app/src/main/java/com/texthip/thip/MainScreen.kt
@@ -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(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)
+
+ 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
diff --git a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt
index f2ecf52d..58afb36b 100644
--- a/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt
+++ b/app/src/main/java/com/texthip/thip/data/manager/FcmTokenManager.kt
@@ -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
@@ -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 {
diff --git a/app/src/main/java/com/texthip/thip/data/model/notification/request/NotificationCheckRequest.kt b/app/src/main/java/com/texthip/thip/data/model/notification/request/NotificationCheckRequest.kt
new file mode 100644
index 00000000..d22e02f0
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/data/model/notification/request/NotificationCheckRequest.kt
@@ -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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationCheckResponse.kt b/app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationCheckResponse.kt
new file mode 100644
index 00000000..fd6c546b
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationCheckResponse.kt
@@ -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
+)
+
+@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
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationListResponse.kt b/app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationListResponse.kt
new file mode 100644
index 00000000..b4133a98
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/data/model/notification/response/NotificationListResponse.kt
@@ -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,
+ @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
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt
index bdb8b381..e9669fd2 100644
--- a/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt
+++ b/app/src/main/java/com/texthip/thip/data/repository/NotificationRepository.kt
@@ -5,10 +5,17 @@ import com.texthip.thip.data.model.base.handleBaseResponse
import com.texthip.thip.data.model.notification.request.FcmTokenRequest
import com.texthip.thip.data.model.notification.request.FcmTokenDeleteRequest
import com.texthip.thip.data.model.notification.request.NotificationEnabledRequest
+import com.texthip.thip.data.model.notification.request.NotificationCheckRequest
import com.texthip.thip.data.model.notification.response.NotificationEnabledResponse
+import com.texthip.thip.data.model.notification.response.NotificationListResponse
+import com.texthip.thip.data.model.notification.response.NotificationCheckResponse
import com.texthip.thip.data.service.NotificationService
import com.texthip.thip.utils.auth.getAppScopeDeviceId
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@@ -17,6 +24,19 @@ class NotificationRepository @Inject constructor(
private val notificationService: NotificationService,
@param:ApplicationContext private val context: Context
) {
+ private val _notificationUpdateFlow = MutableSharedFlow(
+ replay = 0,
+ extraBufferCapacity = 64,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val notificationUpdateFlow: SharedFlow = _notificationUpdateFlow.asSharedFlow()
+
+ private val _notificationRefreshFlow = MutableSharedFlow(
+ replay = 0,
+ extraBufferCapacity = 64,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ val notificationRefreshFlow: SharedFlow = _notificationRefreshFlow.asSharedFlow()
suspend fun registerFcmToken(
deviceId: String,
fcmToken: String
@@ -31,7 +51,7 @@ class NotificationRepository @Inject constructor(
response.handleBaseResponse().getOrNull()
}
}
-
+
suspend fun getNotificationEnableState(): Result {
return runCatching {
val deviceId = context.getAppScopeDeviceId()
@@ -39,7 +59,7 @@ class NotificationRepository @Inject constructor(
response.handleBaseResponse().getOrNull()
}
}
-
+
suspend fun updateNotificationEnabled(enabled: Boolean): Result {
return runCatching {
val deviceId = context.getAppScopeDeviceId()
@@ -51,7 +71,7 @@ class NotificationRepository @Inject constructor(
response.handleBaseResponse().getOrNull()
}
}
-
+
suspend fun deleteFcmToken(): Result {
return runCatching {
val deviceId = context.getAppScopeDeviceId()
@@ -60,4 +80,32 @@ class NotificationRepository @Inject constructor(
response.handleBaseResponse().getOrNull()
}
}
+
+ suspend fun getNotifications(
+ cursor: String? = null,
+ type: String? = null
+ ): Result {
+ return runCatching {
+ val response = notificationService.getNotifications(cursor, type)
+ response.handleBaseResponse().getOrNull()
+ }
+ }
+
+ suspend fun checkNotification(notificationId: Int): Result {
+ return runCatching {
+ val request = NotificationCheckRequest(notificationId = notificationId)
+ val response = notificationService.checkNotification(request)
+ val result = response.handleBaseResponse().getOrNull()
+
+ // 알림 읽기 성공 시 다른 ViewModel들에게 알림 (비차단 emit)
+ if (result != null) {
+ _notificationUpdateFlow.tryEmit(notificationId)
+ }
+ result
+ }
+ }
+
+ fun onNotificationReceived() {
+ _notificationRefreshFlow.tryEmit(Unit)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt b/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt
index 3447316d..db0575ef 100644
--- a/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt
+++ b/app/src/main/java/com/texthip/thip/data/service/NotificationService.kt
@@ -3,7 +3,10 @@ package com.texthip.thip.data.service
import com.texthip.thip.data.model.notification.request.FcmTokenRequest
import com.texthip.thip.data.model.notification.request.FcmTokenDeleteRequest
import com.texthip.thip.data.model.notification.request.NotificationEnabledRequest
+import com.texthip.thip.data.model.notification.request.NotificationCheckRequest
import com.texthip.thip.data.model.notification.response.NotificationEnabledResponse
+import com.texthip.thip.data.model.notification.response.NotificationListResponse
+import com.texthip.thip.data.model.notification.response.NotificationCheckResponse
import com.texthip.thip.data.model.base.BaseResponse
import retrofit2.http.Body
import retrofit2.http.DELETE
@@ -17,19 +20,30 @@ interface NotificationService {
suspend fun registerFcmToken(
@Body request: FcmTokenRequest
): BaseResponse
-
+
@GET("users/notification-settings")
suspend fun getNotificationEnableState(
@Query("deviceId") deviceId: String
): BaseResponse
-
+
@PATCH("notifications/enable-state")
suspend fun updateNotificationEnabled(
@Body request: NotificationEnabledRequest
): BaseResponse
-
+
@DELETE("notifications/fcm-tokens")
suspend fun deleteFcmToken(
@Body request: FcmTokenDeleteRequest
): BaseResponse
+
+ @GET("notifications")
+ suspend fun getNotifications(
+ @Query("cursor") cursor: String? = null,
+ @Query("type") type: String? = null
+ ): BaseResponse
+
+ @POST("notifications/check")
+ suspend fun checkNotification(
+ @Body request: NotificationCheckRequest
+ ): BaseResponse
}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt b/app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt
index ea33b3a0..b7ecfa29 100644
--- a/app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt
+++ b/app/src/main/java/com/texthip/thip/service/MyFirebaseMessagingService.kt
@@ -11,6 +11,7 @@ import com.google.firebase.messaging.RemoteMessage
import com.texthip.thip.MainActivity
import com.texthip.thip.R
import com.texthip.thip.data.manager.FcmTokenManager
+import com.texthip.thip.data.repository.NotificationRepository
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -22,6 +23,9 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
@Inject
lateinit var fcmTokenManager: FcmTokenManager
+ @Inject
+ lateinit var notificationRepository: NotificationRepository
+
companion object {
private const val TAG = "FCM"
private const val CHANNEL_ID = "thip_notifications"
@@ -32,16 +36,25 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
- Log.d(TAG, "From: ${remoteMessage.from}")
- if (remoteMessage.data.isNotEmpty()) {
- Log.d(TAG, "Message data payload: ${remoteMessage.data}")
+ // 푸시 알림 도착 시 알림 상태 새로고침 (비차단 방식)
+ try {
+ notificationRepository.onNotificationReceived()
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to trigger notification refresh", e)
}
- remoteMessage.notification?.let {
- Log.d(TAG, "Message Notification Body: ${it.body}")
- showNotification(it.title, it.body)
+ // Data payload 처리
+ val dataPayload = remoteMessage.data
+ if (dataPayload.isNotEmpty()) {
+ Log.d(TAG, "Message data payload: $dataPayload")
}
+
+ val title = remoteMessage.notification?.title ?: "THIP"
+ val body = remoteMessage.notification?.body ?: "새로운 알림이 있습니다"
+
+ Log.d(TAG, "App is in foreground, showing custom notification: title=$title, body=$body")
+ showNotification(title, body, dataPayload)
}
override fun onNewToken(token: String) {
@@ -54,39 +67,56 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
}
}
- private fun showNotification(title: String?, messageBody: String?) {
+ private fun showNotification(
+ title: String?,
+ messageBody: String?,
+ dataPayload: Map
+ ) {
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ // FCM 데이터를 Intent에 추가
+ dataPayload["notificationId"]?.let { notificationId ->
+ putExtra("notification_id", notificationId)
+ putExtra("from_notification", true)
+ }
}
val pendingIntent = PendingIntent.getActivity(
this,
- 0,
+ System.currentTimeMillis().toInt(), // 고유한 requestCode 사용
intent,
- PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
createNotificationChannel()
val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(title ?: "THIP")
.setContentText(messageBody)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setDefaults(NotificationCompat.DEFAULT_ALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- notificationManager.notify(0, notificationBuilder.build())
+ val notificationId =
+ dataPayload["notificationId"]?.toIntOrNull() ?: System.currentTimeMillis().toInt()
+ notificationManager.notify(notificationId, notificationBuilder.build())
}
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
- NotificationManager.IMPORTANCE_DEFAULT
+ NotificationManager.IMPORTANCE_HIGH
).apply {
description = CHANNEL_DESCRIPTION
+ enableVibration(true)
+ setShowBadge(true)
+ enableLights(true)
}
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/AlarmFilterRow.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/AlarmFilterRow.kt
index f70659bc..542b6db7 100644
--- a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/AlarmFilterRow.kt
+++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/component/AlarmFilterRow.kt
@@ -24,11 +24,13 @@ fun AlarmFilterRow(
OptionChipButton(
text = stringResource(R.string.alarm_feed),
isFilled = true,
+ isSelected = selectedStates[0],
onClick = { onToggle(0) }
)
OptionChipButton(
text = stringResource(R.string.alarm_group),
isFilled = true,
+ isSelected = selectedStates[1],
onClick = { onToggle(1) }
)
}
diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/mock/NotificationType.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/mock/NotificationType.kt
new file mode 100644
index 00000000..1a1f755e
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/mock/NotificationType.kt
@@ -0,0 +1,7 @@
+package com.texthip.thip.ui.common.alarmpage.mock
+
+enum class NotificationType(val value: String) {
+ FEED_AND_ROOM("feedAndRoom"),
+ FEED("feed"),
+ ROOM("room")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt
index 3044e2e5..f2d76254 100644
--- a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/screen/AlarmScreen.kt
@@ -9,39 +9,96 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
+import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.texthip.thip.R
+import com.texthip.thip.data.model.notification.response.NotificationCheckResponse
+import com.texthip.thip.data.model.notification.response.NotificationResponse
import com.texthip.thip.ui.common.alarmpage.component.AlarmFilterRow
-import com.texthip.thip.ui.common.alarmpage.mock.AlarmItem
import com.texthip.thip.ui.common.alarmpage.component.CardAlarm
+import com.texthip.thip.ui.common.alarmpage.mock.NotificationType
+import com.texthip.thip.ui.common.alarmpage.viewmodel.AlarmUiState
+import com.texthip.thip.ui.common.alarmpage.viewmodel.AlarmViewModel
import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar
import com.texthip.thip.ui.theme.ThipTheme
import com.texthip.thip.ui.theme.ThipTheme.colors
import com.texthip.thip.ui.theme.ThipTheme.typography
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AlarmScreen(
- alarmItems: List,
- onCardClick: (AlarmItem) -> Unit = {}, // 나중에 서버랑 연동할 때 사용
- onNavigateBack: () -> Unit = {}
+ onNavigateBack: () -> Unit = {},
+ onNotificationNavigation: (NotificationCheckResponse) -> Unit = {},
+ viewModel: AlarmViewModel = hiltViewModel()
) {
- var selectedStates by remember { mutableStateOf(booleanArrayOf(false, false)) }
- var alarms by remember { mutableStateOf(alarmItems) }
+ val uiState by viewModel.uiState.collectAsState()
- val filteredList = when {
- selectedStates[0] && !selectedStates[1] -> alarms.filter { it.badgeText == stringResource(R.string.alarm_feed) }
- !selectedStates[0] && selectedStates[1] -> alarms.filter { it.badgeText == stringResource(R.string.alarm_group) }
- else -> alarms
+ LaunchedEffect(key1 = Unit) {
+ viewModel.refreshData()
+ }
+
+ AlarmContent(
+ uiState = uiState,
+ onNavigateBack = onNavigateBack,
+ onRefresh = { viewModel.refreshData() },
+ onLoadMore = { viewModel.loadMoreNotifications() },
+ onChangeNotificationType = { viewModel.changeNotificationType(it) },
+ onNotificationClick = { notificationId ->
+ viewModel.checkNotification(notificationId) { response ->
+ onNotificationNavigation(response)
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AlarmContent(
+ uiState: AlarmUiState,
+ onNavigateBack: () -> Unit = {},
+ onRefresh: () -> Unit = {},
+ onLoadMore: () -> Unit = {},
+ onChangeNotificationType: (NotificationType) -> Unit = {},
+ onNotificationClick: (Int) -> Unit = {}
+) {
+ val listState = rememberLazyListState()
+
+ // 무한 스크롤 로직
+ val shouldLoadMore by remember(uiState.canLoadMore, uiState.isLoadingMore) {
+ derivedStateOf {
+ val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+ val totalItems = listState.layoutInfo.totalItemsCount
+ uiState.canLoadMore && !uiState.isLoadingMore && totalItems > 0 && lastVisibleIndex >= totalItems - 3
+ }
+ }
+
+ LaunchedEffect(shouldLoadMore) {
+ if (shouldLoadMore) {
+ onLoadMore()
+ }
+ }
+
+ // 필터 상태 매핑
+ val selectedStates = remember(uiState.currentNotificationType) {
+ when (uiState.currentNotificationType) {
+ NotificationType.FEED -> booleanArrayOf(true, false)
+ NotificationType.ROOM -> booleanArrayOf(false, true)
+ else -> booleanArrayOf(false, false) // FEED_AND_ROOM
+ }
}
Column(
@@ -52,49 +109,79 @@ fun AlarmScreen(
title = stringResource(R.string.alarm_string),
onLeftClick = onNavigateBack,
)
- Column(
- Modifier
- .fillMaxSize()
- .padding(horizontal = 20.dp)
+
+ PullToRefreshBox(
+ isRefreshing = uiState.isLoading,
+ onRefresh = onRefresh,
+ modifier = Modifier.fillMaxSize()
) {
- Spacer(modifier = Modifier.height(20.dp))
- AlarmFilterRow(
- selectedStates = selectedStates, onToggle = { idx ->
- selectedStates = selectedStates.copyOf().also { it[idx] = !it[idx] }
- })
- Spacer(modifier = Modifier.height(20.dp))
-
- if (filteredList.isEmpty()) {
-
- Column(
- modifier = Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Text(
- text = stringResource(R.string.alarm_notification_comment),
- style = typography.smalltitle_sb600_s18_h24,
- color = colors.White
- )
- }
- } else {
- LazyColumn(
- verticalArrangement = Arrangement.spacedBy(20.dp),
- contentPadding = PaddingValues(bottom = 20.dp),
- modifier = Modifier.fillMaxSize()
- ) {
- items(filteredList, key = { it.id }) { alarm ->
- CardAlarm(
- badgeText = alarm.badgeText,
- title = alarm.title,
- message = alarm.message,
- timeAgo = alarm.timeAgo,
- isRead = alarm.isRead,
- onClick = {
- alarms = alarms.map {
- if (it.id == alarm.id) it.copy(isRead = true) else it
+ Column(
+ Modifier
+ .fillMaxSize()
+ .padding(horizontal = 20.dp)
+ ) {
+ Spacer(modifier = Modifier.height(20.dp))
+ AlarmFilterRow(
+ selectedStates = selectedStates,
+ onToggle = { idx ->
+ val newNotificationType = when {
+ // 피드 버튼을 눌렀을 때
+ idx == 0 -> {
+ if (selectedStates[0]) {
+ // 이미 선택된 상태면 전체로 변경
+ NotificationType.FEED_AND_ROOM
+ } else {
+ // 선택되지 않은 상태면 피드만
+ NotificationType.FEED
+ }
+ }
+ // 모임 버튼을 눌렀을 때
+ idx == 1 -> {
+ if (selectedStates[1]) {
+ NotificationType.FEED_AND_ROOM
+ } else {
+ NotificationType.ROOM
+ }
+ }
+
+ else -> NotificationType.FEED_AND_ROOM
+ }
+ onChangeNotificationType(newNotificationType)
+ }
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+
+ if (uiState.notifications.isNotEmpty()) {
+ LazyColumn(
+ state = listState,
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ contentPadding = PaddingValues(bottom = 20.dp),
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(uiState.notifications, key = { it.notificationId }) { notification ->
+ CardAlarm(
+ badgeText = notification.notificationType,
+ title = removeBracketPrefix(notification.title),
+ message = notification.content,
+ timeAgo = notification.postDate,
+ isRead = notification.isChecked,
+ onClick = {
+ onNotificationClick(notification.notificationId)
}
- })
+ )
+ }
+ }
+ } else if (!uiState.isLoading) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.alarm_notification_comment),
+ style = typography.smalltitle_sb600_s18_h24,
+ color = colors.White
+ )
}
}
}
@@ -102,17 +189,46 @@ fun AlarmScreen(
}
}
+private fun removeBracketPrefix(title: String): String {
+ return title.replace(Regex("^\\[.*?\\]\\s*"), "").trim()
+}
+
@Preview(showBackground = true)
@Composable
fun AlarmScreenPreview() {
ThipTheme {
- AlarmScreen(
- alarmItems = listOf(
- AlarmItem(1, "피드", "내 글을 좋아합니다.", "user123님이 내 글에 좋아요를 눌렀어요.", "2", false),
- AlarmItem(2, "모임", "같이 읽기를 시작했어요!", "모임방에서 20분 동안 같이 읽기가 시작되었어요!", "7", false),
- AlarmItem(3, "피드", "내 글에 댓글이 달렸어요.", "user1: 진짜 공감합니다!", "2025.01.12", true),
- AlarmItem(4, "모임", "투표가 시작되었어요!", "투표지를 먼저 열람합니다.", "17", false)
+ AlarmContent(
+ uiState = AlarmUiState(
+ notifications = listOf(
+ NotificationResponse(
+ notificationId = 1,
+ title = "[피드] 내 글을 좋아합니다.",
+ content = "user123님이 내 글에 좋아요를 눌렀어요.",
+ isChecked = false,
+ notificationType = "피드",
+ postDate = "2시간 전"
+ ),
+ NotificationResponse(
+ notificationId = 2,
+ title = "[모임] 같이 읽기를 시작했어요!",
+ content = "모임방에서 20분 동안 같이 읽기가 시작되었어요!",
+ isChecked = false,
+ notificationType = "모임",
+ postDate = "7시간 전"
+ ),
+ NotificationResponse(
+ notificationId = 3,
+ title = "[모임] 투표가 시작되었어요!",
+ content = "투표지를 먼저 열람합니다.",
+ isChecked = true,
+ notificationType = "모임",
+ postDate = "17시간 전"
+ )
+ ),
+ currentNotificationType = NotificationType.FEED_AND_ROOM,
+ isLoading = false,
+ hasMore = true
)
)
}
@@ -122,8 +238,11 @@ fun AlarmScreenPreview() {
@Composable
fun AlarmScreenEmptyPreview() {
ThipTheme {
- AlarmScreen(
- alarmItems = emptyList()
+ AlarmContent(
+ uiState = AlarmUiState(
+ notifications = emptyList(),
+ isLoading = false
+ )
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmUiState.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmUiState.kt
new file mode 100644
index 00000000..24e685ae
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmUiState.kt
@@ -0,0 +1,16 @@
+package com.texthip.thip.ui.common.alarmpage.viewmodel
+
+import com.texthip.thip.data.model.notification.response.NotificationResponse
+import com.texthip.thip.ui.common.alarmpage.mock.NotificationType
+
+data class AlarmUiState(
+ val notifications: List = emptyList(),
+ val currentNotificationType: NotificationType = NotificationType.FEED_AND_ROOM,
+ val isLoading: Boolean = false,
+ val isLoadingMore: Boolean = false,
+ val hasMore: Boolean = true,
+ val error: String? = null
+) {
+ val canLoadMore: Boolean get() = !isLoading && !isLoadingMore && hasMore
+ val hasUnreadNotifications: Boolean get() = notifications.any { !it.isChecked }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt
index efa44466..6b0d540b 100644
--- a/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt
+++ b/app/src/main/java/com/texthip/thip/ui/common/alarmpage/viewmodel/AlarmViewModel.kt
@@ -1,27 +1,164 @@
package com.texthip.thip.ui.common.alarmpage.viewmodel
import androidx.lifecycle.ViewModel
-import com.texthip.thip.ui.common.alarmpage.mock.AlarmItem
+import androidx.lifecycle.viewModelScope
+import com.texthip.thip.data.repository.NotificationRepository
+import com.texthip.thip.data.model.notification.response.NotificationCheckResponse
+import com.texthip.thip.ui.common.alarmpage.mock.NotificationType
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
-class AlarmViewModel : ViewModel() {
- private val _alarmItems = MutableStateFlow>(emptyList())
- val alarmItems: StateFlow> = _alarmItems.asStateFlow()
+@HiltViewModel
+class AlarmViewModel @Inject constructor(
+ private val repository: NotificationRepository
+) : ViewModel() {
+
+ private val _uiState = MutableStateFlow(AlarmUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ private var nextCursor: String? = null
+ private var isLastPage = false
+ private var isLoadingData = false
+ private var loadJob: Job? = null
+
+ private fun updateState(update: (AlarmUiState) -> AlarmUiState) {
+ _uiState.value = update(_uiState.value)
+ }
- // 알림 더미 데이터
init {
- _alarmItems.value = listOf(
- AlarmItem(1, "피드", "내 글을 좋아합니다.", "user123님이 내 글에 좋아요를 눌렀어요.", "2시간 전", false),
- AlarmItem(2, "모임", "같이 읽기를 시작했어요!", "모임방에서 20분 동안 같이 읽기가 시작되었어요!", "7시간 전", false),
- AlarmItem(4, "모임", "투표가 시작되었어요!", "투표지를 먼저 열람합니다.", "17시간 전", false),
- AlarmItem(5, "피드", "팔로워가 새 글을 올렸어요.", "user456님이 새 리뷰를 작성했습니다.", "1일 전", true),
- AlarmItem(6, "모임", "새로운 모임방 초대", "호르몬 체인지 완독하는 방에 초대되었습니다.", "2일 전", false)
- )
+ loadNotifications(reset = true)
+
+ // Repository의 알림 업데이트 이벤트 구독
+ viewModelScope.launch {
+ repository.notificationUpdateFlow.collect { notificationId ->
+ updateNotificationAsRead(notificationId)
+ }
+ }
+
+ // 푸시 알림 도착 시 새로고침 이벤트 구독
+ viewModelScope.launch {
+ repository.notificationRefreshFlow.collect {
+ refreshData()
+ }
+ }
+ }
+
+ fun loadNotifications(reset: Boolean = false) {
+ // reset 시 기존 작업 취소
+ if (reset) {
+ loadJob?.cancel()
+ loadJob = null
+ }
+
+ // 중복 로드 방지 (reset이 아닌 경우에만)
+ if (isLoadingData && !reset) return
+ if (isLastPage && !reset) return
+
+ // launch 전에 isLoadingData 선반영 (플리커 방지)
+ isLoadingData = true
+
+ // UI 상태 즉시 반영
+ if (reset) {
+ updateState {
+ it.copy(
+ isLoading = true,
+ notifications = emptyList(),
+ hasMore = true
+ )
+ }
+ nextCursor = null
+ isLastPage = false
+ } else {
+ updateState { it.copy(isLoadingMore = true) }
+ }
+
+ // 하나의 loadJob에 작업 바인딩
+ val currentJob = viewModelScope.launch {
+ try {
+ val type =
+ if (uiState.value.currentNotificationType == NotificationType.FEED_AND_ROOM) {
+ null
+ } else {
+ uiState.value.currentNotificationType.value
+ }
+
+ repository.getNotifications(nextCursor, type)
+ .onSuccess { notificationListResponse ->
+ notificationListResponse?.let { response ->
+ val currentList =
+ if (reset) emptyList() else uiState.value.notifications
+ updateState {
+ it.copy(
+ notifications = currentList + response.notifications,
+ error = null,
+ hasMore = !response.isLast
+ )
+ }
+ nextCursor = response.nextCursor
+ isLastPage = response.isLast
+ } ?: run {
+ updateState { it.copy(hasMore = false) }
+ isLastPage = true
+ }
+ }
+ .onFailure { exception ->
+ updateState { it.copy(error = exception.message) }
+ }
+ } finally {
+ if (loadJob == coroutineContext[Job]) {
+ isLoadingData = false
+ updateState { it.copy(isLoading = false, isLoadingMore = false) }
+ loadJob = null
+ }
+ }
+ }
+ loadJob = currentJob
+ }
+
+ fun loadMoreNotifications() {
+ loadNotifications(reset = false)
+ }
+
+ fun refreshData() {
+ loadNotifications(reset = true)
+ }
+
+ fun changeNotificationType(notificationType: NotificationType) {
+ if (notificationType != uiState.value.currentNotificationType) {
+ updateState { it.copy(currentNotificationType = notificationType) }
+ loadNotifications(reset = true)
+ }
+ }
+
+ fun checkNotification(notificationId: Int, onNavigate: (NotificationCheckResponse) -> Unit) {
+ viewModelScope.launch {
+ repository.checkNotification(notificationId)
+ .onSuccess { response ->
+ response?.let {
+ // 로컬 상태에서 해당 알림을 읽음으로 표시
+ updateNotificationAsRead(notificationId)
+ onNavigate(it)
+ }
+ }
+ .onFailure { exception ->
+ updateState { it.copy(error = exception.message) }
+ }
+ }
}
- fun onCardClick(item: AlarmItem) {
- // TODO: 알림 카드 클릭 처리
+ private fun updateNotificationAsRead(notificationId: Int) {
+ val updatedNotifications = uiState.value.notifications.map { notification ->
+ if (notification.notificationId == notificationId) {
+ notification.copy(isChecked = true)
+ } else {
+ notification
+ }
+ }
+ updateState { it.copy(notifications = updatedNotifications) }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
index d55debb8..24d30f6e 100644
--- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt
@@ -43,10 +43,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
import com.texthip.thip.R
import com.texthip.thip.data.model.feed.response.AllFeedItem
import com.texthip.thip.data.model.users.response.RecentWriterList
+import com.texthip.thip.ui.common.alarmpage.viewmodel.AlarmViewModel
import com.texthip.thip.ui.common.buttons.FloatingButton
import com.texthip.thip.ui.common.header.AuthorHeader
import com.texthip.thip.ui.common.header.HeaderMenuBarTab
@@ -66,7 +66,6 @@ import com.texthip.thip.utils.color.hexToColor
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FeedScreen(
@@ -85,8 +84,10 @@ fun FeedScreen(
onRefreshConsumed: () -> Unit = {},
navController: NavHostController,
feedViewModel: FeedViewModel = hiltViewModel(),
+ alarmViewModel: AlarmViewModel = hiltViewModel()
) {
val feedUiState by feedViewModel.uiState.collectAsState()
+ val alarmUiState by alarmViewModel.uiState.collectAsState()
val scope = rememberCoroutineScope()
var showProgressBar by remember { mutableStateOf(false) }
val progress = remember { Animatable(0f) }
@@ -154,7 +155,8 @@ fun FeedScreen(
val hasUpdatedFeedData =
navController.currentBackStackEntry?.savedStateHandle?.get("updated_feed_id") != null
val fromProfile =
- navController.currentBackStackEntry?.savedStateHandle?.get("from_profile") ?: false
+ navController.currentBackStackEntry?.savedStateHandle?.get("from_profile")
+ ?: false
if (!hasUpdatedFeedData && !fromProfile) {
// 일반적인 경우: 전체 새로고침 + 스크롤 상단 이동
@@ -177,7 +179,7 @@ fun FeedScreen(
isUserTabChange = false
}
}
-
+
// 같은 탭 재클릭 시 스크롤 상단 이동 처리
LaunchedEffect(shouldScrollToTop) {
if (shouldScrollToTop) {
@@ -185,7 +187,7 @@ fun FeedScreen(
shouldScrollToTop = false
}
}
-
+
// 중복된 로직 제거 - 기존 bottomNavReselected 방식만 사용
LaunchedEffect(resultFeedId) {
@@ -217,11 +219,12 @@ fun FeedScreen(
}
}
}
-
+
// 바텀 네비게이션 탭 재선택 처리 (직접 상태 전달 방식)
LaunchedEffect(onFeedTabReselected) {
if (onFeedTabReselected > 0) {
feedViewModel.refreshOnBottomNavReselect()
+ alarmViewModel.refreshData()
currentListState.scrollToItem(0)
}
}
@@ -256,6 +259,7 @@ fun FeedScreen(
FeedContent(
feedUiState = feedUiState,
+ hasUnreadNotifications = alarmUiState.hasUnreadNotifications,
showProgressBar = showProgressBar,
progress = progress.value,
currentListState = currentListState,
@@ -282,14 +286,18 @@ fun FeedScreen(
},
onChangeFeedLike = feedViewModel::changeFeedLike,
onChangeFeedSave = feedViewModel::changeFeedSave,
- onPullToRefresh = feedViewModel::pullToRefresh
+ onPullToRefresh = {
+ feedViewModel.pullToRefresh()
+ alarmViewModel.refreshData()
+ }
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun FeedContent(
- feedUiState: com.texthip.thip.ui.feed.viewmodel.FeedUiState,
+ feedUiState: FeedUiState,
+ hasUnreadNotifications: Boolean,
showProgressBar: Boolean,
progress: Float,
currentListState: LazyListState,
@@ -331,7 +339,7 @@ private fun FeedContent(
) {
LogoTopAppBar(
leftIcon = painterResource(R.drawable.ic_plusfriend),
- hasNotification = false,
+ hasNotification = hasUnreadNotifications,
onLeftClick = onNavigateToSearchPeople,
onRightClick = onNavigateToNotification,
)
@@ -659,6 +667,7 @@ private fun FeedContentPreview() {
)
)
),
+ hasUnreadNotifications = false,
showProgressBar = false,
progress = 0f,
currentListState = LazyListState(),
diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt
index 0166a32e..add51661 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/note/screen/GroupNoteScreen.kt
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.texthip.thip.R
+import com.texthip.thip.ui.feed.viewmodel.FeedViewModel
import com.texthip.thip.data.model.rooms.response.PostList
import com.texthip.thip.data.model.rooms.response.RoomsRecordsPinResponse
import com.texthip.thip.ui.common.bottomsheet.MenuBottomSheet
@@ -82,15 +83,30 @@ fun GroupNoteScreen(
onEditNoteClick: (post: PostList) -> Unit = {},
onEditVoteClick: (post: PostList) -> Unit = {},
onNavigateToUserProfile: (userId: Long) -> Unit = {},
+ onNavigateToMyProfile: () -> Unit = {},
resultTabIndex: Int? = null,
onResultConsumed: () -> Unit = {},
initialPage: Int? = null,
initialIsOverview: Boolean? = null,
+ initialPostId: Int? = null,
+ openComments: Boolean = false,
viewModel: GroupNoteViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
+ // FeedViewModel을 통해 현재 사용자 정보 가져오기
+ val feedViewModel: FeedViewModel = hiltViewModel()
+ val feedUiState by feedViewModel.uiState.collectAsStateWithLifecycle()
+ val currentUserId = feedUiState.myFeedInfo?.creatorId
+
+ // 내 피드 정보가 없으면 로드
+ LaunchedEffect(Unit) {
+ if (feedUiState.myFeedInfo == null) {
+ feedViewModel.onTabSelected(1)
+ }
+ }
+
var showProgressBar by remember { mutableStateOf(false) }
val progress = remember { Animatable(0f) }
var progressJob by remember { mutableStateOf(null) }
@@ -128,7 +144,7 @@ fun GroupNoteScreen(
LaunchedEffect(key1 = roomId) {
// 기록 생성 후 돌아온 경우가 아닐 때 (처음 진입 시) 초기화
if (resultTabIndex == null) {
- viewModel.initialize(roomId, initialPage, initialIsOverview)
+ viewModel.initialize(roomId, initialPage, initialIsOverview, initialPostId)
}
}
@@ -159,9 +175,19 @@ fun GroupNoteScreen(
},
onEditNoteClick = onEditNoteClick,
onEditVoteClick = onEditVoteClick,
- onNavigateToUserProfile = onNavigateToUserProfile,
+ onNavigateToUserProfile = { userId ->
+ // 현재 사용자 ID와 비교하여 적절한 네비게이션 수행
+ if (currentUserId != null && currentUserId == userId) {
+ // 내 프로필로 이동
+ onNavigateToMyProfile()
+ } else {
+ // 다른 사용자 프로필로 이동
+ onNavigateToUserProfile(userId)
+ }
+ },
showProgressBar = showProgressBar,
- progress = progress.value
+ progress = progress.value,
+ openComments = openComments
)
}
@@ -177,7 +203,8 @@ fun GroupNoteContent(
onEditVoteClick: (post: PostList) -> Unit,
onNavigateToUserProfile: (userId: Long) -> Unit,
showProgressBar: Boolean,
- progress: Float
+ progress: Float,
+ openComments: Boolean = false
) {
var isCommentBottomSheetVisible by remember { mutableStateOf(false) }
var selectedPostForComment by remember { mutableStateOf(null) }
@@ -230,6 +257,52 @@ fun GroupNoteContent(
}
}
+ // 특정 포스트로 스크롤
+ LaunchedEffect(uiState.scrollToPostId, uiState.posts, uiState.isLoading) {
+ val scrollToPostId = uiState.scrollToPostId
+
+ if (scrollToPostId != null && uiState.posts.isNotEmpty() && !uiState.isLoading) {
+ val targetIndex = uiState.posts.indexOfFirst { it.postId == scrollToPostId }
+
+ if (targetIndex != -1) {
+ val targetPost = uiState.posts[targetIndex]
+
+ // 헤더 아이템들을 고려한 실제 인덱스 계산
+ val actualIndex = if (uiState.selectedTabIndex == 0) {
+ targetIndex + 2 // 정보 텍스트 + 프로그레스바 아이템
+ } else {
+ targetIndex + 1 // 프로그레스바 아이템만
+ }
+
+ // LazyColumn이 완전히 구성될 때까지 잠시 대기
+ kotlinx.coroutines.delay(100)
+
+ try {
+ listState.animateScrollToItem(actualIndex)
+
+ // openComments가 true이면 댓글 버텀시트를 자동으로 열기
+ if (openComments) {
+ kotlinx.coroutines.delay(200) // 스크롤 완료 후 잠시 대기
+ selectedPostForComment = targetPost
+ isCommentBottomSheetVisible = true
+ }
+ } catch (e: Exception) {
+ // 애니메이션이 실패하면 일반 스크롤 시도
+ listState.scrollToItem(actualIndex)
+
+ // openComments가 true이면 댓글 버텀시트를 자동으로 열기
+ if (openComments) {
+ kotlinx.coroutines.delay(200) // 스크롤 완료 후 잠시 대기
+ selectedPostForComment = targetPost
+ isCommentBottomSheetVisible = true
+ }
+ }
+
+ onEvent(GroupNoteEvent.ClearScrollTarget)
+ }
+ }
+ }
+
Box(
if (isOverlayVisible) {
Modifier
diff --git a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt
index bf2cc03a..018dcaaa 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/note/viewmodel/GroupNoteViewModel.kt
@@ -39,7 +39,10 @@ data class GroupNoteUiState(
val pageEnd: String = "",
val isOverview: Boolean = false,
val isPageFilter: Boolean = false,
- val totalEnabled: Boolean = false
+ val totalEnabled: Boolean = false,
+
+ // 스크롤 관련 상태
+ val scrollToPostId: Int? = null
)
sealed interface GroupNoteSideEffect {
@@ -62,6 +65,7 @@ sealed interface GroupNoteEvent {
data class OnLikeRecord(val postId: Int, val postType: String) : GroupNoteEvent
data class OnPinRecord(val recordId: Int, val content: String) : GroupNoteEvent
data object RefreshPosts : GroupNoteEvent
+ data object ClearScrollTarget : GroupNoteEvent
}
@@ -82,7 +86,8 @@ class GroupNoteViewModel @Inject constructor(
fun initialize(
roomId: Int,
initialPage: Int? = null,
- initialIsOverview: Boolean? = null
+ initialIsOverview: Boolean? = null,
+ initialPostId: Int? = null
) {
this.roomId = roomId
@@ -97,6 +102,12 @@ class GroupNoteViewModel @Inject constructor(
}
}
+ if (initialPostId != null) {
+ _uiState.update {
+ it.copy(scrollToPostId = initialPostId)
+ }
+ }
+
refreshAllData()
}
@@ -168,6 +179,9 @@ class GroupNoteViewModel @Inject constructor(
is GroupNoteEvent.OnLikeRecord -> likeRecord(event.postId, event.postType)
is GroupNoteEvent.RefreshPosts -> loadPosts(isRefresh = true)
is GroupNoteEvent.OnPinRecord -> pinRecord(event.recordId, event.content)
+ GroupNoteEvent.ClearScrollTarget -> {
+ _uiState.update { it.copy(scrollToPostId = null) }
+ }
else -> {
Log.w("GroupNoteViewModel", "Unhandled event received: $event")
}
diff --git a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
index 033598c9..ee41ae90 100644
--- a/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/group/screen/GroupScreen.kt
@@ -34,6 +34,7 @@ import com.texthip.thip.data.model.rooms.response.RoomMainList
import com.texthip.thip.data.model.rooms.response.RoomMainResponse
import com.texthip.thip.ui.common.buttons.FloatingButton
import com.texthip.thip.ui.common.modal.ToastWithDate
+import com.texthip.thip.ui.common.alarmpage.viewmodel.AlarmViewModel
import com.texthip.thip.ui.common.topappbar.LogoTopAppBar
import com.texthip.thip.ui.feed.component.EmptyMySubscriptionBar
import com.texthip.thip.ui.group.myroom.component.GroupMySectionHeader
@@ -55,18 +56,22 @@ fun GroupScreen(
onNavigateToGroupSearch: () -> Unit = {}, // 검색 화면으로 이동
onNavigateToGroupMy: () -> Unit = {}, // 내 모임방 화면으로 이동
onNavigateToGroupRecruit: (Int) -> Unit = {}, // 모집 중인 모임방 화면으로 이동
- onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동,
+ onNavigateToGroupRoom: (Int) -> Unit = {}, // 기록장 화면으로 이동
onNavigateToGroupSearchAllRooms: () -> Unit = {},
- viewModel: GroupViewModel = hiltViewModel()
+ viewModel: GroupViewModel = hiltViewModel(),
+ alarmViewModel: AlarmViewModel = hiltViewModel()
) {
// 화면 재진입 시 데이터 새로고침
LaunchedEffect(Unit) {
viewModel.resetToInitialState()
+ alarmViewModel.refreshData()
}
val uiState by viewModel.uiState.collectAsState()
+ val alarmUiState by alarmViewModel.uiState.collectAsState()
GroupContent(
uiState = uiState,
+ hasUnreadNotifications = alarmUiState.hasUnreadNotifications,
onNavigateToMakeRoom = onNavigateToMakeRoom,
onNavigateToGroupDone = onNavigateToGroupDone,
onNavigateToAlarm = onNavigateToAlarm,
@@ -75,7 +80,10 @@ fun GroupScreen(
onNavigateToGroupRecruit = onNavigateToGroupRecruit,
onNavigateToGroupRoom = onNavigateToGroupRoom,
onNavigateToGroupSearchAllRooms = onNavigateToGroupSearchAllRooms,
- onRefreshGroupData = { viewModel.refreshGroupData() },
+ onRefreshGroupData = {
+ viewModel.refreshGroupData()
+ alarmViewModel.refreshData()
+ },
onCardVisible = { cardIndex -> viewModel.loadMoreGroups() },
onSelectGenre = { genreIndex -> viewModel.selectGenre(genreIndex) },
onHideToast = { viewModel.hideToast() },
@@ -86,6 +94,7 @@ fun GroupScreen(
@Composable
fun GroupContent(
uiState: GroupUiState,
+ hasUnreadNotifications: Boolean = false,
onNavigateToMakeRoom: () -> Unit = {},
onNavigateToGroupDone: () -> Unit = {},
onNavigateToAlarm: () -> Unit = {},
@@ -136,7 +145,13 @@ fun GroupContent(
groupCards = uiState.myJoinedRooms,
userName = uiState.userName,
onCardClick = { joinedRoom ->
- onNavigateToGroupRoom(joinedRoom.roomId)
+ if (joinedRoom.deadlineDate == null) {
+ // 시작 후
+ onNavigateToGroupRoom(joinedRoom.roomId)
+ } else {
+ // 시작 전
+ onNavigateToGroupRecruit(joinedRoom.roomId)
+ }
},
onCardVisible = onCardVisible
)
@@ -175,7 +190,7 @@ fun GroupContent(
// 상단바
LogoTopAppBar(
leftIcon = painterResource(R.drawable.ic_done),
- hasNotification = false,
+ hasNotification = hasUnreadNotifications,
onLeftClick = onNavigateToGroupDone,
onRightClick = onNavigateToAlarm
)
diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt
index c3e15d00..956f0ad2 100644
--- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt
+++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageNotificationEditScreen.kt
@@ -1,5 +1,7 @@
package com.texthip.thip.ui.mypage.screen
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
@@ -21,6 +23,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -36,6 +39,7 @@ import com.texthip.thip.ui.mypage.viewmodel.MypageNotificationEditUiState
import com.texthip.thip.ui.mypage.viewmodel.MypageNotificationEditViewModel
import com.texthip.thip.ui.theme.ThipTheme.colors
import com.texthip.thip.ui.theme.ThipTheme.typography
+import com.texthip.thip.utils.permission.NotificationPermissionUtils
import kotlinx.coroutines.delay
import java.text.SimpleDateFormat
import java.util.Date
@@ -46,10 +50,30 @@ fun MyPageNotificationEditScreen(
onNavigateBack: () -> Unit,
viewModel: MypageNotificationEditViewModel = hiltViewModel()
) {
+ val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var toastMessage by rememberSaveable { mutableStateOf(null) }
var toastDateTime by rememberSaveable { mutableStateOf("") }
+ // 알림 권한 요청 런처
+ val notificationPermissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission(),
+ onResult = { isGranted ->
+ if (isGranted) {
+ // 권한이 허용되면 알림 활성화
+ viewModel.onNotificationToggle(true)
+ toastMessage = "push_on"
+ val dateFormat = SimpleDateFormat("yyyy년 M월 d일 H시 m분", Locale.KOREAN)
+ toastDateTime = dateFormat.format(Date())
+ } else {
+ // 권한이 거부되면 토스트 메시지 표시
+ toastMessage = "permission_denied"
+ val dateFormat = SimpleDateFormat("yyyy년 M월 d일 H시 m분", Locale.KOREAN)
+ toastDateTime = dateFormat.format(Date())
+ }
+ }
+ )
+
LaunchedEffect(toastMessage) {
if (toastMessage != null) {
delay(3000)
@@ -63,10 +87,25 @@ fun MyPageNotificationEditScreen(
toastDateTime = toastDateTime,
onNavigateBack = onNavigateBack,
onNotificationToggle = { enabled ->
- viewModel.onNotificationToggle(enabled)
- toastMessage = if (enabled) "push_on" else "push_off"
- val dateFormat = SimpleDateFormat("yyyy년 M월 d일 H시 m분", Locale.KOREAN)
- toastDateTime = dateFormat.format(Date())
+ if (enabled) {
+ // 알림을 켜려고 할 때 권한 확인
+ if (NotificationPermissionUtils.shouldRequestNotificationPermission(context)) {
+ // 권한이 필요하면 권한 요청
+ notificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
+ } else {
+ // 권한이 이미 있거나 필요없으면 바로 설정 변경
+ viewModel.onNotificationToggle(enabled)
+ toastMessage = "push_on"
+ val dateFormat = SimpleDateFormat("yyyy년 M월 d일 H시 m분", Locale.KOREAN)
+ toastDateTime = dateFormat.format(Date())
+ }
+ } else {
+ // 알림을 끄는 경우는 권한 체크 없이 바로 설정 변경
+ viewModel.onNotificationToggle(enabled)
+ toastMessage = "push_off"
+ val dateFormat = SimpleDateFormat("yyyy년 M월 d일 H시 m분", Locale.KOREAN)
+ toastDateTime = dateFormat.format(Date())
+ }
}
)
}
@@ -97,9 +136,12 @@ fun MyPageNotificationEditContent(
) {
toastMessage?.let { message ->
ToastWithDate(
- message = stringResource(
- if (message == "push_on") R.string.push_on else R.string.push_off
- ),
+ message = when (message) {
+ "push_on" -> stringResource(R.string.push_on)
+ "push_off" -> stringResource(R.string.push_off)
+ "permission_denied" -> stringResource(R.string.notification_permission_required)
+ else -> stringResource(R.string.push_off)
+ },
date = toastDateTime,
modifier = Modifier.fillMaxWidth()
)
diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt
index 864247c9..85a5ccec 100644
--- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt
+++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/GroupNavigationExtensions.kt
@@ -23,12 +23,14 @@ fun NavHostController.navigateToGroupMakeRoomWithBook(
imageUrl: String,
author: String
) {
- navigate(GroupRoutes.MakeRoomWithBook(
- isbn = isbn,
- title = title,
- imageUrl = imageUrl,
- author = author
- ))
+ navigate(
+ GroupRoutes.MakeRoomWithBook(
+ isbn = isbn,
+ title = title,
+ imageUrl = imageUrl,
+ author = author
+ )
+ )
}
// 완료된 모임방 목록으로 이동
@@ -90,9 +92,19 @@ fun NavHostController.navigateToGroupNote(
roomId: Int,
page: Int? = null,
isOverview: Boolean? = null,
- isExpired: Boolean = false
+ isExpired: Boolean = false,
+ postId: Int? = null
) {
- navigate(GroupRoutes.Note(roomId = roomId, page = page, isOverview = isOverview, isExpired = isExpired))
+ navigate(
+ GroupRoutes.Note(
+ roomId = roomId,
+ page = page,
+ openComments = false,
+ isExpired = isExpired,
+ postId = postId,
+ isOverview = isOverview
+ )
+ )
}
// 기록 생성 화면으로 이동
diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/NotificationNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/NotificationNavigationExtensions.kt
new file mode 100644
index 00000000..a29fd862
--- /dev/null
+++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/NotificationNavigationExtensions.kt
@@ -0,0 +1,66 @@
+package com.texthip.thip.ui.navigator.extensions
+
+import android.util.Log
+import androidx.navigation.NavController
+import com.texthip.thip.data.model.notification.response.NotificationCheckResponse
+import com.texthip.thip.data.model.notification.response.NotificationRoute
+import com.texthip.thip.ui.navigator.routes.FeedRoutes
+import com.texthip.thip.ui.navigator.routes.GroupRoutes
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.contentOrNull
+
+private fun JsonElement.toStringOrNull(): String? {
+ return (this as? JsonPrimitive)?.contentOrNull
+}
+
+fun NavController.navigateFromNotification(response: NotificationCheckResponse) {
+ val params = response.params
+
+ try {
+ when (response.route) {
+ NotificationRoute.FEED_USER -> {
+ val userId = params["userId"]?.toStringOrNull()?.toLongOrNull()
+ if (userId != null) {
+ navigate(FeedRoutes.Others(userId))
+ }
+ }
+
+ NotificationRoute.FEED_DETAIL -> {
+ val feedId = params["feedId"]?.toStringOrNull()?.toLongOrNull()
+ if (feedId != null) {
+ navigate(FeedRoutes.Comment(feedId))
+ }
+ }
+
+ NotificationRoute.ROOM_MAIN -> {
+ val roomId = params["roomId"]?.toStringOrNull()?.toIntOrNull()
+ if (roomId != null) {
+ navigate(GroupRoutes.Room(roomId))
+ }
+ }
+
+ NotificationRoute.ROOM_DETAIL -> {
+ val roomId = params["roomId"]?.toStringOrNull()?.toIntOrNull()
+ if (roomId != null) {
+ navigate(GroupRoutes.Recruit(roomId))
+ }
+ }
+
+ NotificationRoute.ROOM_POST_DETAIL -> {
+ val roomId = params["roomId"]?.toStringOrNull()?.toIntOrNull()
+ val page = params["page"]?.toStringOrNull()?.toIntOrNull()
+ val postId = params["postId"]?.toStringOrNull()?.toIntOrNull()
+ val postType = params["postType"]?.toStringOrNull()
+ val openComments =
+ params["openComments"]?.toStringOrNull()?.toBooleanStrictOrNull() ?: false
+
+ if (roomId != null && page != null) {
+ navigate(GroupRoutes.Note(roomId, page, openComments, false, postId))
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.e("NotificationNav", "Navigation failed", e)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/CommonNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/CommonNavigation.kt
index 734373c7..7885bf5e 100644
--- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/CommonNavigation.kt
+++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/CommonNavigation.kt
@@ -1,14 +1,11 @@
package com.texthip.thip.ui.navigator.navigations
-import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
import com.texthip.thip.ui.common.alarmpage.screen.AlarmScreen
-import com.texthip.thip.ui.common.alarmpage.viewmodel.AlarmViewModel
import com.texthip.thip.ui.common.screen.RegisterBookScreen
+import com.texthip.thip.ui.navigator.extensions.navigateFromNotification
import com.texthip.thip.ui.navigator.routes.CommonRoutes
// Common 관련 네비게이션
@@ -18,16 +15,14 @@ fun NavGraphBuilder.commonNavigation(
) {
// Alarm 화면
composable {
- val alarmViewModel: AlarmViewModel = viewModel()
- val alarmItems by alarmViewModel.alarmItems.collectAsState()
-
AlarmScreen(
- alarmItems = alarmItems,
- onCardClick = { alarmViewModel.onCardClick(it) },
- onNavigateBack = navigateBack
+ onNavigateBack = navigateBack,
+ onNotificationNavigation = { response ->
+ navController.navigateFromNotification(response)
+ }
)
}
-
+
// 책 요청 화면
composable {
RegisterBookScreen(
diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt
index b6ba059f..2f73ad51 100644
--- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt
+++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/GroupNavigation.kt
@@ -342,30 +342,23 @@ fun NavGraphBuilder.groupNavigation(
val route = backStackEntry.toRoute()
val roomId = route.roomId
val page = route.page
+ val openComments = route.openComments
val isOverview = route.isOverview
val isExpired = route.isExpired
+ val postId = route.postId
val result = backStackEntry.savedStateHandle.get("selected_tab_index")
val viewModel: GroupNoteViewModel = hiltViewModel(backStackEntry)
- val feedViewModel: FeedViewModel =
- hiltViewModel(navController.getBackStackEntry(MainTabRoutes.Group))
- val feedUiState by feedViewModel.uiState.collectAsState()
- val myUserId = feedUiState.myFeedInfo?.creatorId
-
- LaunchedEffect(Unit) {
- if (feedUiState.myFeedInfo == null) {
- feedViewModel.onTabSelected(1)
- }
- }
-
GroupNoteScreen(
roomId = roomId,
resultTabIndex = result,
initialPage = page,
initialIsOverview = isOverview,
isExpired = isExpired,
+ initialPostId = postId,
+ openComments = openComments,
onResultConsumed = {
backStackEntry.savedStateHandle.remove("selected_tab_index")
},
@@ -424,11 +417,10 @@ fun NavGraphBuilder.groupNavigation(
)
},
onNavigateToUserProfile = { userId ->
- if (myUserId != null && myUserId == userId) {
- navController.navigate(FeedRoutes.My)
- } else {
- navController.navigate(FeedRoutes.Others(userId))
- }
+ navController.navigate(FeedRoutes.Others(userId))
+ },
+ onNavigateToMyProfile = {
+ navController.navigate(FeedRoutes.My)
},
viewModel = viewModel
)
diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt
index 723054e0..ddc9eb09 100644
--- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt
+++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/GroupRoutes.kt
@@ -43,8 +43,10 @@ sealed class GroupRoutes : Routes() {
data class Note(
val roomId: Int,
val page: Int? = null,
- val isOverview: Boolean? = null,
- val isExpired: Boolean = false
+ val openComments: Boolean = false,
+ val isExpired: Boolean = false,
+ val postId: Int? = null,
+ val isOverview: Boolean? = null
) : GroupRoutes()
@Serializable
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6e5d8af1..4d0c85e2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -127,6 +127,7 @@
알림센터의 모든 알림을 포함해요
푸시 알림이 해제되었어요.
푸시 알림이 설정되었어요.
+ 알림 권한이 필요합니다. 설정에서 권한을 허용해주세요.
texthip2025@gmail.com
이메일로 닉네임과 문의사항을 보내주시면