Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.texthip.thip.data.model.notification.response

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

@Serializable
data class NotificationExistsUncheckedResponse(
@SerialName("exists") val exists: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ 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.notification.response.NotificationExistsUncheckedResponse
import com.texthip.thip.data.service.NotificationService
import com.texthip.thip.utils.auth.getAppScopeDeviceId
import dagger.hilt.android.qualifiers.ApplicationContext
Expand Down Expand Up @@ -108,4 +109,11 @@ class NotificationRepository @Inject constructor(
fun onNotificationReceived() {
_notificationRefreshFlow.tryEmit(Unit)
}

suspend fun existsUncheckedNotifications(): Result<NotificationExistsUncheckedResponse?> {
return runCatching {
val response = notificationService.existsUncheckedNotifications()
response.handleBaseResponse().getOrNull()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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.notification.response.NotificationExistsUncheckedResponse
import com.texthip.thip.data.model.base.BaseResponse
import retrofit2.http.Body
import retrofit2.http.DELETE
Expand Down Expand Up @@ -46,4 +47,7 @@ interface NotificationService {
suspend fun checkNotification(
@Body request: NotificationCheckRequest
): BaseResponse<NotificationCheckResponse>

@GET("notifications/exists-unchecked")
suspend fun existsUncheckedNotifications(): BaseResponse<NotificationExistsUncheckedResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ data class AlarmUiState(
val isLoading: Boolean = false,
val isLoadingMore: Boolean = false,
val hasMore: Boolean = true,
val error: String? = null
val error: String? = null,
val hasUnreadNotifications: Boolean = false // API에서 가져온 읽지 않은 알림 존재 여부
) {
val canLoadMore: Boolean get() = !isLoading && !isLoadingMore && hasMore
val hasUnreadNotifications: Boolean get() = notifications.any { !it.isChecked }
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,20 @@ class AlarmViewModel @Inject constructor(
}

init {
loadNotifications(reset = true)
checkUnreadNotifications()

// Repository의 알림 업데이트 이벤트 구독
viewModelScope.launch {
repository.notificationUpdateFlow.collect { notificationId ->
updateNotificationAsRead(notificationId)
checkUnreadNotifications()
}
}

// 푸시 알림 도착 시 새로고침 이벤트 구독
// 푸시 알림 도착 시 아이콘 상태만 갱신
viewModelScope.launch {
repository.notificationRefreshFlow.collect {
refreshData()
checkUnreadNotifications()
}
}
}
Expand All @@ -54,14 +55,14 @@ class AlarmViewModel @Inject constructor(
loadJob?.cancel()
loadJob = null
}

// 중복 로드 방지 (reset이 아닌 경우에만)
if (isLoadingData && !reset) return
if (isLastPage && !reset) return

// launch 전에 isLoadingData 선반영 (플리커 방지)
isLoadingData = true

// UI 상태 즉시 반영
if (reset) {
updateState {
Expand Down Expand Up @@ -126,6 +127,7 @@ class AlarmViewModel @Inject constructor(

fun refreshData() {
loadNotifications(reset = true)
checkUnreadNotifications()
}

fun changeNotificationType(notificationType: NotificationType) {
Expand Down Expand Up @@ -161,4 +163,21 @@ class AlarmViewModel @Inject constructor(
}
updateState { it.copy(notifications = updatedNotifications) }
}

fun checkUnreadNotifications() {
viewModelScope.launch {
repository.existsUncheckedNotifications()
.onSuccess { response ->
response?.let {
updateState { state ->
state.copy(hasUnreadNotifications = it.exists)
}
}
}
.onFailure { exception ->
// 에러 발생 시 기존 상태 유지 (로그만 남김)
updateState { it.copy(error = exception.message) }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ fun FeedScreen(
LaunchedEffect(onFeedTabReselected) {
if (onFeedTabReselected > 0) {
feedViewModel.refreshOnBottomNavReselect()
alarmViewModel.refreshData()
alarmViewModel.checkUnreadNotifications()
currentListState.scrollToItem(0)
}
}
Expand Down Expand Up @@ -288,7 +288,7 @@ fun FeedScreen(
onChangeFeedSave = feedViewModel::changeFeedSave,
onPullToRefresh = {
feedViewModel.pullToRefresh()
alarmViewModel.refreshData()
alarmViewModel.checkUnreadNotifications()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fun GroupScreen(
// 화면 재진입 시 데이터 새로고침
LaunchedEffect(Unit) {
viewModel.resetToInitialState()
alarmViewModel.refreshData()
alarmViewModel.checkUnreadNotifications()
}
val uiState by viewModel.uiState.collectAsState()
val alarmUiState by alarmViewModel.uiState.collectAsState()
Expand All @@ -80,9 +80,9 @@ fun GroupScreen(
onNavigateToGroupRecruit = onNavigateToGroupRecruit,
onNavigateToGroupRoom = onNavigateToGroupRoom,
onNavigateToGroupSearchAllRooms = onNavigateToGroupSearchAllRooms,
onRefreshGroupData = {
onRefreshGroupData = {
viewModel.refreshGroupData()
alarmViewModel.refreshData()
alarmViewModel.checkUnreadNotifications()
},
onCardVisible = { cardIndex -> viewModel.loadMoreGroups() },
onSelectGenre = { genreIndex -> viewModel.selectGenre(genreIndex) },
Expand Down