Skip to content

Commit 8d385de

Browse files
committed
[NDGL-61] refactor: Home 화면 기본 구조 구현
1 parent 8f9832e commit 8d385de

File tree

6 files changed

+319
-73
lines changed

6 files changed

+319
-73
lines changed

feature/home/src/main/java/com/yapp/ndgl/feature/home/HomeScreen.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

feature/home/src/main/java/com/yapp/ndgl/feature/home/HomeViewModel.kt

Lines changed: 0 additions & 35 deletions
This file was deleted.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.yapp.ndgl.feature.home.main
2+
3+
import androidx.annotation.DrawableRes
4+
import androidx.compose.runtime.Immutable
5+
import androidx.compose.runtime.Stable
6+
import com.yapp.ndgl.core.base.UiIntent
7+
import com.yapp.ndgl.core.base.UiSideEffect
8+
import com.yapp.ndgl.core.base.UiState
9+
import com.yapp.ndgl.data.travel.model.TravelSummary
10+
import java.time.LocalDate
11+
12+
@Stable
13+
data class HomeState(
14+
val userName: String = "",
15+
val myTravel: MyTravel = MyTravel.None,
16+
val popularTravelSelectedTabIndex: Int = 0,
17+
val popularTravelTabs: List<PopularTravelTab> = emptyList(),
18+
val popularTravelsByTab: Map<String, List<TravelSummary>> = emptyMap(),
19+
val recommendedContents: List<TravelSummary> = emptyList(),
20+
) : UiState {
21+
@Stable
22+
sealed interface MyTravel {
23+
@Immutable
24+
data object None : MyTravel
25+
26+
@Immutable
27+
data class Upcoming(
28+
val title: String,
29+
val imageUrl: String,
30+
val dDay: Int,
31+
val startDate: LocalDate,
32+
val endDate: LocalDate,
33+
) : MyTravel
34+
35+
@Immutable
36+
data class InProgress(
37+
val title: String,
38+
val dayCount: Int,
39+
val startDate: LocalDate,
40+
val endDate: LocalDate,
41+
val currentPlace: TravelPlace,
42+
) : MyTravel
43+
}
44+
45+
data class TravelPlace(
46+
val category: String,
47+
val estimatedTime: String,
48+
val name: String,
49+
val thumbnailUrl: String,
50+
)
51+
52+
data class PopularTravelTab(
53+
val tag: String,
54+
val name: String,
55+
@DrawableRes val icon: Int? = null,
56+
)
57+
}
58+
59+
sealed interface HomeIntent : UiIntent {
60+
data class SelectPopularTravelTab(val index: Int) : HomeIntent
61+
}
62+
63+
sealed interface HomeSideEffect : UiSideEffect
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.yapp.ndgl.feature.home.main
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.lazy.LazyColumn
9+
import androidx.compose.material3.Scaffold
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.tooling.preview.Preview
14+
import androidx.compose.ui.unit.dp
15+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
16+
import com.yapp.ndgl.core.ui.R
17+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
18+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr
19+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationIcon
20+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
21+
import java.time.LocalDate
22+
23+
@Composable
24+
internal fun HomeRoute(
25+
viewModel: HomeViewModel = hiltViewModel(),
26+
) {
27+
val state by viewModel.collectAsState()
28+
HomeScreen(
29+
state = state,
30+
onTabSelected = { index ->
31+
viewModel.onIntent(HomeIntent.SelectPopularTravelTab(index))
32+
},
33+
)
34+
}
35+
36+
@Composable
37+
private fun HomeScreen(
38+
state: HomeState = HomeState(),
39+
onTabSelected: (Int) -> Unit = {},
40+
) {
41+
Scaffold(
42+
topBar = {
43+
NDGLNavigationBar(
44+
textAlignType = NDGLNavigationBarAttr.TextAlignType.START,
45+
modifier = Modifier
46+
.fillMaxWidth()
47+
.background(color = NDGLTheme.colors.white),
48+
trailingContents = {
49+
NDGLNavigationIcon(
50+
icon = R.drawable.ic_28_search,
51+
onClick = { /* FIXME: 홈 검색 */ },
52+
)
53+
NDGLNavigationIcon(
54+
icon = R.drawable.ic_28_settings,
55+
onClick = { /* FIXME: 설정 */ },
56+
)
57+
},
58+
)
59+
},
60+
) { innerPadding ->
61+
LazyColumn(
62+
modifier = Modifier.fillMaxSize(),
63+
contentPadding = PaddingValues(
64+
top = innerPadding.calculateTopPadding() + 20.dp,
65+
bottom = 80.dp,
66+
),
67+
verticalArrangement = Arrangement.spacedBy(40.dp),
68+
) {
69+
item {
70+
MyTravelCardSection(
71+
modifier = Modifier.fillMaxWidth(),
72+
myTravel = state.myTravel,
73+
)
74+
}
75+
76+
item {
77+
if (state.popularTravelsByTab.isNotEmpty()) {
78+
PopularTravelSection(
79+
tabs = state.popularTravelTabs,
80+
selectedTabIndex = state.popularTravelSelectedTabIndex,
81+
travelsByTab = state.popularTravelsByTab,
82+
onTabSelected = onTabSelected,
83+
)
84+
}
85+
}
86+
87+
if (state.recommendedContents.isNotEmpty()) {
88+
item {
89+
RecommendedContentSection(
90+
userName = state.userName,
91+
contents = state.recommendedContents,
92+
)
93+
}
94+
}
95+
}
96+
}
97+
}
98+
99+
@Preview(showBackground = true)
100+
@Composable
101+
private fun HomeScreenPreview() {
102+
NDGLTheme {
103+
HomeScreen(
104+
state = HomeState(
105+
myTravel = HomeState.MyTravel.InProgress(
106+
title = "인도 여행",
107+
dayCount = 1,
108+
startDate = LocalDate.of(2024, 12, 23),
109+
endDate = LocalDate.of(2024, 12, 26),
110+
currentPlace = HomeState.TravelPlace(
111+
category = "교통수단",
112+
estimatedTime = "1시간 체류 예상",
113+
name = "인도 국제 공항",
114+
thumbnailUrl = "",
115+
),
116+
),
117+
popularTravelTabs = listOf(
118+
HomeState.PopularTravelTab(tag = "all", name = "전체"),
119+
HomeState.PopularTravelTab(tag = "ppanibottle", name = "빠니보틀", icon = R.drawable.ic_20_video),
120+
HomeState.PopularTravelTab(tag = "gwaktube", name = "곽튜브", icon = R.drawable.ic_20_video),
121+
HomeState.PopularTravelTab(tag = "kongkong", name = "콩콩팡팡", icon = R.drawable.ic_20_tv),
122+
),
123+
// popularTravels = listOf(
124+
// PopularTravel("1", "https://picsum.photos/200/300", "", "곽준빈의 신혼여행", "파리", "7박 9일"),
125+
// PopularTravel("2", "https://picsum.photos/200/300", "", "스위스 여행", "스위스", "5박 6일"),
126+
// PopularTravel("3", "https://picsum.photos/200/300", "", "충격적인 북유럽 물가", "덴마크", "4박 6일"),
127+
// ),
128+
// recommendedContents = listOf(
129+
// RecommendedContent("1", "https://picsum.photos/200/300", "인도", "생각보다 깨끗한 인도 경험하기", "빠니보틀", R.drawable.ic_20_video, "5박 6일"),
130+
// RecommendedContent("2", "https://picsum.photos/200/300", "파리", "생각보다 깨끗한 인도 경험하기", "빠니보틀", R.drawable.ic_20_video, "5박 6일"),
131+
// ),
132+
),
133+
)
134+
}
135+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.yapp.ndgl.feature.home.main
2+
3+
import androidx.lifecycle.viewModelScope
4+
import com.yapp.ndgl.core.base.BaseViewModel
5+
import com.yapp.ndgl.core.ui.R
6+
import com.yapp.ndgl.core.util.suspendRunCatching
7+
import com.yapp.ndgl.data.auth.repository.AuthRepository
8+
import com.yapp.ndgl.data.travel.repository.HomeRepository
9+
import dagger.hilt.android.lifecycle.HiltViewModel
10+
import kotlinx.coroutines.launch
11+
import timber.log.Timber
12+
import javax.inject.Inject
13+
14+
@HiltViewModel
15+
class HomeViewModel @Inject constructor(
16+
private val authRepository: AuthRepository,
17+
private val homeRepository: HomeRepository,
18+
) : BaseViewModel<HomeState, HomeIntent, HomeSideEffect>(
19+
initialState = HomeState(),
20+
) {
21+
init {
22+
initSession()
23+
}
24+
25+
private fun initSession() = viewModelScope.launch {
26+
suspendRunCatching {
27+
authRepository.initSession()
28+
}.onSuccess {
29+
loadHomeContents()
30+
}.onFailure { exception ->
31+
Timber.e("fail to init session: $exception")
32+
loadHomeContents()
33+
}
34+
}
35+
36+
private fun loadHomeContents() {
37+
loadMyTravel()
38+
loadPopularTravel()
39+
loadRecommendedTravel()
40+
}
41+
42+
private fun loadMyTravel() {
43+
viewModelScope.launch {
44+
suspendRunCatching { homeRepository.getMyTravel() }
45+
.onSuccess { travel ->
46+
reduce {
47+
copy(
48+
myTravel = HomeState.MyTravel.InProgress(
49+
title = travel.title,
50+
dayCount = travel.dayCount,
51+
startDate = travel.startDate,
52+
endDate = travel.endDate,
53+
currentPlace = HomeState.TravelPlace(
54+
category = travel.currentPlace.category,
55+
estimatedTime = travel.currentPlace.estimatedTime,
56+
name = travel.currentPlace.name,
57+
thumbnailUrl = travel.currentPlace.thumbnailUrl,
58+
),
59+
),
60+
)
61+
}
62+
}
63+
}
64+
}
65+
66+
private fun loadPopularTravel() {
67+
viewModelScope.launch {
68+
suspendRunCatching { homeRepository.getPopularTravels() }
69+
.onSuccess { travels ->
70+
val travelsByYoutuber = travels.groupBy { travel ->
71+
travel.youtube.youtuber
72+
}
73+
val tabs = travelsByYoutuber.keys
74+
.map { youtuber ->
75+
HomeState.PopularTravelTab(
76+
tag = youtuber,
77+
name = youtuber,
78+
icon = R.drawable.ic_20_video,
79+
)
80+
}.toMutableList()
81+
.apply {
82+
add(
83+
index = 0,
84+
element = HomeState.PopularTravelTab(
85+
tag = "all",
86+
name = "전체",
87+
icon = null,
88+
),
89+
)
90+
}.toList()
91+
val travelsByTab = travelsByYoutuber.toMutableMap().apply {
92+
put("all", travels)
93+
}
94+
reduce {
95+
copy(
96+
popularTravelTabs = tabs,
97+
popularTravelsByTab = travelsByTab,
98+
)
99+
}
100+
}
101+
}
102+
}
103+
104+
private fun loadRecommendedTravel() {
105+
viewModelScope.launch {
106+
suspendRunCatching { homeRepository.getRecommendedTravels() }
107+
.onSuccess { travels ->
108+
reduce { copy(recommendedContents = travels) }
109+
}
110+
}
111+
}
112+
113+
override suspend fun handleIntent(intent: HomeIntent) {
114+
when (intent) {
115+
is HomeIntent.SelectPopularTravelTab -> {
116+
reduce { copy(popularTravelSelectedTabIndex = intent.index) }
117+
}
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)