Skip to content

Commit 71c95e0

Browse files
authored
Merge pull request #25 from YAPP-Github/feature/NDGL-89/impl-my-travel-ui
[NDGL-89] 내 여행 탭 구현
2 parents ac74495 + 17f0475 commit 71c95e0

17 files changed

Lines changed: 1458 additions & 138 deletions

File tree

core/ui/src/main/res/drawable/img_empty_suitcase.xml

Lines changed: 108 additions & 0 deletions
Large diffs are not rendered by default.

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: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package com.yapp.ndgl.data.travel.api
22

33
import com.yapp.ndgl.data.core.model.BaseResponse
4+
import com.yapp.ndgl.data.travel.model.UpcomingTravelList
45
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
56
import retrofit2.http.GET
7+
import retrofit2.http.Query
68

79
interface UserTravelApi {
810
@GET("/api/v1/travels/upcoming")
911
suspend fun getUpcomingTravel(): BaseResponse<UpcomingTravelResponse>
12+
13+
@GET("/api/v1/travels/upcoming/list")
14+
suspend fun getUpcomingTravelList(
15+
@Query("page") page: Int? = null,
16+
@Query("size") size: Int? = null,
17+
): BaseResponse<UpcomingTravelList>
1018
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.yapp.ndgl.data.travel.model
2+
3+
import com.yapp.ndgl.data.core.serializer.LocalDateSerializer
4+
import kotlinx.serialization.Serializable
5+
import java.time.LocalDate
6+
7+
@Serializable
8+
data class UpcomingTravelList(
9+
val content: List<UpcomingTravel>,
10+
val hasNext: Boolean,
11+
) {
12+
@Serializable
13+
data class UpcomingTravel(
14+
val id: Long,
15+
val title: String,
16+
val country: String,
17+
val city: String,
18+
@Serializable(with = LocalDateSerializer::class)
19+
val startDate: LocalDate,
20+
@Serializable(with = LocalDateSerializer::class)
21+
val endDate: LocalDate,
22+
val nights: Int,
23+
val days: Int,
24+
val templateId: Long,
25+
val thumbnail: String? = null,
26+
val profileImage: String? = null,
27+
)
28+
}

data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/UserTravelRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.yapp.ndgl.data.travel.repository
33
import com.yapp.ndgl.data.core.model.error.HttpResponseException
44
import com.yapp.ndgl.data.core.model.getData
55
import com.yapp.ndgl.data.travel.api.UserTravelApi
6+
import com.yapp.ndgl.data.travel.model.UpcomingTravelList
67
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
78
import java.net.HttpURLConnection
89
import javax.inject.Inject
@@ -23,4 +24,8 @@ class UserTravelRepository @Inject constructor(
2324
}
2425
}
2526
}
27+
28+
suspend fun getUpcomingTravelList(): UpcomingTravelList {
29+
return userTravelApi.getUpcomingTravelList().getData()
30+
}
2631
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 com.yapp.ndgl.data.travel.model.ProgramType
10+
import kotlinx.collections.immutable.ImmutableList
11+
import kotlinx.collections.immutable.persistentListOf
12+
import java.time.LocalDate
13+
14+
@Immutable
15+
data class MyTravelState(
16+
val upcomingTravel: UpcomingTravel? = null,
17+
val upcomingTravels: ImmutableList<UpcomingTravelItem> = persistentListOf(),
18+
val recommendedTravels: ImmutableList<RecommendedTravel> = persistentListOf(),
19+
) : UiState {
20+
@Stable
21+
sealed class UpcomingTravel {
22+
abstract val travelId: Long
23+
abstract val title: String
24+
abstract val startDate: LocalDate
25+
abstract val endDate: LocalDate
26+
27+
@Immutable
28+
data class Upcoming(
29+
override val travelId: Long,
30+
override val title: String,
31+
override val startDate: LocalDate,
32+
override val endDate: LocalDate,
33+
val imageUrl: String,
34+
val dDay: Int,
35+
) : UpcomingTravel()
36+
37+
@Immutable
38+
data class InProgress(
39+
override val travelId: Long,
40+
override val title: String,
41+
override val startDate: LocalDate,
42+
override val endDate: LocalDate,
43+
val dayCount: Int,
44+
val currentPlace: TravelPlace? = null,
45+
) : UpcomingTravel()
46+
}
47+
48+
@Immutable
49+
data class TravelPlace(
50+
val placeId: String,
51+
val category: PlaceCategory,
52+
val estimatedDuration: Int,
53+
val name: String,
54+
val thumbnailUrl: String,
55+
)
56+
57+
@Immutable
58+
data class UpcomingTravelItem(
59+
val travelId: Long,
60+
val title: String,
61+
val startDate: LocalDate,
62+
val endDate: LocalDate,
63+
val imageUrl: String,
64+
val dDay: Int,
65+
)
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+
)
79+
}
80+
81+
sealed interface MyTravelIntent : UiIntent {
82+
data class ClickTravel(val travelId: Long) : MyTravelIntent
83+
data class ClickTravelDetail(val travelId: Long) : MyTravelIntent
84+
data class ClickPlaceDetail(val placeId: String) : MyTravelIntent
85+
data object ClickFindNewTravel : MyTravelIntent
86+
}
87+
88+
sealed interface MyTravelSideEffect : UiSideEffect {
89+
data class NavigateToFollowTravel(val travelId: Long, val days: Int) : MyTravelSideEffect
90+
data class NavigateToTravelDetail(val travelId: Long) : MyTravelSideEffect
91+
data class NavigateToTravelPlace(val placeId: String) : MyTravelSideEffect
92+
data object NavigateToPopularTravelList : MyTravelSideEffect
93+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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.PaddingValues
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.statusBarsPadding
10+
import androidx.compose.foundation.lazy.LazyColumn
11+
import androidx.compose.material3.Scaffold
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.tooling.preview.Preview
17+
import androidx.compose.ui.unit.dp
18+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
19+
import com.yapp.ndgl.core.ui.R
20+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
21+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr.TextAlignType
22+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationIcon
23+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
24+
25+
@Composable
26+
internal fun MyTravelRoute(
27+
viewModel: MyTravelViewModel = hiltViewModel(),
28+
navigateToFollowTravel: (Long, Int) -> Unit,
29+
navigateToTravelDetail: (Long) -> Unit,
30+
navigateToTravelPlace: (String) -> Unit,
31+
) {
32+
val state by viewModel.collectAsState()
33+
34+
MyTravelScreen(
35+
state = state,
36+
onTravelClick = { travelId ->
37+
viewModel.onIntent(MyTravelIntent.ClickTravelDetail(travelId = travelId))
38+
},
39+
onPlaceClick = { placeId ->
40+
viewModel.onIntent(MyTravelIntent.ClickPlaceDetail(placeId = placeId))
41+
},
42+
onNewTravelFindClick = {
43+
viewModel.onIntent(MyTravelIntent.ClickFindNewTravel)
44+
},
45+
onTravelTemplateClick = { travelId ->
46+
viewModel.onIntent((MyTravelIntent.ClickTravel(travelId = travelId)))
47+
},
48+
)
49+
50+
viewModel.collectSideEffect { sideEffect ->
51+
when (sideEffect) {
52+
is MyTravelSideEffect.NavigateToFollowTravel -> navigateToFollowTravel(
53+
sideEffect.travelId,
54+
sideEffect.days,
55+
)
56+
57+
is MyTravelSideEffect.NavigateToTravelDetail -> navigateToTravelDetail(
58+
sideEffect.travelId,
59+
)
60+
61+
is MyTravelSideEffect.NavigateToTravelPlace -> navigateToTravelPlace(
62+
sideEffect.placeId,
63+
)
64+
65+
MyTravelSideEffect.NavigateToPopularTravelList -> {
66+
// FIXME: navigate to popular travel list
67+
}
68+
}
69+
}
70+
}
71+
72+
@Composable
73+
private fun MyTravelScreen(
74+
state: MyTravelState,
75+
onTravelClick: (Long) -> Unit,
76+
onPlaceClick: (String) -> Unit,
77+
onNewTravelFindClick: () -> Unit,
78+
onTravelTemplateClick: (Long) -> Unit,
79+
) {
80+
Scaffold(
81+
modifier = Modifier.fillMaxSize(),
82+
topBar = {
83+
NDGLNavigationBar(
84+
textAlignType = TextAlignType.START,
85+
modifier = Modifier
86+
.fillMaxWidth()
87+
.background(color = NDGLTheme.colors.white)
88+
.statusBarsPadding(),
89+
trailingContents = {
90+
NDGLNavigationIcon(
91+
icon = R.drawable.ic_28_search,
92+
onClick = { /* FIXME: 홈 검색 */ },
93+
)
94+
NDGLNavigationIcon(
95+
icon = R.drawable.ic_28_settings,
96+
onClick = { /* FIXME: 설정 */ },
97+
)
98+
},
99+
)
100+
},
101+
) { innerPadding ->
102+
LazyColumn(
103+
modifier = Modifier
104+
.padding(innerPadding)
105+
.fillMaxSize(),
106+
contentPadding = PaddingValues(
107+
top = 20.dp,
108+
bottom = 100.dp,
109+
),
110+
verticalArrangement = Arrangement.spacedBy(40.dp),
111+
horizontalAlignment = Alignment.CenterHorizontally,
112+
) {
113+
if (state.upcomingTravel != null) {
114+
item {
115+
UpcomingTravelCardSection(
116+
modifier = Modifier.fillMaxWidth(),
117+
upcomingTravel = state.upcomingTravel,
118+
onTravelClick = onTravelClick,
119+
onPlaceClick = onPlaceClick,
120+
)
121+
}
122+
}
123+
item {
124+
UpcomingTravelListSection(
125+
upcomingTravels = state.upcomingTravels,
126+
onUserTravelClick = onTravelClick,
127+
onNewTravelFindClick = onNewTravelFindClick,
128+
)
129+
}
130+
if (state.recommendedTravels.isNotEmpty()) {
131+
item {
132+
RecommendedTravelSection(
133+
recommendedTravels = state.recommendedTravels,
134+
onTravelTemplateClick = onTravelTemplateClick,
135+
)
136+
}
137+
}
138+
}
139+
}
140+
}
141+
142+
@Preview(showBackground = true)
143+
@Composable
144+
private fun MyTravelScreenPreview() {
145+
NDGLTheme {
146+
MyTravelScreen(
147+
state = MyTravelState(),
148+
onTravelClick = {},
149+
onPlaceClick = {},
150+
onNewTravelFindClick = {},
151+
onTravelTemplateClick = {},
152+
)
153+
}
154+
}

0 commit comments

Comments
 (0)