Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions core/ui/src/main/res/drawable/img_empty_suitcase.xml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<string name="add_cost">비용 추가</string>
<string name="add_time">시간 추가</string>
<string name="add_memo">메모 추가</string>
<string name="common_dot_separator">•</string>

<!-- Transport Segment -->
<string name="transport_segment_format">약 %1$s • %2$s</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.yapp.ndgl.data.travel.api

import com.yapp.ndgl.data.core.model.BaseResponse
import com.yapp.ndgl.data.travel.model.UpcomingTravelList
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
import retrofit2.http.GET
import retrofit2.http.Query

interface UserTravelApi {
@GET("/api/v1/travels/upcoming")
suspend fun getUpcomingTravel(): BaseResponse<UpcomingTravelResponse>

@GET("/api/v1/travels/upcoming/list")
suspend fun getUpcomingTravelList(
@Query("page") page: Int? = null,
@Query("size") size: Int? = null,
): BaseResponse<UpcomingTravelList>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.yapp.ndgl.data.travel.model

import com.yapp.ndgl.data.core.serializer.LocalDateSerializer
import kotlinx.serialization.Serializable
import java.time.LocalDate

@Serializable
data class UpcomingTravelList(
val content: List<UpcomingTravel>,
val hasNext: Boolean,
) {
@Serializable
data class UpcomingTravel(
val id: Long,
val title: String,
val country: String,
val city: String,
@Serializable(with = LocalDateSerializer::class)
val startDate: LocalDate,
@Serializable(with = LocalDateSerializer::class)
val endDate: LocalDate,
val nights: Int,
val days: Int,
val templateId: Long,
val thumbnail: String? = null,
val profileImage: String? = null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.yapp.ndgl.data.travel.repository
import com.yapp.ndgl.data.core.model.error.HttpResponseException
import com.yapp.ndgl.data.core.model.getData
import com.yapp.ndgl.data.travel.api.UserTravelApi
import com.yapp.ndgl.data.travel.model.UpcomingTravelList
import com.yapp.ndgl.data.travel.model.UpcomingTravelResponse
import java.net.HttpURLConnection
import javax.inject.Inject
Expand All @@ -23,4 +24,8 @@ class UserTravelRepository @Inject constructor(
}
}
}

suspend fun getUpcomingTravelList(): UpcomingTravelList {
return userTravelApi.getUpcomingTravelList().getData()
}
Comment thread
jihee-dev marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.yapp.ndgl.feature.travel.mytravel

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
import com.yapp.ndgl.data.travel.model.PlaceCategory
import com.yapp.ndgl.data.travel.model.ProgramType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.time.LocalDate

@Immutable
data class MyTravelState(
val upcomingTravel: UpcomingTravel? = null,
val upcomingTravels: ImmutableList<UpcomingTravelItem> = persistentListOf(),
val recommendedTravels: ImmutableList<RecommendedTravel> = persistentListOf(),
) : UiState {
@Stable
sealed class UpcomingTravel {
abstract val travelId: Long
abstract val title: String
abstract val startDate: LocalDate
abstract val endDate: LocalDate

@Immutable
data class Upcoming(
override val travelId: Long,
override val title: String,
override val startDate: LocalDate,
override val endDate: LocalDate,
val imageUrl: String,
val dDay: Int,
) : UpcomingTravel()

@Immutable
data class InProgress(
override val travelId: Long,
override val title: String,
override val startDate: LocalDate,
override val endDate: LocalDate,
val dayCount: Int,
val currentPlace: TravelPlace? = null,
) : UpcomingTravel()
}

@Immutable
data class TravelPlace(
val placeId: String,
val category: PlaceCategory,
val estimatedDuration: Int,
val name: String,
val thumbnailUrl: String,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)

@Immutable
data class UpcomingTravelItem(
val travelId: Long,
val title: String,
val startDate: LocalDate,
val endDate: LocalDate,
val imageUrl: String,
val dDay: Int,
)

@Immutable
data class RecommendedTravel(
val travelId: Long,
val title: String,
val country: String,
val city: String,
val nights: Int,
val days: Int,
val programName: String,
val programType: ProgramType,
val thumbnailUrl: String,
)
}

sealed interface MyTravelIntent : UiIntent {
data class ClickTravel(val travelId: Long) : MyTravelIntent
data class ClickTravelDetail(val travelId: Long) : MyTravelIntent
data class ClickPlaceDetail(val placeId: String) : MyTravelIntent
data object ClickFindNewTravel : MyTravelIntent
}

sealed interface MyTravelSideEffect : UiSideEffect {
data class NavigateToFollowTravel(val travelId: Long, val days: Int) : MyTravelSideEffect
data class NavigateToTravelDetail(val travelId: Long) : MyTravelSideEffect
data class NavigateToTravelPlace(val placeId: String) : MyTravelSideEffect
data object NavigateToPopularTravelList : MyTravelSideEffect
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.yapp.ndgl.feature.travel.mytravel

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.R
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr.TextAlignType
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationIcon
import com.yapp.ndgl.core.ui.theme.NDGLTheme

@Composable
internal fun MyTravelRoute(
viewModel: MyTravelViewModel = hiltViewModel(),
navigateToFollowTravel: (Long, Int) -> Unit,
navigateToTravelDetail: (Long) -> Unit,
navigateToTravelPlace: (String) -> Unit,
) {
val state by viewModel.collectAsState()

MyTravelScreen(
state = state,
onTravelClick = { travelId ->
viewModel.onIntent(MyTravelIntent.ClickTravelDetail(travelId = travelId))
},
onPlaceClick = { placeId ->
viewModel.onIntent(MyTravelIntent.ClickPlaceDetail(placeId = placeId))
},
onNewTravelFindClick = {
viewModel.onIntent(MyTravelIntent.ClickFindNewTravel)
},
onTravelTemplateClick = { travelId ->
viewModel.onIntent((MyTravelIntent.ClickTravel(travelId = travelId)))
},
)

viewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
is MyTravelSideEffect.NavigateToFollowTravel -> navigateToFollowTravel(
sideEffect.travelId,
sideEffect.days,
)

is MyTravelSideEffect.NavigateToTravelDetail -> navigateToTravelDetail(
sideEffect.travelId,
)

is MyTravelSideEffect.NavigateToTravelPlace -> navigateToTravelPlace(
sideEffect.placeId,
)

MyTravelSideEffect.NavigateToPopularTravelList -> {
// FIXME: navigate to popular travel list
}
}
}
}

@Composable
private fun MyTravelScreen(
state: MyTravelState,
onTravelClick: (Long) -> Unit,
onPlaceClick: (String) -> Unit,
onNewTravelFindClick: () -> Unit,
onTravelTemplateClick: (Long) -> Unit,
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
NDGLNavigationBar(
textAlignType = TextAlignType.START,
modifier = Modifier
.fillMaxWidth()
.background(color = NDGLTheme.colors.white)
.statusBarsPadding(),
trailingContents = {
NDGLNavigationIcon(
icon = R.drawable.ic_28_search,
onClick = { /* FIXME: 홈 검색 */ },
)
NDGLNavigationIcon(
icon = R.drawable.ic_28_settings,
onClick = { /* FIXME: 설정 */ },
)
},
)
},
) { innerPadding ->
LazyColumn(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
contentPadding = PaddingValues(
top = 20.dp,
bottom = 100.dp,
),
verticalArrangement = Arrangement.spacedBy(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (state.upcomingTravel != null) {
item {
UpcomingTravelCardSection(
modifier = Modifier.fillMaxWidth(),
upcomingTravel = state.upcomingTravel,
onTravelClick = onTravelClick,
onPlaceClick = onPlaceClick,
)
}
}
item {
UpcomingTravelListSection(
upcomingTravels = state.upcomingTravels,
onUserTravelClick = onTravelClick,
onNewTravelFindClick = onNewTravelFindClick,
)
}
if (state.recommendedTravels.isNotEmpty()) {
item {
RecommendedTravelSection(
recommendedTravels = state.recommendedTravels,
onTravelTemplateClick = onTravelTemplateClick,
)
}
}
}
}
}

@Preview(showBackground = true)
@Composable
private fun MyTravelScreenPreview() {
NDGLTheme {
MyTravelScreen(
state = MyTravelState(),
onTravelClick = {},
onPlaceClick = {},
onNewTravelFindClick = {},
onTravelTemplateClick = {},
)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading