Skip to content

Commit 2a7cdf9

Browse files
committed
[NDGL-69] feature: TravelDetail 및 PlaceDetail 내 장소 관련 API 연동
1 parent 30555df commit 2a7cdf9

7 files changed

Lines changed: 141 additions & 105 deletions

File tree

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailContract.kt

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import com.yapp.ndgl.core.base.UiIntent
44
import com.yapp.ndgl.core.base.UiSideEffect
55
import com.yapp.ndgl.core.base.UiState
66
import com.yapp.ndgl.core.util.formatDecimal
7+
import com.yapp.ndgl.feature.travel.model.AlternativePlace
78
import com.yapp.ndgl.feature.travel.model.PlaceDetailTab
89
import com.yapp.ndgl.feature.travel.model.PlacePhoto
910
import com.yapp.ndgl.feature.travel.model.PlaceType
1011
import com.yapp.ndgl.feature.travel.model.PriceRange
12+
import com.yapp.ndgl.feature.travel.model.TipContent
1113
import kotlin.time.Duration
1214
import kotlin.time.Duration.Companion.hours
1315

@@ -42,18 +44,6 @@ data class PlaceInfo(
4244
get() = userRatingCount?.formatDecimal() ?: ""
4345
}
4446

45-
data class TipContent(
46-
val creatorName: String,
47-
val tips: List<String>,
48-
)
49-
50-
data class AlternativePlace(
51-
val id: Int,
52-
val name: String,
53-
val thumbnail: String,
54-
val placeType: PlaceType,
55-
)
56-
5747
sealed interface PlaceDetailIntent : UiIntent {
5848
data class SelectTab(val tab: PlaceDetailTab) : PlaceDetailIntent
5949
data class ClickChangePlace(val alternativePlace: AlternativePlace) : PlaceDetailIntent

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/placedetail/PlaceDetailScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
5050
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr
5151
import com.yapp.ndgl.core.ui.theme.NDGLTheme
5252
import com.yapp.ndgl.core.ui.util.launchBrowser
53+
import com.yapp.ndgl.feature.travel.model.AlternativePlace
5354
import com.yapp.ndgl.feature.travel.model.PlaceDetailTab
5455
import com.yapp.ndgl.feature.travel.model.PlacePhoto
5556
import com.yapp.ndgl.feature.travel.model.PlaceType
5657
import com.yapp.ndgl.feature.travel.model.Price
5758
import com.yapp.ndgl.feature.travel.model.PriceRange
59+
import com.yapp.ndgl.feature.travel.model.TipContent
5860
import com.yapp.ndgl.feature.travel.placedetail.component.PlaceDetailTabRow
5961
import com.yapp.ndgl.feature.travel.placedetail.component.PlaceInfoTab
6062
import com.yapp.ndgl.feature.travel.placedetail.component.PlacePhotoTab
@@ -324,13 +326,13 @@ private fun PlaceDetailScreenPreview() {
324326
),
325327
alternativePlaces = listOf(
326328
AlternativePlace(
327-
id = 2,
329+
id = "",
328330
name = "젤라또 디 산 크리스피노",
329331
thumbnail = "",
330332
placeType = PlaceType.CAFE,
331333
),
332334
AlternativePlace(
333-
id = 3,
335+
id = "",
334336
name = "지올리티",
335337
thumbnail = "",
336338
placeType = PlaceType.CAFE,
Lines changed: 87 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,108 @@
11
package com.yapp.ndgl.feature.travel.placedetail
22

3+
import androidx.lifecycle.viewModelScope
34
import com.yapp.ndgl.core.base.BaseViewModel
5+
import com.yapp.ndgl.core.util.suspendRunCatching
6+
import com.yapp.ndgl.data.travel.repository.PlaceRepository
7+
import com.yapp.ndgl.feature.travel.model.AlternativePlace
48
import com.yapp.ndgl.feature.travel.model.PlaceDetailTab
59
import com.yapp.ndgl.feature.travel.model.PlacePhoto
6-
import com.yapp.ndgl.feature.travel.model.PlaceType
710
import com.yapp.ndgl.feature.travel.model.Price
811
import com.yapp.ndgl.feature.travel.model.PriceRange
12+
import com.yapp.ndgl.feature.travel.model.TipContent
13+
import com.yapp.ndgl.feature.travel.model.toPlaceType
14+
import com.yapp.ndgl.navigation.model.RouteAlternativePlace
15+
import com.yapp.ndgl.navigation.model.RouteTipContent
916
import dagger.assisted.Assisted
1017
import dagger.assisted.AssistedFactory
1118
import dagger.assisted.AssistedInject
1219
import dagger.hilt.android.lifecycle.HiltViewModel
20+
import kotlinx.coroutines.delay
21+
import kotlinx.coroutines.launch
1322
import kotlin.time.Duration.Companion.hours
1423

1524
@HiltViewModel(assistedFactory = PlaceDetailViewModel.Factory::class)
1625
class PlaceDetailViewModel @AssistedInject constructor(
1726
@Assisted private val placeId: String,
27+
@Assisted private val tipContent: RouteTipContent?,
28+
@Assisted private val alternativePlaces: List<RouteAlternativePlace>,
29+
private val placeRepository: PlaceRepository,
1830
) : BaseViewModel<PlaceDetailState, PlaceDetailIntent, PlaceDetailSideEffect>(
1931
initialState = PlaceDetailState(),
2032
) {
2133
init {
22-
loadPlaceData()
34+
loadPlaceDetail()
2335
}
2436

25-
private fun loadPlaceData() {
26-
// TODO: Load from repository (현재는 테스트를 위한 더미 데이터)
27-
reduce {
28-
copy(
29-
placeInfo = PlaceInfo(
30-
id = placeId,
31-
name = "젤라테리아 파씨 (Gelateria Fassi)",
32-
placeType = PlaceType.RESTAURANT,
33-
priceRange = PriceRange(
34-
startPrice = Price(currencyCode = "EUR", units = "5", symbol = ""),
35-
endPrice = Price(currencyCode = "EUR", units = "15", symbol = ""),
36-
),
37-
address = "Via Principe Eugenio, 65, 00185 Roma RM, Italy",
38-
phoneNumber = "+39 06 446 4740",
39-
openingHours = "매일 12:00 ~ 24:00",
40-
googleMapsUri = "https://maps.google.com/?cid=14776686710302251978&g_mp=CiVnb29nbGUubWFwcy" +
41-
"5wbGFjZXMudjEuUGxhY2VzLkdldFBsYWNlEAIYBCAA",
42-
websiteUrl = "https://www.gyukatsu-motomura.com/shop/shinjukuhonten",
43-
rating = 4.7,
44-
userRatingCount = 12450,
45-
estimatedDuration = 1.hours,
46-
thumbnail = "https://images.unsplash.com/photo-1567206563064-6f60f40a2b57",
47-
tipContent = TipContent(
48-
creatorName = "",
49-
tips = listOf(
50-
"리조(쌀) 맛은 무조건 드셔보세요. 파씨의 시그니처입니다.",
51-
"생크림(Panna)을 무료로 올려주니 꼭 추가해서 드세요!",
52-
"매장 내부에 앉아서 먹을 수 있는 공간이 꽤 넓습니다.",
53-
),
54-
),
55-
alternativePlaces = listOf(
56-
AlternativePlace(
57-
id = 1,
58-
name = "폼피 티라미수 (Pompi Tiramisu)",
59-
thumbnail = "https://images.unsplash.com/photo-1571877227200-a0d98ea607e9",
60-
placeType = PlaceType.CAFE,
61-
),
62-
AlternativePlace(
63-
id = 2,
64-
name = "지올리띠 (Giolitti)",
65-
thumbnail = "https://images.unsplash.com/photo-1505394033343-43adc2f44bb2",
66-
placeType = PlaceType.CAFE,
67-
),
68-
AlternativePlace(
69-
id = 3,
70-
name = "라 로칸다 디 바코 (La Locanda di Bacco)",
71-
thumbnail = "https://images.unsplash.com/photo-1555396273-367ea4eb4db5",
72-
placeType = PlaceType.RESTAURANT,
73-
),
37+
private fun loadPlaceDetail() = viewModelScope.launch {
38+
suspendRunCatching {
39+
placeRepository.getPlace(placeId)
40+
}.onSuccess { response ->
41+
loadPlacePhotos()
42+
43+
val place = response.place
44+
reduce {
45+
copy(
46+
placeInfo = PlaceInfo(
47+
id = place.id,
48+
name = place.name,
49+
placeType = place.category.toPlaceType(),
50+
priceRange = place.priceRange?.let {
51+
PriceRange(
52+
startPrice = Price(
53+
currencyCode = it.startPrice.currencyCode,
54+
units = it.startPrice.units,
55+
symbol = it.startPrice.symbol,
56+
),
57+
endPrice = Price(currencyCode = it.endPrice.currencyCode, units = it.endPrice.units, symbol = it.endPrice.symbol),
58+
)
59+
},
60+
rating = place.rating,
61+
userRatingCount = place.userRatingCount,
62+
address = place.formattedAddress,
63+
phoneNumber = place.nationalPhoneNumber,
64+
openingHours = place.regularOpeningHours?.joinToString("\n"),
65+
googleMapsUri = place.googleMapsUri,
66+
websiteUrl = place.websiteUri,
67+
estimatedDuration = 1.hours,
68+
thumbnail = place.thumbnail.orEmpty(),
69+
tipContent = tipContent?.let { TipContent(creatorName = it.creatorName, tips = it.tips) },
70+
alternativePlaces = alternativePlaces.map { routePlace ->
71+
AlternativePlace(
72+
id = routePlace.id,
73+
name = routePlace.name,
74+
thumbnail = routePlace.thumbnail,
75+
placeType = routePlace.placeType.toPlaceType(),
76+
)
77+
},
78+
latitude = place.location.latitude,
79+
longitude = place.location.longitude,
7480
),
75-
latitude = 41.9028,
76-
longitude = 12.4964,
77-
),
78-
photos = listOf(
79-
PlacePhoto(url = "https://picsum.photos/id/10/400/600", width = 400, height = 600),
80-
PlacePhoto(url = "https://picsum.photos/id/20/600/400", width = 600, height = 400),
81-
PlacePhoto(url = "https://picsum.photos/id/30/400/400", width = 400, height = 400),
82-
PlacePhoto(url = "https://picsum.photos/id/40/400/500", width = 400, height = 500),
83-
PlacePhoto(url = "https://picsum.photos/id/50/500/400", width = 500, height = 400),
84-
PlacePhoto(url = "https://picsum.photos/id/60/400/300", width = 400, height = 300),
85-
PlacePhoto(url = "https://picsum.photos/id/70/300/400", width = 300, height = 400),
86-
PlacePhoto(url = "https://picsum.photos/id/80/400/450", width = 400, height = 450),
87-
),
88-
)
81+
)
82+
}
83+
}.onFailure {
84+
// TODO: 에러 처리
85+
}
86+
}
87+
88+
private fun loadPlacePhotos() = viewModelScope.launch {
89+
repeat(3) { index ->
90+
delay(1000)
91+
val result = suspendRunCatching { placeRepository.getPlacePhotos(placeId) }
92+
val photos = result.getOrNull()?.photos
93+
94+
if (!photos.isNullOrEmpty()) {
95+
reduce {
96+
copy(
97+
photos = photos.map { PlacePhoto(url = it.photoUri, width = it.widthPx, height = it.heightPx) },
98+
)
99+
}
100+
return@launch
101+
}
102+
103+
result.onFailure {
104+
// FIXME: 에러 뷰
105+
}
89106
}
90107
}
91108

@@ -131,6 +148,10 @@ class PlaceDetailViewModel @AssistedInject constructor(
131148

132149
@AssistedFactory
133150
interface Factory {
134-
fun create(placeId: String): PlaceDetailViewModel
151+
fun create(
152+
placeId: String,
153+
tipContent: RouteTipContent?,
154+
alternativePlaces: List<RouteAlternativePlace>,
155+
): PlaceDetailViewModel
135156
}
136157
}

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailContract.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package com.yapp.ndgl.feature.travel.traveldetail
33
import com.yapp.ndgl.core.base.UiIntent
44
import com.yapp.ndgl.core.base.UiSideEffect
55
import com.yapp.ndgl.core.base.UiState
6+
import com.yapp.ndgl.feature.travel.model.AlternativePlace
67
import com.yapp.ndgl.feature.travel.model.PlaceType
8+
import com.yapp.ndgl.feature.travel.model.TipContent
79
import com.yapp.ndgl.feature.travel.model.TransportSegment
810
import kotlin.time.Duration
911
import kotlin.time.Duration.Companion.hours
@@ -38,7 +40,7 @@ data class ContentInfo(
3840

3941
data class VideoInfo(
4042
val title: String = "",
41-
val name: String = "",
43+
val creatorName: String = "",
4244
val profileImage: String = "",
4345
val thumbnail: String = "",
4446
val link: String = "",
@@ -84,6 +86,8 @@ data class TravelPlace(
8486
val userData: UserData = UserData(),
8587
val startTime: Duration,
8688
val transportToNext: TransportSegment? = null,
89+
val travelerTips: List<String> = emptyList(),
90+
val alternativePlaces: List<AlternativePlace> = emptyList(),
8791
) {
8892
val duration: Duration
8993
get() = userData.estimatedDuration
@@ -122,7 +126,7 @@ sealed interface TravelDetailIntent : UiIntent {
122126
data class ClickAddMemo(val placeId: Int) : TravelDetailIntent
123127
data class ClickFindRoute(val googleMapsUri: String) : TravelDetailIntent
124128
data object DismissPlaceBottomSheet : TravelDetailIntent
125-
data class NavigateToPlaceDetail(val placeId: String) : TravelDetailIntent
129+
data class NavigateToTravelPlaceDetail(val placeId: String) : TravelDetailIntent
126130
data object DismissTimeBottomSheet : TravelDetailIntent
127131
data class ConfirmDuration(val duration: Duration) : TravelDetailIntent
128132
data object DismissCostModal : TravelDetailIntent
@@ -133,6 +137,11 @@ sealed interface TravelDetailIntent : UiIntent {
133137

134138
sealed interface TravelDetailSideEffect : UiSideEffect {
135139
data object NavigateBack : TravelDetailSideEffect
136-
data class NavigateToPlaceDetail(val placeId: String) : TravelDetailSideEffect
140+
data class NavigateToTravelPlaceDetail(
141+
val placeId: String,
142+
val tipContent: TipContent?,
143+
val alternativePlaces: List<AlternativePlace>?,
144+
) : TravelDetailSideEffect
145+
137146
data class NavigateToBrowser(val url: String) : TravelDetailSideEffect
138147
}

feature/travel/src/main/java/com/yapp/ndgl/feature/travel/traveldetail/TravelDetailScreen.kt

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ import com.yapp.ndgl.core.ui.util.dropShadow
6767
import com.yapp.ndgl.core.ui.util.launchBrowser
6868
import com.yapp.ndgl.core.ui.util.rememberReorderableState
6969
import com.yapp.ndgl.core.ui.util.reorderable
70+
import com.yapp.ndgl.feature.travel.model.AlternativePlace
7071
import com.yapp.ndgl.feature.travel.model.PlaceType
72+
import com.yapp.ndgl.feature.travel.model.TipContent
7173
import com.yapp.ndgl.feature.travel.model.TransportSegment
7274
import com.yapp.ndgl.feature.travel.model.TransportType
7375
import com.yapp.ndgl.feature.travel.traveldetail.component.ContentCard
@@ -90,16 +92,16 @@ import kotlin.time.Duration.Companion.minutes
9092
internal fun TravelDetailRoute(
9193
viewModel: TravelDetailViewModel = hiltViewModel(),
9294
navigateBack: () -> Unit,
93-
navigateToPlaceDetail: (String) -> Unit,
95+
navigateToTravelPlaceDetail: (String, TipContent?, List<AlternativePlace>?) -> Unit,
9496
) {
9597
val state by viewModel.collectAsState()
9698
val context = LocalContext.current
9799

98100
viewModel.collectSideEffect { sideEffect ->
99101
when (sideEffect) {
100102
is TravelDetailSideEffect.NavigateBack -> navigateBack()
101-
is TravelDetailSideEffect.NavigateToPlaceDetail -> {
102-
navigateToPlaceDetail(sideEffect.placeId)
103+
is TravelDetailSideEffect.NavigateToTravelPlaceDetail -> {
104+
navigateToTravelPlaceDetail(sideEffect.placeId, sideEffect.tipContent, sideEffect.alternativePlaces)
103105
}
104106

105107
is TravelDetailSideEffect.NavigateToBrowser -> {
@@ -134,7 +136,7 @@ internal fun TravelDetailRoute(
134136
confirmEditMode = { viewModel.onIntent(TravelDetailIntent.ConfirmEditMode) },
135137
clickPlaceItem = { viewModel.onIntent(TravelDetailIntent.ClickPlaceItem(it)) },
136138
dismissPlaceBottomSheet = { viewModel.onIntent(TravelDetailIntent.DismissPlaceBottomSheet) },
137-
navigateToPlaceDetail = { viewModel.onIntent(TravelDetailIntent.NavigateToPlaceDetail(it)) },
139+
navigateToTravelPlaceDetail = { viewModel.onIntent(TravelDetailIntent.NavigateToTravelPlaceDetail(it)) },
138140
clickAddTime = { viewModel.onIntent(TravelDetailIntent.ClickAddTime(it)) },
139141
clickAddMemo = { viewModel.onIntent(TravelDetailIntent.ClickAddMemo(it)) },
140142
clickAddCost = { viewModel.onIntent(TravelDetailIntent.ClickAddCost(it)) },
@@ -179,7 +181,7 @@ private fun TravelDetailScreen(
179181
clickAddCost: (Int) -> Unit,
180182
clickFindRoute: (String) -> Unit,
181183
dismissPlaceBottomSheet: () -> Unit,
182-
navigateToPlaceDetail: (String) -> Unit,
184+
navigateToTravelPlaceDetail: (String) -> Unit,
183185
dismissTimeBottomSheet: () -> Unit,
184186
confirmDuration: (Duration) -> Unit,
185187
dismissCostModal: () -> Unit,
@@ -532,7 +534,7 @@ private fun TravelDetailScreen(
532534
PlaceBottomSheet(
533535
place = state.selectedPlace,
534536
onDismissRequest = dismissPlaceBottomSheet,
535-
navigateToPlaceDetail = { navigateToPlaceDetail(state.selectedPlace.googlePlaceId) },
537+
navigateToTravelPlaceDetail = { navigateToTravelPlaceDetail(state.selectedPlace.googlePlaceId) },
536538
onAddTimeClick = clickAddTime,
537539
onAddCostClick = clickAddCost,
538540
onAddMemoClick = clickAddMemo,
@@ -627,7 +629,7 @@ private fun TravelDetailScreenPreview() {
627629
days = 4,
628630
videoInfo = VideoInfo(
629631
title = "방콕 풀코스, 동남아 안 가본 곽튜브와 함께 【방콕】",
630-
name = "빠니보틀",
632+
creatorName = "빠니보틀",
631633
profileImage = "",
632634
thumbnail = "",
633635
link = "",
@@ -699,7 +701,7 @@ private fun TravelDetailScreenPreview() {
699701
clickAddCost = {},
700702
clickFindRoute = {},
701703
dismissPlaceBottomSheet = {},
702-
navigateToPlaceDetail = {},
704+
navigateToTravelPlaceDetail = {},
703705
dismissTimeBottomSheet = {},
704706
confirmDuration = { _ -> },
705707
dismissCostModal = {},
@@ -728,7 +730,7 @@ private fun TravelDetailScreenEditModePreview() {
728730
days = 4,
729731
videoInfo = VideoInfo(
730732
title = "방콕 풀코스, 동남아 안 가본 곽튜브와 함께 【방콕】",
731-
name = "빠니보틀",
733+
creatorName = "빠니보틀",
732734
profileImage = "",
733735
thumbnail = "",
734736
link = "",
@@ -800,7 +802,7 @@ private fun TravelDetailScreenEditModePreview() {
800802
clickAddCost = {},
801803
clickFindRoute = {},
802804
dismissPlaceBottomSheet = {},
803-
navigateToPlaceDetail = {},
805+
navigateToTravelPlaceDetail = {},
804806
dismissTimeBottomSheet = {},
805807
confirmDuration = { _ -> },
806808
dismissCostModal = {},

0 commit comments

Comments
 (0)