diff --git a/core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt b/core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt index 2b37457d..1b6eec5f 100644 --- a/core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt @@ -16,4 +16,6 @@ interface NewsRepository { fun getNewsInfo(): Flow> fun getNoticeInfo(): Flow> + + suspend fun getNumber(): Result> } diff --git a/core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt b/core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt index f1df55bd..da1a67ee 100644 --- a/core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt @@ -19,6 +19,10 @@ interface UserInfoRepository { fun getIsAdmin(): Flow + fun getNewsNumber(): Flow + + fun getNoticeNumber(): Flow + suspend fun saveAccessToken(accessToken: String) suspend fun saveRefreshToken(refreshToken: String) @@ -35,6 +39,10 @@ interface UserInfoRepository { suspend fun saveIsAdmin(isAdmin: Boolean) + suspend fun saveNewsNumber(newsNumber: Int) + + suspend fun saveNoticeNumber(noticeNumber: Int) + suspend fun clearAll() suspend fun clearForRefreshToken() diff --git a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt index f1fb5ace..fddfb4d6 100644 --- a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt @@ -61,4 +61,13 @@ internal class DefaultNewsRepository @Inject constructor( pagingData.map { it.toNoticeInfoModel() } } } + + override suspend fun getNumber(): Result> { + return runCatching { + mapOf( + "news" to newsService.getNumber().data.newsNumber, + "notice" to newsService.getNumber().data.noticeNumber + ) + }.onFailure { return it.handleThrowable() } + } } diff --git a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt index 9b38325d..bc4a1c57 100644 --- a/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt +++ b/core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt @@ -32,6 +32,12 @@ internal class DefaultUserInfoRepository @Inject constructor( override fun getIsAdmin(): Flow = wablePreferencesDataSource.isAdmin + override fun getNewsNumber(): Flow = + wablePreferencesDataSource.newsNumber + + override fun getNoticeNumber(): Flow = + wablePreferencesDataSource.noticeNumber + override suspend fun saveAccessToken(accessToken: String) { wablePreferencesDataSource.updateAccessToken(accessToken) } @@ -64,6 +70,14 @@ internal class DefaultUserInfoRepository @Inject constructor( wablePreferencesDataSource.updateIsAdmin(isAdmin) } + override suspend fun saveNewsNumber(newsNumber: Int) { + wablePreferencesDataSource.updateNewsNumber(newsNumber) + } + + override suspend fun saveNoticeNumber(noticeNumber: Int) { + wablePreferencesDataSource.updateNoticeNumber(noticeNumber) + } + override suspend fun clearAll() { wablePreferencesDataSource.clear() } diff --git a/core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt b/core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt index a09a956f..29e37c70 100644 --- a/core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt +++ b/core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt @@ -26,6 +26,8 @@ class DefaultWablePreferenceDatasource @Inject constructor( val MemberProfileUrl = stringPreferencesKey("memberProfileUrl") val IsPushAlarmAllowed = booleanPreferencesKey("isPushAlarmAllowed") val IsAdmin = booleanPreferencesKey("isAdmin") + val NewsNumber = intPreferencesKey("newsNumber") + val NoticeNumber = intPreferencesKey("noticeNumber") } override val accessToken: Flow = dataStore.data @@ -76,6 +78,18 @@ class DefaultWablePreferenceDatasource @Inject constructor( preferences[PreferencesKeys.IsAdmin] ?: false } + override val newsNumber: Flow = dataStore.data + .catch { handleError(it) } + .map { preferences -> + preferences[PreferencesKeys.NewsNumber] ?: -1 + } + + override val noticeNumber: Flow = dataStore.data + .catch { handleError(it) } + .map { preferences -> + preferences[PreferencesKeys.NoticeNumber] ?: -1 + } + override suspend fun updateAccessToken(accessToken: String) { dataStore.edit { preferences -> preferences[PreferencesKeys.AccessToken] = accessToken @@ -124,6 +138,18 @@ class DefaultWablePreferenceDatasource @Inject constructor( } } + override suspend fun updateNewsNumber(newsNumber: Int) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.NewsNumber] = newsNumber + } + } + + override suspend fun updateNoticeNumber(noticeNumber: Int) { + dataStore.edit { preferences -> + preferences[PreferencesKeys.NoticeNumber] = noticeNumber + } + } + override suspend fun clear() { dataStore.edit { preferences -> preferences.clear() diff --git a/core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt b/core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt index 18801467..e03591e2 100644 --- a/core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt +++ b/core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt @@ -11,6 +11,8 @@ interface WablePreferencesDataSource { val memberProfileUrl: Flow val isPushAlarmAllowed: Flow val isAdmin: Flow + val newsNumber: Flow + val noticeNumber: Flow suspend fun updateAccessToken(accessToken: String) @@ -28,6 +30,10 @@ interface WablePreferencesDataSource { suspend fun updateIsAdmin(isAdmin: Boolean) + suspend fun updateNewsNumber(newsNumber: Int) + + suspend fun updateNoticeNumber(noticeNumber: Int) + suspend fun clear() suspend fun clearForRefreshToken() diff --git a/core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt b/core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt index 23f0d3d3..f931a7b8 100644 --- a/core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt +++ b/core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt @@ -1,5 +1,6 @@ package com.teamwable.network.datasource +import com.teamwable.network.dto.response.main.ResponseNewsNumberDto import com.teamwable.network.dto.response.news.ResponseGameTypeDto import com.teamwable.network.dto.response.news.ResponseNewsInfoDto import com.teamwable.network.dto.response.news.ResponseNoticeInfoDto @@ -28,4 +29,7 @@ interface NewsService { suspend fun getNoticeInfo( @Query(value = "cursor") contentId: Long = -1, ): BaseResponse> + + @GET("api/v1/information/number") + suspend fun getNumber(): BaseResponse } diff --git a/core/network/src/main/java/com/teamwable/network/datasource/NotificationService.kt b/core/network/src/main/java/com/teamwable/network/datasource/NotificationService.kt index 4fabc048..5ae09a0a 100644 --- a/core/network/src/main/java/com/teamwable/network/datasource/NotificationService.kt +++ b/core/network/src/main/java/com/teamwable/network/datasource/NotificationService.kt @@ -2,7 +2,7 @@ package com.teamwable.network.datasource import com.teamwable.network.dto.response.notification.ResponseInformationDto import com.teamwable.network.dto.response.notification.ResponseNotificationsDto -import com.teamwable.network.dto.response.notification.ResponseNumberDto +import com.teamwable.network.dto.response.notification.ResponseNotificationNumberDto import com.teamwable.network.util.BaseResponse import retrofit2.http.GET import retrofit2.http.PATCH @@ -10,7 +10,7 @@ import retrofit2.http.Query interface NotificationService { @GET("api/v1/notification/number") - suspend fun getNumber(): BaseResponse + suspend fun getNumber(): BaseResponse @PATCH("api/v1/notification-check") suspend fun patchCheck(): BaseResponse diff --git a/core/network/src/main/java/com/teamwable/network/dto/response/main/ResponseNewsNumberDto.kt b/core/network/src/main/java/com/teamwable/network/dto/response/main/ResponseNewsNumberDto.kt new file mode 100644 index 00000000..7638113c --- /dev/null +++ b/core/network/src/main/java/com/teamwable/network/dto/response/main/ResponseNewsNumberDto.kt @@ -0,0 +1,12 @@ +package com.teamwable.network.dto.response.main + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseNewsNumberDto( + @SerialName("newsNumber") + val newsNumber: Int = 0, + @SerialName("noticeNumber") + val noticeNumber: Int = 0 +) diff --git a/core/network/src/main/java/com/teamwable/network/dto/response/notification/ResponseNumberDto.kt b/core/network/src/main/java/com/teamwable/network/dto/response/notification/ResponseNotificationNumberDto.kt similarity index 84% rename from core/network/src/main/java/com/teamwable/network/dto/response/notification/ResponseNumberDto.kt rename to core/network/src/main/java/com/teamwable/network/dto/response/notification/ResponseNotificationNumberDto.kt index 2c5ebd2b..1e32a4b0 100644 --- a/core/network/src/main/java/com/teamwable/network/dto/response/notification/ResponseNumberDto.kt +++ b/core/network/src/main/java/com/teamwable/network/dto/response/notification/ResponseNotificationNumberDto.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ResponseNumberDto( +data class ResponseNotificationNumberDto( @SerialName("notificationNumber") val notificationNumber: Int = 0 ) diff --git a/feature/news/src/main/java/com/teamwable/news/NewsFragment.kt b/feature/news/src/main/java/com/teamwable/news/NewsFragment.kt index ebf60a41..15d4e8a1 100644 --- a/feature/news/src/main/java/com/teamwable/news/NewsFragment.kt +++ b/feature/news/src/main/java/com/teamwable/news/NewsFragment.kt @@ -1,7 +1,10 @@ package com.teamwable.news +import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator +import com.teamwable.common.uistate.UiState import com.teamwable.common.util.AmplitudeNewsTag.CLICK_GAMESCHEDULE import com.teamwable.common.util.AmplitudeNewsTag.CLICK_NEWS import com.teamwable.common.util.AmplitudeNewsTag.CLICK_NOTICE @@ -13,10 +16,20 @@ import com.teamwable.ui.extensions.colorOf import com.teamwable.ui.extensions.statusBarColorOf import com.teamwable.ui.extensions.statusBarModeOf import com.teamwable.ui.extensions.stringOf +import com.teamwable.ui.extensions.viewLifeCycle +import com.teamwable.ui.extensions.viewLifeCycleScope import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber @AndroidEntryPoint class NewsFragment : BindingFragment(FragmentNewsBinding::inflate) { + private val viewModel: NewsViewModel by viewModels() + + private var serverNewsNumber = -1 + private var serverNoticeNumber = -1 + override fun initView() { statusBarColorOf(com.teamwable.ui.R.color.black) statusBarModeOf(false) @@ -24,8 +37,48 @@ class NewsFragment : BindingFragment(FragmentNewsBinding::i initNewsViewPagerAdapter() initTabClickListener() - setBadgeOnNews(NewsTabType.NEWS.idx, true) - setBadgeOnNews(NewsTabType.NOTICE.idx, true) + setupNumberObserve() + } + + private fun setupNumberObserve() { + viewModel.newsNumberUiState.flowWithLifecycle(viewLifeCycle).onEach { state -> + when (state) { + is UiState.Success -> { + serverNewsNumber = getServerNumber(state, "news") + serverNoticeNumber = getServerNumber(state, "notice") + + saveNumberFromServerToLocal() + } + + else -> Unit + } + }.launchIn(viewLifeCycleScope) + } + + private fun getServerNumber(state: UiState.Success>, idx: String) = + state.data[idx]?.takeIf { it >= 0 } ?: 0 + + + private suspend fun saveNumberFromServerToLocal() { +// viewModel.saveNewsNumber(1) +// viewModel.saveNoticeNumber(2) + + val localNewsNumber = viewModel.getNewsNumberFromLocal() + val localNoticeNumber = viewModel.getNoticeNumberFromLocal() + + if (serverNewsNumber > localNewsNumber) { + Timber.tag("here").d("news server: $serverNewsNumber, local: $localNewsNumber") + setBadgeOnNews(NewsTabType.NEWS.ordinal, true) + } else { + Timber.tag("here").d("equal news server: $serverNewsNumber, local: $localNewsNumber") + } + + if (serverNoticeNumber > localNoticeNumber) { + Timber.tag("here").d("notice server: $serverNoticeNumber, local: $localNoticeNumber") + setBadgeOnNews(NewsTabType.NOTICE.ordinal, true) + } else { + Timber.tag("here").d("equal notice server: $serverNoticeNumber, local: $localNoticeNumber") + } } private fun setBadgeOnNews(idx: Int, isVisible: Boolean) { @@ -41,10 +94,10 @@ class NewsFragment : BindingFragment(FragmentNewsBinding::i vpNews.adapter = NewsViewPagerAdapter(this@NewsFragment) TabLayoutMediator(tlNews, vpNews) { tab, position -> when (position) { - NewsTabType.MATCH.idx -> tab.text = stringOf(R.string.tv_news_tab_match) - NewsTabType.RANK.idx -> tab.text = stringOf(R.string.tv_news_tab_rank) - NewsTabType.NEWS.idx -> tab.text = stringOf(R.string.tv_news_tab_news) - NewsTabType.NOTICE.idx -> tab.text = stringOf(R.string.tv_news_tab_notice) + NewsTabType.MATCH.ordinal -> tab.text = stringOf(R.string.tv_news_tab_match) + NewsTabType.RANK.ordinal -> tab.text = stringOf(R.string.tv_news_tab_rank) + NewsTabType.NEWS.ordinal -> tab.text = stringOf(R.string.tv_news_tab_news) + NewsTabType.NOTICE.ordinal -> tab.text = stringOf(R.string.tv_news_tab_notice) } }.attach() } @@ -54,16 +107,18 @@ class NewsFragment : BindingFragment(FragmentNewsBinding::i binding.tlNews.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { when (tab?.position) { - NewsTabType.MATCH.idx -> trackEvent(CLICK_GAMESCHEDULE) - NewsTabType.RANK.idx -> trackEvent(CLICK_RANKING) - NewsTabType.NEWS.idx -> { + NewsTabType.MATCH.ordinal -> trackEvent(CLICK_GAMESCHEDULE) + NewsTabType.RANK.ordinal -> trackEvent(CLICK_RANKING) + NewsTabType.NEWS.ordinal -> { trackEvent(CLICK_NEWS) - setBadgeOnNews(NewsTabType.NEWS.idx, false) + setBadgeOnNews(NewsTabType.NEWS.ordinal, false) + viewModel.saveNewsNumber(serverNewsNumber) } - NewsTabType.NOTICE.idx -> { + NewsTabType.NOTICE.ordinal -> { trackEvent(CLICK_NOTICE) - setBadgeOnNews(NewsTabType.NOTICE.idx, false) + setBadgeOnNews(NewsTabType.NOTICE.ordinal, false) + viewModel.saveNoticeNumber(serverNoticeNumber) } } } diff --git a/feature/news/src/main/java/com/teamwable/news/NewsTabType.kt b/feature/news/src/main/java/com/teamwable/news/NewsTabType.kt index a8410f9c..fbde2f62 100644 --- a/feature/news/src/main/java/com/teamwable/news/NewsTabType.kt +++ b/feature/news/src/main/java/com/teamwable/news/NewsTabType.kt @@ -1,10 +1,8 @@ package com.teamwable.news -import androidx.annotation.StringRes - -enum class NewsTabType(@StringRes val idx: Int) { - MATCH(0), - RANK(1), - NEWS(2), - NOTICE(3), +enum class NewsTabType { + MATCH, + RANK, + NEWS, + NOTICE, } diff --git a/feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt b/feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt index f408e9a2..1f434005 100644 --- a/feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt +++ b/feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt @@ -4,17 +4,22 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.teamwable.common.uistate.UiState import com.teamwable.data.repository.NewsRepository +import com.teamwable.data.repository.UserInfoRepository import com.teamwable.model.news.NewsMatchModel import com.teamwable.model.news.NewsRankModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class NewsViewModel -@Inject constructor(private val newsRepository: NewsRepository) : ViewModel() { +@Inject constructor( + private val newsRepository: NewsRepository, + private val userInfoRepository: UserInfoRepository +) : ViewModel() { private val _gameTypeUiState = MutableStateFlow>(UiState.Loading) val gameTypeUiState = _gameTypeUiState.asStateFlow() @@ -24,8 +29,12 @@ class NewsViewModel private val _rankUiState = MutableStateFlow>>(UiState.Loading) val rankUiState = _rankUiState.asStateFlow() + private val _newsNumberUiState = MutableStateFlow>>(UiState.Loading) + val newsNumberUiState = _newsNumberUiState.asStateFlow() + init { getGameType() + getNewsNumber() } private fun getGameType() { @@ -51,4 +60,28 @@ class NewsViewModel .onFailure { _rankUiState.value = UiState.Failure(it.message.toString()) } } } + + private fun getNewsNumber() { + viewModelScope.launch { + newsRepository.getNumber() + .onSuccess { _newsNumberUiState.value = UiState.Success(it) } + .onFailure { _newsNumberUiState.value = UiState.Failure(it.message.toString()) } + } + } + + suspend fun getNewsNumberFromLocal() = userInfoRepository.getNewsNumber().first() + + fun saveNewsNumber(newsNumber: Int) { + viewModelScope.launch { + userInfoRepository.saveNewsNumber(newsNumber) + } + } + + suspend fun getNoticeNumberFromLocal() = userInfoRepository.getNoticeNumber().first() + + fun saveNoticeNumber(noticeNumber: Int) { + viewModelScope.launch { + userInfoRepository.saveNoticeNumber(noticeNumber) + } + } } diff --git a/feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt b/feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt index a13cbdf7..fd6c1b84 100644 --- a/feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt +++ b/feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt @@ -13,10 +13,10 @@ class NewsViewPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) override fun createFragment(position: Int): Fragment { return when (position) { - NewsTabType.MATCH.idx -> NewsMatchFragment() - NewsTabType.RANK.idx -> NewsRankFragment() - NewsTabType.NEWS.idx -> NewsNewsFragment() - NewsTabType.NOTICE.idx -> NewsNoticeFragment() + NewsTabType.MATCH.ordinal -> NewsMatchFragment() + NewsTabType.RANK.ordinal -> NewsRankFragment() + NewsTabType.NEWS.ordinal -> NewsNewsFragment() + NewsTabType.NOTICE.ordinal -> NewsNoticeFragment() else -> NewsMatchFragment() } } diff --git a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeItem.kt b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeItem.kt index b895e199..2a1c0549 100644 --- a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeItem.kt +++ b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeItem.kt @@ -2,15 +2,19 @@ package com.teamwable.news.notice import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.teamwable.designsystem.theme.WableTheme @@ -29,13 +33,30 @@ fun NewsNoticeItem( .clickable { onItemClick(data) } .padding(vertical = 12.dp, horizontal = 20.dp) ) { - Row { - Text(text = data.newsTitle, style = WableTheme.typography.body01) - Spacer(modifier = Modifier.weight(1f)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = data.newsTitle, + style = WableTheme.typography.body01, + modifier = Modifier + .align(Alignment.CenterVertically) + .weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.width(10.dp)) WableNewsTimeText(data.time) } Spacer(modifier = Modifier.height(2.dp)) - Text(text = data.newsText, color = WableTheme.colors.gray600, maxLines = 2, style = WableTheme.typography.body04) + Text( + text = data.newsText, + color = WableTheme.colors.gray600, + style = WableTheme.typography.body04, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) } }