diff --git a/core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseNewsDtoMapper.kt b/core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseNewsDtoMapper.kt index e5852be2..6770a8bf 100644 --- a/core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseNewsDtoMapper.kt +++ b/core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseNewsDtoMapper.kt @@ -6,6 +6,7 @@ import com.teamwable.model.news.NewsMatchScoreModel import com.teamwable.model.news.NewsRankModel import com.teamwable.network.dto.response.news.ResponseGameDto import com.teamwable.network.dto.response.news.ResponseNewsInfoDto +import com.teamwable.network.dto.response.news.ResponseNoticeInfoDto import com.teamwable.network.dto.response.news.ResponseRankDto import com.teamwable.network.dto.response.news.ResponseScheduleDto @@ -43,3 +44,12 @@ internal fun ResponseNewsInfoDto.toNewsInfoModel(): NewsInfoModel = newsText = newsText, time = time, ) + +internal fun ResponseNoticeInfoDto.toNoticeInfoModel(): NewsInfoModel = + NewsInfoModel( + newsId = noticeId, + newsTitle = noticeTitle, + newsImage = noticeImage, + newsText = noticeText, + time = time + ) 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 74db3073..2b37457d 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 @@ -14,4 +14,6 @@ interface NewsRepository { suspend fun getRank(): Result> fun getNewsInfo(): Flow> + + fun getNoticeInfo(): Flow> } 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 9dd3c0ea..f1fb5ace 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 @@ -7,6 +7,7 @@ import androidx.paging.map import com.teamwable.data.mapper.toModel.toNewsInfoModel import com.teamwable.data.mapper.toModel.toNewsMatchModel import com.teamwable.data.mapper.toModel.toNewsRankModel +import com.teamwable.data.mapper.toModel.toNoticeInfoModel import com.teamwable.data.repository.NewsRepository import com.teamwable.model.news.NewsInfoModel import com.teamwable.model.news.NewsMatchModel @@ -49,4 +50,15 @@ internal class DefaultNewsRepository @Inject constructor( pagingData.map { it.toNewsInfoModel() } } } + + override fun getNoticeInfo(): Flow> { + return Pager(PagingConfig(pageSize = 15, prefetchDistance = 1)) { + GenericPagingSource( + apiCall = { cursor -> newsService.getNoticeInfo(cursor).data }, + getNextCursor = { feeds -> feeds.lastOrNull()?.noticeId }, + ) + }.flow.map { pagingData -> + pagingData.map { it.toNoticeInfoModel() } + } + } } 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 056c6643..23f0d3d3 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 @@ -2,6 +2,7 @@ package com.teamwable.network.datasource import com.teamwable.network.dto.response.news.ResponseGameTypeDto import com.teamwable.network.dto.response.news.ResponseNewsInfoDto +import com.teamwable.network.dto.response.news.ResponseNoticeInfoDto import com.teamwable.network.dto.response.news.ResponseRankDto import com.teamwable.network.dto.response.news.ResponseScheduleDto import com.teamwable.network.util.BaseResponse @@ -22,4 +23,9 @@ interface NewsService { suspend fun getNewsInfo( @Query(value = "cursor") contentId: Long = -1, ): BaseResponse> + + @GET("api/v1/information/notice") + suspend fun getNoticeInfo( + @Query(value = "cursor") contentId: Long = -1, + ): BaseResponse> } diff --git a/core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseNoticeInfoDto.kt b/core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseNoticeInfoDto.kt new file mode 100644 index 00000000..cdee7b16 --- /dev/null +++ b/core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseNoticeInfoDto.kt @@ -0,0 +1,18 @@ +package com.teamwable.network.dto.response.news + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseNoticeInfoDto( + @SerialName("noticeId") + val noticeId: Long, + @SerialName("noticeTitle") + val noticeTitle: String, + @SerialName("noticeText") + val noticeText: String, + @SerialName("noticeImage") + val noticeImage: String?, + @SerialName("time") + val time: String, +) diff --git a/core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt b/core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt index 0c370c52..98be7d0b 100644 --- a/core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt +++ b/core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt @@ -2,13 +2,13 @@ package com.teamwable.ui.extensions import android.content.Intent import android.net.Uri -import android.provider.Settings import android.view.View import android.widget.Toast import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar @@ -55,3 +55,9 @@ fun Fragment.statusBarColorOf( fun Fragment.openUri(uri: String) { Intent(Intent.ACTION_VIEW, Uri.parse(uri)).also { startActivity(it) } } + +fun Fragment.statusBarModeOf(isLightStatusBar: Boolean = true) { + requireActivity().window.apply { + WindowInsetsControllerCompat(this, decorView).isAppearanceLightStatusBars = isLightStatusBar + } +} 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 9bdef28f..ebf60a41 100644 --- a/feature/news/src/main/java/com/teamwable/news/NewsFragment.kt +++ b/feature/news/src/main/java/com/teamwable/news/NewsFragment.kt @@ -11,6 +11,7 @@ import com.teamwable.news.databinding.FragmentNewsBinding import com.teamwable.ui.base.BindingFragment 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 dagger.hilt.android.AndroidEntryPoint @@ -18,6 +19,8 @@ import dagger.hilt.android.AndroidEntryPoint class NewsFragment : BindingFragment(FragmentNewsBinding::inflate) { override fun initView() { statusBarColorOf(com.teamwable.ui.R.color.black) + statusBarModeOf(false) + initNewsViewPagerAdapter() initTabClickListener() @@ -74,5 +77,6 @@ class NewsFragment : BindingFragment(FragmentNewsBinding::i override fun onDestroyView() { super.onDestroyView() statusBarColorOf(com.teamwable.ui.R.color.white) + statusBarModeOf() } } 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 b821ad66..f408e9a2 100644 --- a/feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt +++ b/feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.teamwable.common.uistate.UiState import com.teamwable.data.repository.NewsRepository -import com.teamwable.model.news.NewsInfoModel import com.teamwable.model.news.NewsMatchModel import com.teamwable.model.news.NewsRankModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -25,17 +24,6 @@ class NewsViewModel private val _rankUiState = MutableStateFlow>>(UiState.Loading) val rankUiState = _rankUiState.asStateFlow() - val dummyNotice = listOf( - NewsInfoModel(1, "와블 커뮤니티 업데이트 안내", "본문입니다 본문입니다 본문입니다 본문입니다 본문입니다 본문입니다 본문입니다 본문입니다 본문입니다", "www.11", "2024-01-10 11:47:18"), - NewsInfoModel(2, "제목2", "내용2", null, "2024-06-12 20:00:37"), - NewsInfoModel(3, "제목3", "내용3", "www.33", "2024-11-22 04:50:26"), - NewsInfoModel(4, "제목4", "내용4", "www.33", "2024-11-22 04:50:26"), - NewsInfoModel(5, "제목5", "내용5", "www.33", "2024-11-22 04:50:26"), - NewsInfoModel(6, "제목6", "내용6", "www.33", "2024-11-22 04:50:26"), - NewsInfoModel(7, "제목7", "내용7", "www.33", "2024-11-22 04:50:26"), - NewsInfoModel(8, "제목8", "내용8", "www.33", "2024-11-22 04:50:26"), - ) - init { getGameType() } diff --git a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeFragment.kt b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeFragment.kt index 4cb97ac7..bfb0b708 100644 --- a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeFragment.kt +++ b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeFragment.kt @@ -1,7 +1,5 @@ package com.teamwable.news.notice -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.navigation.fragment.findNavController import com.teamwable.designsystem.theme.WableTheme @@ -13,18 +11,16 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class NewsNoticeFragment : BindingFragment(FragmentNewsNoticeBinding::inflate) { - @RequiresApi(Build.VERSION_CODES.O) override fun initView() { initComposeView() } - @RequiresApi(Build.VERSION_CODES.O) private fun initComposeView() { binding.composeNewsNotice.apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { WableTheme { - NewsNoticeRoute(navigateToDetail = { notice -> navigateToDetail(notice) }) + NewsNoticeRoute(navigateToDetail = ::navigateToDetail) } } } 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 cbf5e05a..b895e199 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 @@ -1,8 +1,5 @@ package com.teamwable.news.notice -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -14,31 +11,41 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.teamwable.designsystem.theme.WableTheme import com.teamwable.model.news.NewsInfoModel -import com.teamwable.ui.util.CalculateTime +import com.teamwable.news.news.component.WableNewsTimeText -@RequiresApi(Build.VERSION_CODES.O) @Composable fun NewsNoticeItem( - context: Context, data: NewsInfoModel, - navigateToDetail: (NewsInfoModel) -> Unit + onItemClick: (NewsInfoModel) -> Unit ) { Column( modifier = Modifier .fillMaxWidth() .background(WableTheme.colors.white) - .clickable { navigateToDetail(data) } + .clickable { onItemClick(data) } .padding(vertical = 12.dp, horizontal = 20.dp) ) { Row { Text(text = data.newsTitle, style = WableTheme.typography.body01) Spacer(modifier = Modifier.weight(1f)) - Text(text = CalculateTime().getCalculateTime(context, data.time), color = WableTheme.colors.gray500, style = WableTheme.typography.caption04) + WableNewsTimeText(data.time) } Spacer(modifier = Modifier.height(2.dp)) Text(text = data.newsText, color = WableTheme.colors.gray600, maxLines = 2, style = WableTheme.typography.body04) } } + +@Preview(showBackground = true) +@Composable +fun NewsNoticeItemPreview() { + WableTheme { + NewsNoticeItem( + data = NewsInfoModel(1, "제목", "내용", null, "2024-01-10 11:47:18"), + onItemClick = {} + ) + } +} diff --git a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeRoute.kt b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeRoute.kt index b8fb059b..729d7f11 100644 --- a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeRoute.kt +++ b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeRoute.kt @@ -1,54 +1,107 @@ package com.teamwable.news.notice -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.flowWithLifecycle +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import com.teamwable.designsystem.component.paging.WablePagingSpinner +import com.teamwable.designsystem.component.screen.LoadingScreen import com.teamwable.designsystem.component.screen.NewsNoticeEmptyScreen import com.teamwable.designsystem.theme.WableTheme +import com.teamwable.designsystem.type.ContentType import com.teamwable.model.news.NewsInfoModel -import com.teamwable.news.NewsViewModel import com.teamwable.news.R +import com.teamwable.news.news.model.NewsInfoSideEffect +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOf -@RequiresApi(Build.VERSION_CODES.O) @Composable internal fun NewsNoticeRoute( - viewModel: NewsViewModel = hiltViewModel(), + viewModel: NewsNoticeViewModel = hiltViewModel(), navigateToDetail: (NewsInfoModel) -> Unit ) { - val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val noticeItems = viewModel.noticePagingFlow.collectAsLazyPagingItems() - viewModel.dummyNotice.apply { - if (this.isNotEmpty()) { - NewsNoticeScreen(context = context, notices = this, navigateToDetail = navigateToDetail) - } else { - NewsNoticeEmptyScreen(R.string.tv_news_notice_empty) - } + val isLoading = noticeItems.loadState.refresh is LoadState.Loading + val isEmpty = noticeItems.itemCount == 0 && !isLoading + + LaunchedEffect(lifecycleOwner) { + viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle) + .collectLatest { sideEffect -> + when (sideEffect) { + is NewsInfoSideEffect.NavigateToDetail -> navigateToDetail(sideEffect.newsInfoModel) + else -> Unit + } + } + } + + when { + isLoading -> LoadingScreen() + isEmpty -> NewsNoticeEmptyScreen(emptyTxt = R.string.tv_news_notice_empty) + else -> NewsNoticeScreen( + noticeItems = noticeItems, + onItemClick = viewModel::onItemClick + ) } } -@RequiresApi(Build.VERSION_CODES.O) @Composable fun NewsNoticeScreen( - context: Context, - notices: List, - navigateToDetail: (NewsInfoModel) -> Unit + noticeItems: LazyPagingItems, + onItemClick: (NewsInfoModel) -> Unit ) { LazyColumn(modifier = Modifier.fillMaxSize()) { - items(items = notices, key = { item -> item.newsId }) { notice -> - NewsNoticeItem(context, notice, navigateToDetail) - HorizontalDivider( - thickness = 1.dp, - color = WableTheme.colors.gray200, - ) + items( + count = noticeItems.itemCount, + key = noticeItems.itemKey { it.newsId }, + contentType = noticeItems.itemContentType { ContentType.Item.name }, + ) { idx -> + noticeItems[idx]?.let { + NewsNoticeItem(it, onItemClick) + HorizontalDivider( + thickness = 1.dp, + color = WableTheme.colors.gray200, + ) + } } + item( + key = ContentType.Spinner.name, + contentType = noticeItems.itemContentType { ContentType.Spinner.name }, + ) { + if (noticeItems.loadState.append is LoadState.Loading) { + WablePagingSpinner( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun NewsNoticeScreenPreview() { + WableTheme { + NewsNoticeScreen( + onItemClick = {}, + noticeItems = flowOf(PagingData.from(emptyList())).collectAsLazyPagingItems(), + ) } } diff --git a/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeViewModel.kt b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeViewModel.kt new file mode 100644 index 00000000..7c5dc0cd --- /dev/null +++ b/feature/news/src/main/java/com/teamwable/news/notice/NewsNoticeViewModel.kt @@ -0,0 +1,30 @@ +package com.teamwable.news.notice + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import com.teamwable.data.repository.NewsRepository +import com.teamwable.model.news.NewsInfoModel +import com.teamwable.news.news.model.NewsInfoSideEffect +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NewsNoticeViewModel @Inject constructor( + newsRepository: NewsRepository, +) : ViewModel() { + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow get() = _sideEffect.asSharedFlow() + + val noticePagingFlow = newsRepository.getNoticeInfo().cachedIn(viewModelScope) + + fun onItemClick(newsInfoModel: NewsInfoModel) { + viewModelScope.launch { + _sideEffect.emit(NewsInfoSideEffect.NavigateToDetail(newsInfoModel)) + } + } +}