From 3416e5f8e54daf1bdc30296224156f196da0c37a Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Tue, 18 Jun 2024 17:50:34 +0900 Subject: [PATCH] =?UTF-8?q?Feat=20:=20=EB=B8=8C=EB=A6=AC=ED=95=91=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20API=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dev/briefing/BriefingNavHost.kt | 7 +- .../repository/impl/DefaultScrapRepository.kt | 12 +- .../data/repository/model/BriefingArticle.kt | 9 +- .../app/core/model/BriefingArticle.kt | 8 +- .../app/core/model/BriefingArticleCategory.kt | 1 + .../app/feature/home/HomeNavigation.kt | 6 +- .../app/feature/home/HomeScreen.kt | 11 +- feature/newsdetail/build.gradle.kts | 2 + .../newsdetail/NewsDetailNavigation.kt | 37 +++- .../feature/newsdetail/NewsDetailScreen.kt | 197 ++++++++++++------ .../feature/newsdetail/NewsDetailViewModel.kt | 102 +++++++++ .../newsdetail/src/main/res/values/string.xml | 15 ++ 12 files changed, 314 insertions(+), 93 deletions(-) create mode 100644 feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailViewModel.kt create mode 100644 feature/newsdetail/src/main/res/values/string.xml diff --git a/app/src/main/java/com/dev/briefing/BriefingNavHost.kt b/app/src/main/java/com/dev/briefing/BriefingNavHost.kt index 0b8bf54..c49fd2c 100644 --- a/app/src/main/java/com/dev/briefing/BriefingNavHost.kt +++ b/app/src/main/java/com/dev/briefing/BriefingNavHost.kt @@ -9,6 +9,7 @@ import store.newsbriefing.app.feature.bookmark.bookmarkScreen import store.newsbriefing.app.feature.bookmark.navigateToBookmark import store.newsbriefing.app.feature.home.homeScreen import store.newsbriefing.app.feature.home.navigateToHome +import store.newsbriefing.app.feature.newsdetail.navigateToNewsDetail import store.newsbriefing.app.feature.newsdetail.newsDetailScreen import store.newsbriefing.app.feature.setting.navigateToSetting import store.newsbriefing.app.feature.setting.settingScreen @@ -26,13 +27,15 @@ fun BriefingNavHost( homeScreen( showSnackbar = appState::showSnackBar, navigateToSettingRoute = appState.navController::navigateToSetting, - navigateToBookmarkRoute = appState.navController::navigateToBookmark + navigateToBookmarkRoute = appState.navController::navigateToBookmark, + navigateToNewsDetail = appState.navController::navigateToNewsDetail ) bookmarkScreen( showSnackbar = appState::showSnackBar ) newsDetailScreen( - showSnackbar = appState::showSnackBar + showSnackbar = appState::showSnackBar, + navigateUp = appState.navController::navigateUp ) settingScreen( showSnackbar = appState::showSnackBar diff --git a/core/data/src/main/java/store/newsbriefing/app/core/data/repository/impl/DefaultScrapRepository.kt b/core/data/src/main/java/store/newsbriefing/app/core/data/repository/impl/DefaultScrapRepository.kt index 704b7da..807f44d 100644 --- a/core/data/src/main/java/store/newsbriefing/app/core/data/repository/impl/DefaultScrapRepository.kt +++ b/core/data/src/main/java/store/newsbriefing/app/core/data/repository/impl/DefaultScrapRepository.kt @@ -6,20 +6,22 @@ import store.newsbriefing.app.core.data.repository.ScrapRepository import store.newsbriefing.app.core.data.repository.model.asExternalModel import store.newsbriefing.app.core.model.Scrap import store.newsbriefing.app.core.network.datasource.ScrapNetworkDataSource +import javax.inject.Inject -internal class DefaultScrapRepository(private val scrapNetworkDataSource: ScrapNetworkDataSource) : - ScrapRepository { +internal class DefaultScrapRepository @Inject constructor( + private val scrapNetworkDataSource: ScrapNetworkDataSource +) : ScrapRepository { override fun getScrap(memberId: Long): Flow> = flow { val scraps = scrapNetworkDataSource.getScrap(memberId).map { it.asExternalModel() } emit(scraps) } override suspend fun setScrap(memberId: Long, articleId: Long) { - scrapNetworkDataSource.setScrap(memberId, articleId) + return scrapNetworkDataSource.setScrap(memberId, articleId) } - override suspend fun unScrap(memberId: Long, articleId: Long){ - val result = scrapNetworkDataSource.unScrap(memberId, articleId) + override suspend fun unScrap(memberId: Long, articleId: Long) { + return scrapNetworkDataSource.unScrap(memberId, articleId) } } \ No newline at end of file diff --git a/core/data/src/main/java/store/newsbriefing/app/core/data/repository/model/BriefingArticle.kt b/core/data/src/main/java/store/newsbriefing/app/core/data/repository/model/BriefingArticle.kt index 269561e..4d4fa92 100644 --- a/core/data/src/main/java/store/newsbriefing/app/core/data/repository/model/BriefingArticle.kt +++ b/core/data/src/main/java/store/newsbriefing/app/core/data/repository/model/BriefingArticle.kt @@ -1,10 +1,11 @@ package store.newsbriefing.app.core.data.repository.model -import store.newsbriefing.app.core.common.util.toZoneDateTime import store.newsbriefing.app.core.model.BriefingArticle +import store.newsbriefing.app.core.model.BriefingArticleCategory import store.newsbriefing.app.core.model.BriefingArticleRelated import store.newsbriefing.app.core.model.BriefingArticleSummary import store.newsbriefing.app.core.model.BriefingCategoryArticles +import store.newsbriefing.app.core.model.TimeOfDay import store.newsbriefing.app.core.network.model.NetworkBriefingArticle import store.newsbriefing.app.core.network.model.NetworkBriefingArticleRelated import store.newsbriefing.app.core.network.model.NetworkBriefingArticleSummary @@ -30,15 +31,15 @@ fun NetworkBriefingArticle.asExternalModel(): BriefingArticle { title = title, subtitle = subtitle, content = content, - date = date.toZoneDateTime(), + date = date, articles = articles.map { it.asExternalModel() }, isScrap = isScrap, isBriefingOpen = isBriefingOpen, isWarning = isWarning, scrapCount = scrapCount, gptModel = gptModel, - timeOfDay = timeOfDay, - type = type + timeOfDay = TimeOfDay.fromValue(timeOfDay), + category = BriefingArticleCategory.fromTypeName(type) ) } diff --git a/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticle.kt b/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticle.kt index d8b94ff..8c4be9a 100644 --- a/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticle.kt +++ b/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticle.kt @@ -1,20 +1,18 @@ package store.newsbriefing.app.core.model -import java.time.ZonedDateTime - data class BriefingArticle( val id: Long, val ranks: Int, val title: String, val subtitle: String, val content: String, - val date: ZonedDateTime, + val date: String, val articles: List, val isScrap: Boolean, val isBriefingOpen: Boolean, val isWarning: Boolean, val scrapCount: Int, val gptModel: String, - val timeOfDay: String, - val type: String + val timeOfDay: TimeOfDay, + val category: BriefingArticleCategory ) diff --git a/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticleCategory.kt b/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticleCategory.kt index 248bc28..11ed558 100644 --- a/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticleCategory.kt +++ b/core/model/src/main/java/store/newsbriefing/app/core/model/BriefingArticleCategory.kt @@ -1,4 +1,5 @@ package store.newsbriefing.app.core.model + enum class BriefingArticleCategory(val typeId: String) { KOREA("KOREA"), GLOBAL("GLOBAL"), diff --git a/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeNavigation.kt b/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeNavigation.kt index 45605eb..3e76db4 100644 --- a/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeNavigation.kt +++ b/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeNavigation.kt @@ -13,7 +13,8 @@ fun NavController.navigateToHome() { fun NavGraphBuilder.homeScreen( showSnackbar: (String) -> Unit, navigateToBookmarkRoute: () -> Unit, - navigateToSettingRoute: () -> Unit + navigateToSettingRoute: () -> Unit, + navigateToNewsDetail: (String) -> Unit ) { composable( route = homeRoute @@ -21,7 +22,8 @@ fun NavGraphBuilder.homeScreen( HomeRoute( showSnackbar = showSnackbar, navigateToBookmarkRoute = navigateToBookmarkRoute, - navigateToSettingRoute = navigateToSettingRoute + navigateToSettingRoute = navigateToSettingRoute, + navigateToNewsDetail = navigateToNewsDetail ) } } \ No newline at end of file diff --git a/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeScreen.kt b/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeScreen.kt index 886f623..90f40da 100644 --- a/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeScreen.kt +++ b/feature/home/src/main/java/store/newsbriefing/app/feature/home/HomeScreen.kt @@ -66,6 +66,7 @@ internal fun HomeRoute( showSnackbar: (String) -> Unit, navigateToBookmarkRoute: () -> Unit, navigateToSettingRoute: () -> Unit, + navigateToNewsDetail: (String) -> Unit, homeViewModel: HomeViewModel = hiltViewModel() ) { val uiState by homeViewModel.uiState.collectAsStateWithLifecycle( @@ -87,7 +88,8 @@ internal fun HomeRoute( showSnackbar = showSnackbar, loadBriefings = homeViewModel::loadBriefings, navigateToBookmarkRoute = navigateToBookmarkRoute, - navigateToSettingRoute = navigateToSettingRoute + navigateToSettingRoute = navigateToSettingRoute, + navigateToNewsDetail = navigateToNewsDetail ) } @@ -112,7 +114,8 @@ internal fun HomeScreen( loadBriefings: (BriefingArticleCategory, Boolean) -> Unit, showSnackbar: (String) -> Unit = { }, navigateToBookmarkRoute: () -> Unit = { }, - navigateToSettingRoute: () -> Unit = { } + navigateToSettingRoute: () -> Unit = { }, + navigateToNewsDetail: (String) -> Unit ) { Column( modifier = Modifier @@ -170,8 +173,8 @@ internal fun HomeScreen( else -> emptyList() } } ?: emptyList() - ) { - + ) { articleId -> + navigateToNewsDetail("$articleId") } } } diff --git a/feature/newsdetail/build.gradle.kts b/feature/newsdetail/build.gradle.kts index d9f5c3b..d3308af 100644 --- a/feature/newsdetail/build.gradle.kts +++ b/feature/newsdetail/build.gradle.kts @@ -9,6 +9,8 @@ android { } dependencies { + implementation(projects.core.data) + api(projects.core.model) api(projects.core.common) api(projects.core.designsystem) diff --git a/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailNavigation.kt b/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailNavigation.kt index 2d5b030..3b1e2ff 100644 --- a/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailNavigation.kt +++ b/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailNavigation.kt @@ -1,23 +1,50 @@ package store.newsbriefing.app.feature.newsdetail +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.NavType import androidx.navigation.compose.composable +import androidx.navigation.navArgument +import java.net.URLDecoder +import java.net.URLEncoder +import kotlin.text.Charsets.UTF_8 +private val URL_CHARACTER_ENCODING = UTF_8.name() + +internal const val NEWS_ID_ARG = "newsId" const val NewsDetailRoute = "news_detail_route" -fun NavController.navigateToNewsDetail() { - navigate(NewsDetailRoute) +internal class NewsDetailArgs(val newsId: String) { + constructor(savedStateHandle: SavedStateHandle) : + this(URLDecoder.decode(checkNotNull(savedStateHandle[NEWS_ID_ARG]), URL_CHARACTER_ENCODING)) +} + +fun NavController.navigateToNewsDetail(newsId: String, navOptions: NavOptionsBuilder.() -> Unit = {}) { + navigate(createNewsDetailRoute(newsId)) { + navOptions() + } +} + +fun createNewsDetailRoute(newsId: String): String { + val encodedId = URLEncoder.encode(newsId, URL_CHARACTER_ENCODING) + return "$NewsDetailRoute/$encodedId" } fun NavGraphBuilder.newsDetailScreen( - showSnackbar: (String) -> Unit + showSnackbar: (String) -> Unit, + navigateUp: () -> Unit ) { composable( - route = NewsDetailRoute + route = "${NewsDetailRoute}/{$NEWS_ID_ARG}", + arguments = listOf( + navArgument(NEWS_ID_ARG) { type = NavType.StringType }, + ), ) { NewsDetailRoute( - showSnackbar = showSnackbar + showSnackbar = showSnackbar, + navigateUp = navigateUp ) } } \ No newline at end of file diff --git a/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailScreen.kt b/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailScreen.kt index 54a59fa..ddf2837 100644 --- a/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailScreen.kt +++ b/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailScreen.kt @@ -14,101 +14,137 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat.startActivity +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.flow.collectLatest +import store.newsbriefing.app.core.designsystem.LoadingDialog import store.newsbriefing.app.core.designsystem.theme.BriefingTheme import store.newsbriefing.app.core.designsystem.theme.Pretendard +import store.newsbriefing.app.core.model.BriefingArticleCategory import store.newsbriefing.app.core.model.BriefingArticleRelated +import store.newsbriefing.app.core.model.TimeOfDay @Composable internal fun NewsDetailRoute( - showSnackbar: (String) -> Unit + showSnackbar: (String) -> Unit, + navigateUp: () -> Unit, + newsDetailViewModel: NewsDetailViewModel = hiltViewModel() ) { - NewsDetailScreen( - showSnackbar = showSnackbar + val uiState by newsDetailViewModel.uiState.collectAsStateWithLifecycle( + lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current ) -} -@Preview -@Composable -fun NewsDetailScreenPreview() { - BriefingTheme { - NewsDetailScreen( - showSnackbar = {} - ) + LaunchedEffect(Unit) { + newsDetailViewModel.eventFlow.collectLatest { event -> + when (event) { + is NewsDetailEvent.ErrorOccurred -> { + showSnackbar(event.message) + } + } + } } + + NewsDetailScreen( + uiState = uiState, + setScrap = newsDetailViewModel::setScrap, + unScrap = newsDetailViewModel::unScrap, + showSnackbar = showSnackbar, + navigateUp = navigateUp + ) } @Composable internal fun NewsDetailScreen( - showSnackbar: (String) -> Unit + uiState: NewsDetailUiState, + setScrap: (Long) -> Unit, + unScrap: (Long) -> Unit, + showSnackbar: (String) -> Unit, + navigateUp: () -> Unit ) { val context = LocalContext.current - Column( - modifier = Modifier - .fillMaxSize() - .background(BriefingTheme.colorScheme.BackgroundWhite) - ) { - TopBar(onBack = { }) { - + when (uiState.article) { + is BriefingArticleUiState.Loading -> { + LoadingDialog() } + is BriefingArticleUiState.Error -> { + // Error + } + is BriefingArticleUiState.Success -> { + val news = uiState.article.data - NewsDetailHeader( - title = "배터리 혁명", - category = "사회 1" - ) - - NewsSummarySection( - title = "2차 전지 혁명으로 인한 놀라운 발견과 문제 해결", - content = "배터리 혁명은 현대 산업과 일상 생활에 혁명적인 변화를 가져왔다. 전기 자동차 및 이동식 장치들은 더 큰 용량과 효율성을 가진 배터리로 긴 주행거리와 높은 성능을 실현하였다. 또한 재생 에너지 저장 시스템으로 활용되어 전력 그리드 안정성을 증진시키고 친환경 에너지 전환을 촉진하고 있다. 연구의 진보로 배터리 수명과 충전 시간이 개선되며, 이는 모바일 기기부터 심지어 대규모 에너지 저장까지 다양한 분야에서 혁신을 이뤄내고 있다", - ) + Column( + modifier = Modifier + .fillMaxSize() + .background(BriefingTheme.colorScheme.BackgroundWhite) + .verticalScroll(rememberScrollState()) + ) { + TopBar( + onBack = navigateUp, + ) { + // 공유하기 기능 + } + + NewsDetailHeader( + title = news.title, + category = news.category, + ranks = news.ranks, + date = news.date, + timeOfDay = news.timeOfDay, + gptModel = news.gptModel + ) - Spacer(modifier = Modifier.height(65.dp)) - - ScrapButton( - modifier = Modifier.align(Alignment.CenterHorizontally), - isBookmarked = false - ) { - - } + NewsSummarySection( + title = news.title, + content = news.content, + ) - RelatedNewsSection( - relatedNewsList = listOf( - BriefingArticleRelated( - id = 1, - press = "KBS충북", - title = "배터리 혁명은 현대 산업과 일상 생활에 혁명적인 변화를 가져왔다.", - url = "https://naver.com" - ), - BriefingArticleRelated( - id = 2, - press = "KBS충북", - title = "배터리 혁명은 현대 산업과 일상 생활에 혁명적인 변화를 가져왔다.", - url = "https://naver.com" - ), - ) - ) { url -> - val webPage: Uri = Uri.parse(url) - val intent = Intent(Intent.ACTION_VIEW, webPage) - startActivity(context, intent, null) + Spacer(modifier = Modifier.height(65.dp)) + + ScrapButton( + modifier = Modifier.align(Alignment.CenterHorizontally), + isBookmarked = news.isScrap + ) { + if (news.isScrap) { + unScrap(news.id) + } else { + setScrap(news.id) + } + } + + Spacer(modifier = Modifier.height(43.dp)) + + RelatedNewsSection( + relatedNewsList = news.articles + ) { url -> + val webPage: Uri = Uri.parse(url) + val intent = Intent(Intent.ACTION_VIEW, webPage) + startActivity(context, intent, null) + } + } } } } @@ -184,7 +220,11 @@ private fun NewsSummarySection( private fun NewsDetailHeader( modifier: Modifier = Modifier, title: String, - category: String + category: BriefingArticleCategory, + ranks: Int, + date: String, + timeOfDay: TimeOfDay, + gptModel: String ) { Column( modifier = modifier @@ -192,7 +232,10 @@ private fun NewsDetailHeader( ) { Spacer(modifier = Modifier.height(13.dp)) - NewsCategoryLabel(category = category) + NewsCategoryLabel( + category = category, + ranks = ranks + ) Spacer(modifier = Modifier.height(13.dp)) @@ -208,7 +251,10 @@ private fun NewsDetailHeader( Spacer(modifier = Modifier.height(7.dp)) - NewsDate(date = "2023.10.31 아침") + NewsDate( + date = "$date ${stringResource(id = timeOfDay.getStringResId())}", + gptModel = gptModel + ) Spacer(modifier = Modifier.height(11.dp)) @@ -226,7 +272,8 @@ private fun NewsDetailHeader( @Composable private fun NewsCategoryLabel( - category: String + category: BriefingArticleCategory, + ranks: Int ) { Box( modifier = Modifier @@ -240,7 +287,7 @@ private fun NewsCategoryLabel( ) ) { Text( - text = category, + text = "${stringResource(id = category.getStringResId())} $ranks", style = TextStyle( fontFamily = Pretendard, fontWeight = FontWeight.Normal, @@ -254,7 +301,8 @@ private fun NewsCategoryLabel( @Composable private fun NewsDate( - date: String + date: String, + gptModel: String ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -281,7 +329,7 @@ private fun NewsDate( ) Text( - text = "GPT-3로 생성됨", + text = stringResource(id = R.string.generated_engine, gptModel), style = TextStyle( fontFamily = Pretendard, fontWeight = FontWeight.Normal, @@ -318,7 +366,7 @@ private fun ScrapButton( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "+ 스크랩", + text = stringResource(id = R.string.scrap), style = TextStyle( fontFamily = Pretendard, fontWeight = FontWeight.Medium, @@ -338,10 +386,10 @@ private fun RelatedNewsSection( onClickNews: (String) -> Unit ) { Column( - modifier = Modifier.padding(horizontal = 21.dp) + modifier = modifier.padding(horizontal = 21.dp) ) { Text( - text = "관련 기사", + text = stringResource(id = R.string.related_articles), style = TextStyle( fontFamily = Pretendard, fontWeight = FontWeight.SemiBold, @@ -432,4 +480,21 @@ private fun RelatedNewsItem( color = BriefingTheme.colorScheme.TextGray ) } +} + +private fun TimeOfDay.getStringResId(): Int { + return when (this) { + TimeOfDay.MORNING -> R.string.time_of_day_morning + TimeOfDay.EVENING -> R.string.time_of_day_evening + } +} + +private fun BriefingArticleCategory.getStringResId(): Int { + return when (this) { + BriefingArticleCategory.KOREA -> R.string.category_korea + BriefingArticleCategory.GLOBAL -> R.string.category_global + BriefingArticleCategory.SOCIAL -> R.string.category_social + BriefingArticleCategory.SCIENCE -> R.string.category_science + BriefingArticleCategory.ECONOMY -> R.string.category_economy + } } \ No newline at end of file diff --git a/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailViewModel.kt b/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailViewModel.kt new file mode 100644 index 0000000..f11c51c --- /dev/null +++ b/feature/newsdetail/src/main/java/store/newsbriefing/app/feature/newsdetail/NewsDetailViewModel.kt @@ -0,0 +1,102 @@ +package store.newsbriefing.app.feature.newsdetail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import store.newsbriefing.app.core.common.util.EventFlow +import store.newsbriefing.app.core.common.util.MutableEventFlow +import store.newsbriefing.app.core.common.util.asEventFlow +import store.newsbriefing.app.core.data.repository.BriefingRepository +import store.newsbriefing.app.core.data.repository.ScrapRepository +import store.newsbriefing.app.core.data.repository.impl.DefaultMemberTokenRepository +import store.newsbriefing.app.core.model.BriefingArticle +import javax.inject.Inject + +sealed class NewsDetailEvent { + data class ErrorOccurred(val message: String) : NewsDetailEvent() +} + +data class NewsDetailUiState( + val article: BriefingArticleUiState = BriefingArticleUiState.Loading +) + +sealed interface BriefingArticleUiState { + data class Success(val data: BriefingArticle) : BriefingArticleUiState + data object Error : BriefingArticleUiState + data object Loading : BriefingArticleUiState +} + +@HiltViewModel +class NewsDetailViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val briefingRepository: BriefingRepository, + private val scrapRepository: ScrapRepository, + private val memberTokenRepository: DefaultMemberTokenRepository +) : ViewModel() { + + private val newsDetailArgs = NewsDetailArgs(savedStateHandle) + private val newsId = newsDetailArgs.newsId.toLong() + + val uiState: StateFlow + get() = _uiState.asStateFlow() + private val _uiState: MutableStateFlow = MutableStateFlow(NewsDetailUiState()) + + val eventFlow: EventFlow + get() = _eventFlow.asEventFlow() + private val _eventFlow: MutableEventFlow = MutableEventFlow() + + init { + viewModelScope.launch { + loadBriefingArticle(newsId) + } + } + + fun loadBriefingArticle(id: Long) = viewModelScope.launch { + briefingRepository.getBriefingArticle(articleId = id).collect { news -> + _uiState.update { + it.copy( + article = BriefingArticleUiState.Success(news) + ) + } + } + } + + fun setScrap(id: Long) { + viewModelScope.launch { + val memberId = memberTokenRepository.getMemberToken().first().memberId + scrapRepository.setScrap(memberId, id) + _uiState.update { currentState -> + currentState.copy( + article = when (val currentNews = currentState.article) { + is BriefingArticleUiState.Success -> + currentNews.copy(data = currentNews.data.copy(isScrap = true)) + else -> currentNews + } + ) + } + } + } + + fun unScrap(id: Long) { + viewModelScope.launch { + val memberId = memberTokenRepository.getMemberToken().first().memberId + scrapRepository.unScrap(memberId, id) + _uiState.update { currentState -> + currentState.copy( + article = when (val currentNews = currentState.article) { + is BriefingArticleUiState.Success -> + currentNews.copy(data = currentNews.data.copy(isScrap = false)) + else -> currentNews + } + ) + } + } + } +} \ No newline at end of file diff --git a/feature/newsdetail/src/main/res/values/string.xml b/feature/newsdetail/src/main/res/values/string.xml new file mode 100644 index 0000000..2c26dfb --- /dev/null +++ b/feature/newsdetail/src/main/res/values/string.xml @@ -0,0 +1,15 @@ + + + 한국 + 세계 + 사회 + 과학 + 경제 + + 아침 + 저녁 + + %s로 생성됨 + + 스크랩 + 관련 기사 + \ No newline at end of file