diff --git a/core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt b/core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt
new file mode 100644
index 00000000..9c93ef44
--- /dev/null
+++ b/core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt
@@ -0,0 +1,67 @@
+package com.yapp.ndgl.core.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.yapp.ndgl.core.ui.theme.NDGLTheme
+
+@Composable
+fun CommonErrorView(
+ modifier: Modifier = Modifier,
+ title: String? = null,
+ description: String? = null,
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 165.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = painterResource(R.drawable.img_empty_browser),
+ contentDescription = null,
+ modifier = Modifier.size(100.dp),
+ )
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = title ?: stringResource(R.string.common_error_title),
+ color = NDGLTheme.colors.black500,
+ textAlign = TextAlign.Center,
+ style = NDGLTheme.typography.subtitleMdSemiBold,
+ )
+ Text(
+ text = description ?: stringResource(R.string.common_error_description),
+ color = NDGLTheme.colors.black400,
+ textAlign = TextAlign.Center,
+ style = NDGLTheme.typography.bodyLgRegular,
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun CommonErrorViewPreview() {
+ NDGLTheme {
+ CommonErrorView(
+ modifier = Modifier,
+ )
+ }
+}
diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml
index 2207fe15..5f067cb4 100644
--- a/core/ui/src/main/res/values/strings.xml
+++ b/core/ui/src/main/res/values/strings.xml
@@ -15,6 +15,9 @@
•
다시 시도
전체
+ 알 수 없는 오류입니다
+ 정보를 불러올 수 없어요
+ 인터넷 연결 확인 후 다시 시도해 주세요
서비스 이용 전\n반드시 확인해주세요.
diff --git a/data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt b/data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt
index 602253b6..7bcfa65c 100644
--- a/data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt
+++ b/data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt
@@ -18,13 +18,14 @@ import javax.inject.Singleton
class TravelTemplateRepository @Inject constructor(
private val travelTemplateApi: TravelTemplateApi,
) {
- suspend fun getAllPopularTravelTemplates(): PopularTravelTemplates {
- return travelTemplateApi.getPopularTravelTemplates().getData()
+ suspend fun getAllPopularTravelTemplates(page: Int = 0): PopularTravelTemplates {
+ return travelTemplateApi.getPopularTravelTemplates(page = page).getData()
}
- suspend fun getPopularTravelTemplates(travelProgramId: Long): PopularTravelTemplates {
+ suspend fun getPopularTravelTemplates(travelProgramId: Long, page: Int = 0): PopularTravelTemplates {
return travelTemplateApi.getPopularTravelTemplates(
travelProgramId = travelProgramId,
+ page = page,
).getData()
}
diff --git a/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt b/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt
index 582b6bf6..9737aaf6 100644
--- a/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt
+++ b/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt
@@ -1,6 +1,7 @@
package com.yapp.ndgl.feature.home.popular
import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
import com.yapp.ndgl.core.base.UiIntent
import com.yapp.ndgl.core.base.UiSideEffect
import com.yapp.ndgl.core.base.UiState
@@ -9,32 +10,50 @@ import com.yapp.ndgl.feature.home.model.TravelProgramTab
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
-import kotlinx.collections.immutable.persistentMapOf
-
-@Immutable
-data class PopularTravelListState(
- val travelProgramTabs: ImmutableList = persistentListOf(),
- val selectedTabIndex: Int = 0,
- val allPopularTravels: ImmutableList = persistentListOf(),
- val popularTravelsByProgram: ImmutableMap> = persistentMapOf(),
-) : UiState {
- val selectedProgramTravels: ImmutableList by lazy {
- val selectTab = travelProgramTabs.getOrElse(selectedTabIndex) { TravelProgramTab.All }
-
- when (selectTab) {
- TravelProgramTab.All -> allPopularTravels
- is TravelProgramTab.Custom -> popularTravelsByProgram.getOrDefault(
- selectTab.programId,
- persistentListOf(),
- )
+
+@Stable
+sealed class PopularTravelListState : UiState {
+ data object Loading : PopularTravelListState()
+
+ @Immutable
+ data class Success(
+ val travelProgramTabs: ImmutableList,
+ val allPopularTravels: ImmutableList,
+ val popularTravelsByProgram: ImmutableMap>,
+ val selectedTabIndex: Int = 0,
+ ) : PopularTravelListState() {
+ val selectedProgramTravels: ImmutableList by lazy {
+ val selectTab = travelProgramTabs.getOrElse(selectedTabIndex) { TravelProgramTab.All }
+
+ when (selectTab) {
+ TravelProgramTab.All -> allPopularTravels
+ is TravelProgramTab.Custom -> popularTravelsByProgram.getOrDefault(
+ selectTab.programId,
+ persistentListOf(),
+ )
+ }
+ }
+
+ sealed interface PopularTravelListItem {
+ data class Travel(
+ val travelContent: TravelContent,
+ ) : PopularTravelListItem
+
+ data class Loading(
+ val nextPage: Int,
+ ) : PopularTravelListItem
}
}
+
+ data object Error : PopularTravelListState()
}
sealed interface PopularTravelListIntent : UiIntent {
data object ClickSearchTravelTemplate : PopularTravelListIntent
data class SelectPopularTravelTab(val index: Int) : PopularTravelListIntent
data class ClickTravel(val travelId: Long, val days: Int) : PopularTravelListIntent
+ data class LoadMore(val nextPage: Int) : PopularTravelListIntent
+ data object ClickRetry : PopularTravelListIntent
}
sealed interface PopularTravelListSideEffect : UiSideEffect {
@@ -43,4 +62,10 @@ sealed interface PopularTravelListSideEffect : UiSideEffect {
val travelId: Long,
val days: Int,
) : PopularTravelListSideEffect
+
+ data class ShowSnackBar(val type: Type) : PopularTravelListSideEffect {
+ enum class Type {
+ ERR_UNKNOWN,
+ }
+ }
}
diff --git a/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt b/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt
index ad558016..345313d0 100644
--- a/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt
+++ b/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt
@@ -2,6 +2,7 @@ package com.yapp.ndgl.feature.home.popular
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -10,24 +11,45 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+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.lifecycle.viewmodel.compose.hiltViewModel
+import com.yapp.ndgl.core.ui.CommonErrorView
+import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButton
+import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButtonAttr
import com.yapp.ndgl.core.ui.designsystem.NDGLChipTab
import com.yapp.ndgl.core.ui.designsystem.NDGLChipTabAttr
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationIcon
+import com.yapp.ndgl.core.ui.designsystem.NDGLSnackbar
import com.yapp.ndgl.core.ui.theme.NDGLTheme
+import com.yapp.ndgl.data.travel.model.ProgramType
import com.yapp.ndgl.feature.home.R
import com.yapp.ndgl.feature.home.component.TravelTemplate
+import com.yapp.ndgl.feature.home.model.TravelContent
import com.yapp.ndgl.feature.home.model.TravelProgramTab
+import com.yapp.ndgl.feature.home.popular.PopularTravelListSideEffect.ShowSnackBar.Type
+import com.yapp.ndgl.feature.home.popular.PopularTravelListState.Success.PopularTravelListItem
import com.yapp.ndgl.feature.home.util.toIconRes
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.launch
import com.yapp.ndgl.core.ui.R as CoreR
@Composable
@@ -39,8 +61,13 @@ internal fun PopularTravelListRoute(
) {
val state by viewModel.collectAsState()
+ val snackbarHostState = remember { SnackbarHostState() }
+ val coroutineScope = rememberCoroutineScope()
+ val errUnknownMessage = stringResource(CoreR.string.common_err_unknown)
+
PopularTravelListScreen(
state = state,
+ snackbarHostState = snackbarHostState,
goBack = { goBack() },
onSearchClick = {
viewModel.onIntent(PopularTravelListIntent.ClickSearchTravelTemplate)
@@ -51,6 +78,12 @@ internal fun PopularTravelListRoute(
onTravelClick = { travelId, days ->
viewModel.onIntent(PopularTravelListIntent.ClickTravel(travelId, days))
},
+ onLoadMore = { nextPage ->
+ viewModel.onIntent(PopularTravelListIntent.LoadMore(nextPage))
+ },
+ onRetryClick = {
+ viewModel.onIntent(PopularTravelListIntent.ClickRetry)
+ },
)
viewModel.collectSideEffect { sideEffect ->
@@ -60,6 +93,15 @@ internal fun PopularTravelListRoute(
sideEffect.travelId,
sideEffect.days,
)
+
+ is PopularTravelListSideEffect.ShowSnackBar -> {
+ val message = when (sideEffect.type) {
+ Type.ERR_UNKNOWN -> errUnknownMessage
+ }
+ coroutineScope.launch {
+ snackbarHostState.showSnackbar(message)
+ }
+ }
}
}
}
@@ -67,10 +109,13 @@ internal fun PopularTravelListRoute(
@Composable
private fun PopularTravelListScreen(
state: PopularTravelListState,
+ snackbarHostState: SnackbarHostState,
goBack: () -> Unit,
onSearchClick: () -> Unit,
onTabSelected: (Int) -> Unit,
onTravelClick: (Long, Int) -> Unit,
+ onLoadMore: (Int) -> Unit,
+ onRetryClick: () -> Unit,
) {
Scaffold(
topBar = {
@@ -91,51 +136,208 @@ private fun PopularTravelListScreen(
},
)
},
+ snackbarHost = {
+ SnackbarHost(snackbarHostState) { data ->
+ NDGLSnackbar(
+ modifier = Modifier.padding(bottom = 24.dp),
+ snackbarData = data,
+ )
+ }
+ },
) { innerPadding ->
- Column(
- modifier = Modifier
- .fillMaxSize()
- .padding(innerPadding),
- verticalArrangement = Arrangement.spacedBy(12.dp),
- ) {
- NDGLChipTab(
- tabs = state.travelProgramTabs.map { tab ->
- when (tab) {
- TravelProgramTab.All -> NDGLChipTabAttr.Tab(
- tag = "All",
- name = stringResource(CoreR.string.common_all),
- )
-
- is TravelProgramTab.Custom -> NDGLChipTabAttr.Tab(
- tag = tab.programId.toString(),
- name = tab.name,
- leadingIcon = tab.type.toIconRes(),
- )
- }
- }.toPersistentList(),
- selectedIndex = state.selectedTabIndex,
+ when (state) {
+ PopularTravelListState.Loading -> Unit
+ is PopularTravelListState.Success -> PopularTravelListContent(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ state = state,
onTabSelected = onTabSelected,
- modifier = Modifier.padding(start = 24.dp, top = 20.dp),
+ onTravelClick = onTravelClick,
+ onLoadMore = onLoadMore,
+ )
+
+ PopularTravelListState.Error -> ErrorView(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ onRetryClick = onRetryClick,
)
+ }
+ }
+}
+
+@Composable
+private fun PopularTravelListContent(
+ modifier: Modifier,
+ state: PopularTravelListState.Success,
+ onTabSelected: (Int) -> Unit,
+ onTravelClick: (Long, Int) -> Unit,
+ onLoadMore: (Int) -> Unit,
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ NDGLChipTab(
+ tabs = state.travelProgramTabs.map { tab ->
+ when (tab) {
+ TravelProgramTab.All -> NDGLChipTabAttr.Tab(
+ tag = "All",
+ name = stringResource(CoreR.string.common_all),
+ )
+
+ is TravelProgramTab.Custom -> NDGLChipTabAttr.Tab(
+ tag = tab.programId.toString(),
+ name = tab.name,
+ leadingIcon = tab.type.toIconRes(),
+ )
+ }
+ }.toPersistentList(),
+ selectedIndex = state.selectedTabIndex,
+ onTabSelected = onTabSelected,
+ modifier = Modifier.padding(start = 24.dp, top = 20.dp),
+ )
- LazyColumn(
- modifier = Modifier.weight(1f),
- contentPadding = PaddingValues(top = 12.dp, bottom = 80.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
- ) {
- items(
- items = state.selectedProgramTravels,
- key = { travelTemplate -> travelTemplate.travelId },
- ) { travel ->
- TravelTemplate(
- travel = travel,
+ LazyColumn(
+ modifier = Modifier.weight(1f),
+ contentPadding = PaddingValues(top = 12.dp, bottom = 80.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ items(
+ items = state.selectedProgramTravels,
+ key = { item ->
+ when (item) {
+ is PopularTravelListItem.Travel -> item.travelContent.travelId
+ is PopularTravelListItem.Loading -> "loading_${item.nextPage}"
+ }
+ },
+ ) { item ->
+ when (item) {
+ is PopularTravelListItem.Travel -> TravelTemplate(
+ travel = item.travelContent,
onTravelTemplateClick = onTravelClick,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
)
+
+ is PopularTravelListItem.Loading -> LoadingItem(
+ nextPage = item.nextPage,
+ onLoadMore = onLoadMore,
+ )
}
}
}
}
}
+
+@Composable
+private fun LoadingItem(
+ nextPage: Int,
+ onLoadMore: (Int) -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircularProgressIndicator(color = NDGLTheme.colors.green500)
+ }
+
+ LaunchedEffect(Unit) {
+ onLoadMore(nextPage)
+ }
+}
+
+@Composable
+private fun ErrorView(
+ modifier: Modifier,
+ onRetryClick: () -> Unit,
+) {
+ val scrollState = rememberScrollState()
+
+ Column(modifier = modifier) {
+ CommonErrorView(
+ modifier = Modifier
+ .verticalScroll(scrollState)
+ .weight(1f),
+ )
+ NDGLCTAButton(
+ type = NDGLCTAButtonAttr.Type.PRIMARY,
+ size = NDGLCTAButtonAttr.Size.LARGE,
+ status = NDGLCTAButtonAttr.Status.ACTIVE,
+ label = stringResource(CoreR.string.common_retry),
+ onClick = onRetryClick,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp, vertical = 16.dp),
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun PopularTravelListScreenPreview() {
+ val travelContent = TravelContent(
+ travelId = 1L,
+ title = "Sample Travel",
+ country = "US",
+ countryName = "미국",
+ city = "뉴욕",
+ nights = 2,
+ days = 3,
+ programName = "Sample Program",
+ programType = ProgramType.YOUTUBE,
+ thumbnail = "",
+ )
+
+ val state = PopularTravelListState.Success(
+ travelProgramTabs = persistentListOf(
+ TravelProgramTab.All,
+ TravelProgramTab.Custom(1L, "Youtube", ProgramType.YOUTUBE),
+ ),
+ allPopularTravels = persistentListOf(
+ PopularTravelListItem.Travel(travelContent),
+ PopularTravelListItem.Travel(travelContent.copy(travelId = 2L)),
+ PopularTravelListItem.Travel(travelContent.copy(travelId = 3L)),
+ ),
+ popularTravelsByProgram = persistentMapOf(),
+ )
+
+ NDGLTheme {
+ PopularTravelListScreen(
+ state = state,
+ snackbarHostState = remember { SnackbarHostState() },
+ goBack = {},
+ onSearchClick = {},
+ onTabSelected = {},
+ onTravelClick = { _, _ -> },
+ onLoadMore = {},
+ onRetryClick = {},
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun LoadingItemPreview() {
+ NDGLTheme {
+ LoadingItem(
+ nextPage = 1,
+ onLoadMore = {},
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun ErrorViewPreview() {
+ NDGLTheme {
+ ErrorView(
+ modifier = Modifier.fillMaxSize(),
+ onRetryClick = {},
+ )
+ }
+}
diff --git a/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt b/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt
index 20d5600c..7c5fc18b 100644
--- a/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt
+++ b/feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt
@@ -9,6 +9,8 @@ import com.yapp.ndgl.data.travel.repository.TravelProgramRepository
import com.yapp.ndgl.data.travel.repository.TravelTemplateRepository
import com.yapp.ndgl.feature.home.model.TravelContent
import com.yapp.ndgl.feature.home.model.TravelProgramTab
+import com.yapp.ndgl.feature.home.popular.PopularTravelListSideEffect.ShowSnackBar.Type
+import com.yapp.ndgl.feature.home.popular.PopularTravelListState.Success.PopularTravelListItem
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -17,7 +19,6 @@ import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
-import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
@@ -25,8 +26,11 @@ class PopularTravelListViewModel @Inject constructor(
private val travelProgramRepository: TravelProgramRepository,
private val travelTemplateRepository: TravelTemplateRepository,
) : BaseViewModel(
- initialState = PopularTravelListState(),
+ initialState = PopularTravelListState.Loading,
) {
+ @Volatile
+ private var isLoadingMore = false
+
init {
loadPopularTemplates()
}
@@ -37,8 +41,7 @@ class PopularTravelListViewModel @Inject constructor(
.onSuccess { programs ->
loadPopularTemplatesByPrograms(programs = programs)
}.onFailure {
- // FIXME: 에러 뷰
- Timber.d("fail to load popular $it")
+ reduce { PopularTravelListState.Error }
}
}
}
@@ -69,22 +72,22 @@ class PopularTravelListViewModel @Inject constructor(
}
}.toImmutableList()
- val allTravels = allTemplateDeferred.await()
- ?.content
- ?.map { it.toTravelContent() }
- ?.toImmutableList()
- ?: persistentListOf()
+ val allResult = allTemplateDeferred.await()
+ val allTravels = buildList {
+ allResult?.content?.forEach { add(PopularTravelListItem.Travel(it.toTravelContent())) }
+ if (allResult?.hasNext == true) add(PopularTravelListItem.Loading(nextPage = 1))
+ }.toImmutableList()
- val travelsByProgram = mutableMapOf>()
+ val travelsByProgram = mutableMapOf>()
popularTemplateDeferred.awaitAll().forEach { (program, result) ->
- travelsByProgram[program.id] = result?.content
- ?.map { it.toTravelContent() }
- ?.toImmutableList()
- ?: persistentListOf()
+ travelsByProgram[program.id] = buildList {
+ result?.content?.forEach { add(PopularTravelListItem.Travel(it.toTravelContent())) }
+ if (result?.hasNext == true) add(PopularTravelListItem.Loading(nextPage = 1))
+ }.toImmutableList()
}
reduce {
- copy(
+ PopularTravelListState.Success(
travelProgramTabs = tabs,
allPopularTravels = allTravels,
popularTravelsByProgram = travelsByProgram.toImmutableMap(),
@@ -93,6 +96,73 @@ class PopularTravelListViewModel @Inject constructor(
}
}
+ private fun loadMore(nextPage: Int) {
+ if (isLoadingMore) return
+
+ val currentUiState = state.value
+ if ((currentUiState is PopularTravelListState.Success).not()) return
+
+ isLoadingMore = true
+
+ val selectedTab = currentUiState
+ .travelProgramTabs
+ .getOrElse(currentUiState.selectedTabIndex, { TravelProgramTab.All })
+
+ viewModelScope.launch {
+ suspendRunCatching {
+ when (selectedTab) {
+ TravelProgramTab.All -> travelTemplateRepository.getAllPopularTravelTemplates(
+ page = nextPage,
+ )
+
+ is TravelProgramTab.Custom -> travelTemplateRepository.getPopularTravelTemplates(
+ travelProgramId = selectedTab.programId,
+ page = nextPage,
+ )
+ }
+ }.onSuccess { templates ->
+ val newItems = buildList {
+ templates.content.forEach {
+ add(PopularTravelListItem.Travel(it.toTravelContent()))
+ }
+ if (templates.hasNext) {
+ add(PopularTravelListItem.Loading(nextPage = nextPage + 1))
+ }
+ }
+ reduce {
+ if ((this is PopularTravelListState.Success).not()) return@reduce this
+
+ when (selectedTab) {
+ TravelProgramTab.All -> copy(
+ allPopularTravels = allPopularTravels.appendNextPage(newItems)
+ .toImmutableList(),
+ )
+
+ is TravelProgramTab.Custom -> {
+ val current =
+ popularTravelsByProgram[selectedTab.programId] ?: persistentListOf()
+ copy(
+ popularTravelsByProgram = popularTravelsByProgram
+ .toMutableMap()
+ .apply {
+ set(
+ selectedTab.programId,
+ current.appendNextPage(newItems).toImmutableList(),
+ )
+ }
+ .toImmutableMap(),
+ )
+ }
+ }
+ }
+ }.onFailure {
+ postSideEffect(PopularTravelListSideEffect.ShowSnackBar(type = Type.ERR_UNKNOWN))
+ }
+
+ isLoadingMore = false
+ }
+ }
+
private fun TravelTemplateSummary.toTravelContent() = TravelContent(
travelId = id,
title = title,
@@ -106,11 +176,21 @@ class PopularTravelListViewModel @Inject constructor(
thumbnail = thumbnail ?: "",
)
+ private fun ImmutableList.appendNextPage(
+ newItems: List,
+ ) = (if (lastOrNull() is PopularTravelListItem.Loading) dropLast(1) else this) + newItems
+
override suspend fun handleIntent(intent: PopularTravelListIntent) {
when (intent) {
PopularTravelListIntent.ClickSearchTravelTemplate -> postNavigateToSearchTravelTemplate()
is PopularTravelListIntent.SelectPopularTravelTab -> selectTab(intent.index)
- is PopularTravelListIntent.ClickTravel -> postNavigateToTravelTemplate(intent.travelId, intent.days)
+ is PopularTravelListIntent.ClickTravel -> postNavigateToTravelTemplate(
+ intent.travelId,
+ intent.days,
+ )
+
+ is PopularTravelListIntent.LoadMore -> loadMore(intent.nextPage)
+ PopularTravelListIntent.ClickRetry -> loadPopularTemplates()
}
}
@@ -119,7 +199,10 @@ class PopularTravelListViewModel @Inject constructor(
}
private fun selectTab(index: Int) {
- reduce { copy(selectedTabIndex = index) }
+ reduce {
+ if ((this is PopularTravelListState.Success).not()) return@reduce this
+ copy(selectedTabIndex = index)
+ }
}
private fun postNavigateToTravelTemplate(travelId: Long, days: Int) {
diff --git a/feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt b/feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt
index d947dac2..3b8b4354 100644
--- a/feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt
+++ b/feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt
@@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import com.yapp.ndgl.core.ui.CommonErrorView
import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButton
import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButtonAttr
import com.yapp.ndgl.core.ui.designsystem.NDGLSearchNavigationBar
@@ -118,7 +119,7 @@ private fun TemplateSearchScreen(
)
}
- TemplateSearchState.SearchResult.Error -> item { ErrorView() }
+ TemplateSearchState.SearchResult.Error -> item { CommonErrorView() }
}
}
@@ -208,41 +209,6 @@ private fun EmptyResultView() {
}
}
-@Composable
-private fun ErrorView() {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp, vertical = 165.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Image(
- painter = painterResource(CoreR.drawable.img_empty_browser),
- contentDescription = null,
- modifier = Modifier.size(100.dp),
- )
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- text = stringResource(R.string.home_template_search_error_title),
- color = NDGLTheme.colors.black500,
- textAlign = TextAlign.Center,
- style = NDGLTheme.typography.subtitleMdSemiBold,
- )
- Text(
- text = stringResource(R.string.home_template_search_error_description),
- color = NDGLTheme.colors.black400,
- textAlign = TextAlign.Center,
- style = NDGLTheme.typography.bodyLgRegular,
- )
- }
- }
-}
-
@Preview(showBackground = true)
@Composable
private fun InitialEmptyViewPreview() {
@@ -259,14 +225,6 @@ private fun EmptyResultViewPreview() {
}
}
-@Preview(showBackground = true)
-@Composable
-private fun ErrorViewPreview() {
- NDGLTheme {
- ErrorView()
- }
-}
-
@Preview(showBackground = true)
@Composable
private fun TemplateSearchScreenFilledPreview() {
diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml
index c4cdd8d3..0d5e6cfd 100644
--- a/feature/home/src/main/res/values/strings.xml
+++ b/feature/home/src/main/res/values/strings.xml
@@ -26,8 +26,6 @@
좋아하는 유튜버나 가고 싶은\n여행지를 검색할 수 있어요
검색 결과가 없어요
철자를 확인하거나\n다른 키워드로 검색해보세요.
- 정보를 불러올 수 없어요
- 인터넷 연결 확인 후 다시 시도해 주세요
설정