Skip to content

Commit 455e5db

Browse files
committed
[NDGL-89] feat: 내 여행 탭 - 추천 여행 템플릿 섹션 추가
1 parent b43166f commit 455e5db

5 files changed

Lines changed: 338 additions & 73 deletions

File tree

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelContract.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.yapp.ndgl.core.base.UiIntent
66
import com.yapp.ndgl.core.base.UiSideEffect
77
import com.yapp.ndgl.core.base.UiState
88
import com.yapp.ndgl.data.travel.model.PlaceCategory
9+
import com.yapp.ndgl.data.travel.model.ProgramType
910
import kotlinx.collections.immutable.ImmutableList
1011
import kotlinx.collections.immutable.persistentListOf
1112
import java.time.LocalDate
@@ -14,6 +15,7 @@ import java.time.LocalDate
1415
data class MyTravelState(
1516
val upcomingTravel: UpcomingTravel? = null,
1617
val upcomingTravels: ImmutableList<UpcomingTravelItem> = persistentListOf(),
18+
val recommendedTravels: ImmutableList<RecommendedTravel> = persistentListOf(),
1719
) : UiState {
1820
@Stable
1921
sealed class UpcomingTravel {
@@ -61,6 +63,19 @@ data class MyTravelState(
6163
val imageUrl: String,
6264
val dDay: Int,
6365
)
66+
67+
@Immutable
68+
data class RecommendedTravel(
69+
val travelId: Long,
70+
val title: String,
71+
val country: String,
72+
val city: String,
73+
val nights: Int,
74+
val days: Int,
75+
val programName: String,
76+
val programType: ProgramType,
77+
val thumbnailUrl: String,
78+
)
6479
}
6580

6681
sealed interface MyTravelIntent : UiIntent {

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelScreen.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ internal fun MyTravelRoute(
4242
onNewTravelFindClick = {
4343
viewModel.onIntent(MyTravelIntent.ClickFindNewTravel)
4444
},
45+
onTravelTemplateClick = { travelId ->
46+
viewModel.onIntent((MyTravelIntent.ClickTravel(travelId = travelId)))
47+
},
4548
)
4649

4750
viewModel.collectSideEffect { sideEffect ->
@@ -72,6 +75,7 @@ private fun MyTravelScreen(
7275
onTravelClick: (Long) -> Unit,
7376
onPlaceClick: (String) -> Unit,
7477
onNewTravelFindClick: () -> Unit,
78+
onTravelTemplateClick: (Long) -> Unit,
7579
) {
7680
Scaffold(
7781
modifier = Modifier.fillMaxSize(),
@@ -123,6 +127,14 @@ private fun MyTravelScreen(
123127
onNewTravelFindClick = onNewTravelFindClick,
124128
)
125129
}
130+
if (state.recommendedTravels.isNotEmpty()) {
131+
item {
132+
RecommendedTravelSection(
133+
recommendedTravels = state.recommendedTravels,
134+
onTravelTemplateClick = onTravelTemplateClick,
135+
)
136+
}
137+
}
126138
}
127139
}
128140
}
@@ -136,6 +148,7 @@ private fun MyTravelScreenPreview() {
136148
onTravelClick = {},
137149
onPlaceClick = {},
138150
onNewTravelFindClick = {},
151+
onTravelTemplateClick = {},
139152
)
140153
}
141154
}

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/mytravel/MyTravelViewModel.kt

Lines changed: 99 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package com.yapp.ndgl.feature.travel.mytravel
33
import androidx.lifecycle.viewModelScope
44
import com.yapp.ndgl.core.base.BaseViewModel
55
import com.yapp.ndgl.core.util.suspendRunCatching
6+
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
7+
import com.yapp.ndgl.data.travel.repository.TravelTemplateRepository
68
import com.yapp.ndgl.data.travel.repository.UserTravelRepository
79
import dagger.hilt.android.lifecycle.HiltViewModel
10+
import kotlinx.collections.immutable.ImmutableList
811
import kotlinx.collections.immutable.persistentListOf
912
import kotlinx.collections.immutable.toImmutableList
13+
import kotlinx.coroutines.async
1014
import kotlinx.coroutines.launch
1115
import java.time.LocalDate
1216
import java.time.temporal.ChronoUnit
@@ -15,90 +19,112 @@ import javax.inject.Inject
1519
@HiltViewModel
1620
class MyTravelViewModel @Inject constructor(
1721
private val userTravelRepository: UserTravelRepository,
22+
private val travelTemplateRepository: TravelTemplateRepository,
1823
) : BaseViewModel<MyTravelState, MyTravelIntent, MyTravelSideEffect>(
1924
initialState = MyTravelState(),
2025
) {
2126
init {
22-
loadUpcomingTravel()
23-
loadUpcomingTravelList()
24-
}
25-
26-
private fun loadUpcomingTravel() {
2727
viewModelScope.launch {
28-
suspendRunCatching {
29-
userTravelRepository.getUpcomingTravel()
30-
}.onSuccess { travel ->
31-
if (travel == null) {
32-
reduce { copy(upcomingTravel = null) }
33-
return@onSuccess
34-
}
35-
36-
val today = LocalDate.now()
37-
val myTravel = when {
38-
today < travel.startDate -> {
39-
val dDay = ChronoUnit.DAYS.between(today, travel.startDate).toInt()
40-
MyTravelState.UpcomingTravel.Upcoming(
41-
travelId = travel.userTravelId,
42-
title = travel.title,
43-
startDate = travel.startDate,
44-
endDate = travel.endDate,
45-
imageUrl = travel.upcomingUserTravelPlace?.place?.thumbnail ?: "",
46-
dDay = dDay,
47-
)
48-
}
49-
50-
today <= travel.endDate -> {
51-
val dayCount =
52-
ChronoUnit.DAYS.between(travel.startDate, today).toInt() + 1
53-
val upcomingPlace = travel.upcomingUserTravelPlace
54-
MyTravelState.UpcomingTravel.InProgress(
55-
travelId = travel.userTravelId,
56-
title = travel.title,
57-
startDate = travel.startDate,
58-
endDate = travel.endDate,
59-
dayCount = dayCount,
60-
currentPlace = upcomingPlace?.place?.let { place ->
61-
MyTravelState.TravelPlace(
62-
placeId = place.googlePlaceId,
63-
category = place.category,
64-
estimatedDuration = upcomingPlace.estimatedDuration,
65-
name = place.name,
66-
thumbnailUrl = place.thumbnail ?: "",
67-
)
68-
},
69-
)
70-
}
28+
val upcomingDeferred = async { loadUpcomingTravel() }
29+
val listDeferred = async { loadUpcomingTravelList() }
30+
31+
val upcomingTravel = upcomingDeferred.await()
32+
val upcomingTravels = listDeferred.await()
7133

72-
else -> null
73-
}
74-
reduce { copy(upcomingTravel = myTravel) }
75-
}.onFailure {
76-
reduce { copy(upcomingTravel = null) }
34+
reduce { copy(upcomingTravel = upcomingTravel, upcomingTravels = upcomingTravels) }
35+
36+
if (upcomingTravel == null && upcomingTravels.isEmpty()) {
37+
loadRecommendedTravels()
7738
}
7839
}
7940
}
8041

81-
private fun loadUpcomingTravelList() {
82-
viewModelScope.launch {
83-
suspendRunCatching { userTravelRepository.getUpcomingTravelList() }
84-
.onSuccess { result ->
85-
val today = LocalDate.now()
86-
val travels = result.content.map { travel ->
87-
val dDay = ChronoUnit.DAYS.between(today, travel.startDate).toInt()
88-
MyTravelState.UpcomingTravelItem(
89-
travelId = travel.id,
90-
title = travel.title,
91-
startDate = travel.startDate,
92-
endDate = travel.endDate,
93-
imageUrl = travel.thumbnail ?: "",
94-
dDay = dDay,
42+
private suspend fun loadUpcomingTravel(): MyTravelState.UpcomingTravel? {
43+
val travel = suspendRunCatching {
44+
userTravelRepository.getUpcomingTravel()
45+
}.getOrNull() ?: return null
46+
47+
return mapToUpcomingTravel(travel)
48+
}
49+
50+
private fun mapToUpcomingTravel(travel: UpcomingTravelResponse): MyTravelState.UpcomingTravel? {
51+
val today = LocalDate.now()
52+
return when {
53+
today < travel.startDate -> {
54+
val dDay = ChronoUnit.DAYS.between(today, travel.startDate).toInt()
55+
MyTravelState.UpcomingTravel.Upcoming(
56+
travelId = travel.userTravelId,
57+
title = travel.title,
58+
startDate = travel.startDate,
59+
endDate = travel.endDate,
60+
imageUrl = travel.upcomingUserTravelPlace?.place?.thumbnail ?: "",
61+
dDay = dDay,
62+
)
63+
}
64+
65+
today <= travel.endDate -> {
66+
val dayCount =
67+
ChronoUnit.DAYS.between(travel.startDate, today).toInt() + 1
68+
val upcomingPlace = travel.upcomingUserTravelPlace
69+
MyTravelState.UpcomingTravel.InProgress(
70+
travelId = travel.userTravelId,
71+
title = travel.title,
72+
startDate = travel.startDate,
73+
endDate = travel.endDate,
74+
dayCount = dayCount,
75+
currentPlace = upcomingPlace?.place?.let { place ->
76+
MyTravelState.TravelPlace(
77+
placeId = place.googlePlaceId,
78+
category = place.category,
79+
estimatedDuration = upcomingPlace.estimatedDuration,
80+
name = place.name,
81+
thumbnailUrl = place.thumbnail ?: "",
9582
)
96-
}.toImmutableList()
97-
reduce { copy(upcomingTravels = travels) }
98-
}
99-
.onFailure {
100-
reduce { copy(upcomingTravels = persistentListOf()) }
101-
}
83+
},
84+
)
85+
}
86+
87+
else -> null
88+
}
89+
}
90+
91+
private suspend fun loadUpcomingTravelList(): ImmutableList<MyTravelState.UpcomingTravelItem> {
92+
val result = suspendRunCatching {
93+
userTravelRepository.getUpcomingTravelList()
94+
}.getOrNull() ?: return persistentListOf()
95+
96+
val today = LocalDate.now()
97+
return result.content.map { travel ->
98+
val dDay = ChronoUnit.DAYS.between(today, travel.startDate).toInt()
99+
MyTravelState.UpcomingTravelItem(
100+
travelId = travel.id,
101+
title = travel.title,
102+
startDate = travel.startDate,
103+
endDate = travel.endDate,
104+
imageUrl = travel.thumbnail ?: "",
105+
dDay = dDay,
106+
)
107+
}.toImmutableList()
108+
}
109+
110+
private suspend fun loadRecommendedTravels() {
111+
suspendRunCatching {
112+
travelTemplateRepository.getRecommendTravelTemplates()
113+
}.onSuccess { result ->
114+
val travels = result.content.map { template ->
115+
MyTravelState.RecommendedTravel(
116+
travelId = template.id,
117+
title = template.title,
118+
country = template.country,
119+
city = template.city,
120+
nights = template.nights,
121+
days = template.days,
122+
programName = template.programName,
123+
programType = template.programType,
124+
thumbnailUrl = template.thumbnail ?: "",
125+
)
126+
}.toImmutableList()
127+
reduce { copy(recommendedTravels = travels) }
102128
}
103129
}
104130

0 commit comments

Comments
 (0)