Skip to content

Commit 9f72d9b

Browse files
committed
[NDGL-89] feat: 내 여행 탭 - 다가오는 여행 섹션 추가
1 parent ac74495 commit 9f72d9b

11 files changed

Lines changed: 662 additions & 138 deletions

File tree

core/ui/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<string name="add_cost">비용 추가</string>
1313
<string name="add_time">시간 추가</string>
1414
<string name="add_memo">메모 추가</string>
15+
<string name="common_dot_separator">•</string>
1516

1617
<!-- Transport Segment -->
1718
<string name="transport_segment_format">약 %1$s • %2$s</string>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package com.yapp.ndgl.feature.travel.mytravel
2+
3+
import androidx.compose.runtime.Immutable
4+
import androidx.compose.runtime.Stable
5+
import com.yapp.ndgl.core.base.UiIntent
6+
import com.yapp.ndgl.core.base.UiSideEffect
7+
import com.yapp.ndgl.core.base.UiState
8+
import com.yapp.ndgl.data.travel.model.PlaceCategory
9+
import java.time.LocalDate
10+
11+
@Immutable
12+
data class MyTravelState(
13+
val upcomingTravel: UpcomingTravel? = null,
14+
) : UiState {
15+
@Stable
16+
sealed class UpcomingTravel {
17+
abstract val travelId: Long
18+
abstract val title: String
19+
abstract val startDate: LocalDate
20+
abstract val endDate: LocalDate
21+
22+
@Immutable
23+
data class Upcoming(
24+
override val travelId: Long,
25+
override val title: String,
26+
override val startDate: LocalDate,
27+
override val endDate: LocalDate,
28+
val imageUrl: String,
29+
val dDay: Int,
30+
) : UpcomingTravel()
31+
32+
@Immutable
33+
data class InProgress(
34+
override val travelId: Long,
35+
override val title: String,
36+
override val startDate: LocalDate,
37+
override val endDate: LocalDate,
38+
val dayCount: Int,
39+
val currentPlace: TravelPlace? = null,
40+
) : UpcomingTravel()
41+
}
42+
43+
data class TravelPlace(
44+
val placeId: String,
45+
val category: PlaceCategory,
46+
val estimatedDuration: Int,
47+
val name: String,
48+
val thumbnailUrl: String,
49+
)
50+
}
51+
52+
sealed interface MyTravelIntent : UiIntent {
53+
data class ClickTravel(val travelId: Long) : MyTravelIntent
54+
data class ClickTravelDetail(val travelId: Long) : MyTravelIntent
55+
data class ClickPlaceDetail(val placeId: String) : MyTravelIntent
56+
}
57+
58+
sealed interface MyTravelSideEffect : UiSideEffect {
59+
data class NavigateToFollowTravel(val travelId: Long, val days: Int) : MyTravelSideEffect
60+
data class NavigateToTravelDetail(val travelId: Long) : MyTravelSideEffect
61+
data class NavigateToTravelPlace(val placeId: String) : MyTravelSideEffect
62+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.yapp.ndgl.feature.travel.mytravel
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.layout.statusBarsPadding
9+
import androidx.compose.foundation.lazy.LazyColumn
10+
import androidx.compose.material3.Scaffold
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.runtime.getValue
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.tooling.preview.Preview
16+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
17+
import com.yapp.ndgl.core.ui.R
18+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
19+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr.TextAlignType
20+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationIcon
21+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
22+
23+
@Composable
24+
internal fun MyTravelRoute(
25+
viewModel: MyTravelViewModel = hiltViewModel(),
26+
navigateToFollowTravel: (Long, Int) -> Unit,
27+
navigateToTravelDetail: (Long) -> Unit,
28+
navigateToTravelPlace: (String) -> Unit,
29+
) {
30+
val state by viewModel.collectAsState()
31+
32+
MyTravelScreen(
33+
state = state,
34+
onTravelClick = { travelId ->
35+
viewModel.onIntent(MyTravelIntent.ClickTravelDetail(travelId = travelId))
36+
},
37+
onPlaceClick = { placeId ->
38+
viewModel.onIntent(MyTravelIntent.ClickPlaceDetail(placeId = placeId))
39+
},
40+
)
41+
42+
viewModel.collectSideEffect { sideEffect ->
43+
when (sideEffect) {
44+
is MyTravelSideEffect.NavigateToFollowTravel -> navigateToFollowTravel(
45+
sideEffect.travelId,
46+
sideEffect.days,
47+
)
48+
49+
is MyTravelSideEffect.NavigateToTravelDetail -> navigateToTravelDetail(
50+
sideEffect.travelId,
51+
)
52+
53+
is MyTravelSideEffect.NavigateToTravelPlace -> navigateToTravelPlace(
54+
sideEffect.placeId,
55+
)
56+
}
57+
}
58+
}
59+
60+
@Composable
61+
private fun MyTravelScreen(
62+
state: MyTravelState,
63+
onTravelClick: (Long) -> Unit,
64+
onPlaceClick: (String) -> Unit,
65+
) {
66+
Scaffold(
67+
modifier = Modifier.fillMaxSize(),
68+
topBar = {
69+
NDGLNavigationBar(
70+
textAlignType = TextAlignType.START,
71+
modifier = Modifier
72+
.fillMaxWidth()
73+
.background(color = NDGLTheme.colors.white)
74+
.statusBarsPadding(),
75+
trailingContents = {
76+
NDGLNavigationIcon(
77+
icon = R.drawable.ic_28_search,
78+
onClick = { /* FIXME: 홈 검색 */ },
79+
)
80+
NDGLNavigationIcon(
81+
icon = R.drawable.ic_28_settings,
82+
onClick = { /* FIXME: 설정 */ },
83+
)
84+
},
85+
)
86+
},
87+
) { innerPadding ->
88+
LazyColumn(
89+
modifier = Modifier
90+
.padding(innerPadding)
91+
.fillMaxSize(),
92+
verticalArrangement = Arrangement.Center,
93+
horizontalAlignment = Alignment.CenterHorizontally,
94+
) {
95+
if (state.upcomingTravel != null) {
96+
item {
97+
UpcomingTravelCardSection(
98+
modifier = Modifier.fillMaxWidth(),
99+
upcomingTravel = state.upcomingTravel,
100+
onTravelClick = onTravelClick,
101+
onPlaceClick = onPlaceClick,
102+
)
103+
}
104+
}
105+
}
106+
}
107+
}
108+
109+
@Preview(showBackground = true)
110+
@Composable
111+
private fun MyTravelScreenPreview() {
112+
NDGLTheme {
113+
MyTravelScreen(
114+
state = MyTravelState(),
115+
onTravelClick = {},
116+
onPlaceClick = {},
117+
)
118+
}
119+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.yapp.ndgl.feature.travel.mytravel
2+
3+
import androidx.lifecycle.viewModelScope
4+
import com.yapp.ndgl.core.base.BaseViewModel
5+
import com.yapp.ndgl.core.util.suspendRunCatching
6+
import com.yapp.ndgl.data.travel.repository.UserTravelRepository
7+
import dagger.hilt.android.lifecycle.HiltViewModel
8+
import kotlinx.coroutines.launch
9+
import java.time.LocalDate
10+
import java.time.temporal.ChronoUnit
11+
import javax.inject.Inject
12+
13+
@HiltViewModel
14+
class MyTravelViewModel @Inject constructor(
15+
private val userTravelRepository: UserTravelRepository,
16+
) : BaseViewModel<MyTravelState, MyTravelIntent, MyTravelSideEffect>(
17+
initialState = MyTravelState(),
18+
) {
19+
init {
20+
loadUpcomingTravel()
21+
loadUpcomingTravelList()
22+
}
23+
24+
private fun loadUpcomingTravel() {
25+
viewModelScope.launch {
26+
suspendRunCatching {
27+
userTravelRepository.getUpcomingTravel()
28+
}.onSuccess { travel ->
29+
if (travel == null) {
30+
reduce { copy(upcomingTravel = null) }
31+
return@onSuccess
32+
}
33+
34+
val today = LocalDate.now()
35+
val myTravel = when {
36+
today < travel.startDate -> {
37+
val dDay = ChronoUnit.DAYS.between(today, travel.startDate).toInt()
38+
MyTravelState.UpcomingTravel.Upcoming(
39+
travelId = travel.userTravelId,
40+
title = travel.title,
41+
startDate = travel.startDate,
42+
endDate = travel.endDate,
43+
imageUrl = travel.upcomingUserTravelPlace?.place?.thumbnail ?: "",
44+
dDay = dDay,
45+
)
46+
}
47+
48+
today <= travel.endDate -> {
49+
val dayCount =
50+
ChronoUnit.DAYS.between(travel.startDate, today).toInt() + 1
51+
val upcomingPlace = travel.upcomingUserTravelPlace
52+
MyTravelState.UpcomingTravel.InProgress(
53+
travelId = travel.userTravelId,
54+
title = travel.title,
55+
startDate = travel.startDate,
56+
endDate = travel.endDate,
57+
dayCount = dayCount,
58+
currentPlace = upcomingPlace?.place?.let { place ->
59+
MyTravelState.TravelPlace(
60+
placeId = place.googlePlaceId,
61+
category = place.category,
62+
estimatedDuration = upcomingPlace.estimatedDuration,
63+
name = place.name,
64+
thumbnailUrl = place.thumbnail ?: "",
65+
)
66+
},
67+
)
68+
}
69+
70+
else -> null
71+
}
72+
reduce { copy(upcomingTravel = myTravel) }
73+
}.onFailure {
74+
reduce { copy(upcomingTravel = null) }
75+
}
76+
}
77+
}
78+
79+
private fun loadUpcomingTravelList() {
80+
// FIXME: 다가오는 여행 목록 조회
81+
}
82+
83+
override suspend fun handleIntent(intent: MyTravelIntent) {
84+
when (intent) {
85+
is MyTravelIntent.ClickTravel -> postNavigateToFollowTravel(travelId = intent.travelId)
86+
is MyTravelIntent.ClickTravelDetail -> postNavigateToTravelDetail(travelId = intent.travelId)
87+
is MyTravelIntent.ClickPlaceDetail -> postNavigateToPlaceDetail(placeId = intent.placeId)
88+
}
89+
}
90+
91+
private fun postNavigateToFollowTravel(travelId: Long, days: Int = 1) {
92+
postSideEffect(MyTravelSideEffect.NavigateToFollowTravel(travelId = travelId, days = days))
93+
}
94+
95+
private fun postNavigateToTravelDetail(travelId: Long) {
96+
postSideEffect(MyTravelSideEffect.NavigateToTravelDetail(travelId = travelId))
97+
}
98+
99+
private fun postNavigateToPlaceDetail(placeId: String) {
100+
postSideEffect(MyTravelSideEffect.NavigateToTravelPlace(placeId = placeId))
101+
}
102+
}

0 commit comments

Comments
 (0)