Skip to content

Commit 8f9832e

Browse files
committed
[NDGL-61] design: 홈 화면 섹션별 UI 구현
1 parent 963681c commit 8f9832e

File tree

4 files changed

+740
-0
lines changed

4 files changed

+740
-0
lines changed
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
package com.yapp.ndgl.feature.home.main
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
8+
import androidx.compose.foundation.layout.BoxScope
9+
import androidx.compose.foundation.layout.Column
10+
import androidx.compose.foundation.layout.Row
11+
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.size
15+
import androidx.compose.foundation.shape.CircleShape
16+
import androidx.compose.foundation.shape.RoundedCornerShape
17+
import androidx.compose.material3.Icon
18+
import androidx.compose.material3.Text
19+
import androidx.compose.runtime.Composable
20+
import androidx.compose.ui.Alignment
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.draw.clip
23+
import androidx.compose.ui.graphics.vector.ImageVector
24+
import androidx.compose.ui.layout.ContentScale
25+
import androidx.compose.ui.res.painterResource
26+
import androidx.compose.ui.res.stringResource
27+
import androidx.compose.ui.res.vectorResource
28+
import androidx.compose.ui.tooling.preview.Preview
29+
import androidx.compose.ui.unit.dp
30+
import coil3.compose.AsyncImage
31+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
32+
import com.yapp.ndgl.feature.home.R
33+
import com.yapp.ndgl.feature.home.main.HomeState.MyTravel
34+
import com.yapp.ndgl.feature.home.main.HomeState.TravelPlace
35+
import java.time.LocalDate
36+
import java.time.format.DateTimeFormatter
37+
import com.yapp.ndgl.core.ui.R as CoreR
38+
39+
@Composable
40+
internal fun MyTravelCardSection(
41+
myTravel: MyTravel,
42+
modifier: Modifier = Modifier,
43+
) {
44+
when (myTravel) {
45+
MyTravel.None -> EmptyTravelCard(
46+
modifier = modifier,
47+
onCardClick = { /* FIXME: 인기 여행 컨텐츠 전체보기 페이지 이동 */ },
48+
)
49+
50+
is MyTravel.Upcoming -> UpcomingTravelCard(
51+
modifier = modifier,
52+
travel = myTravel,
53+
onCardClick = { /* FIXME: 내 여행 페이지 이동 */ },
54+
)
55+
56+
is MyTravel.InProgress -> InProgressTravelCard(
57+
travel = myTravel,
58+
onCardClick = { /* FIXME: 내 여행 페이지 이동 */ },
59+
onPlaceClick = { /* FIXME: 장소 상세 보기 페이지 이동 */ },
60+
)
61+
}
62+
}
63+
64+
@Composable
65+
private fun EmptyTravelCard(
66+
modifier: Modifier,
67+
onCardClick: () -> Unit,
68+
) {
69+
CardContainer(
70+
modifier = modifier,
71+
onCardClick = onCardClick,
72+
) {
73+
Row(
74+
modifier = Modifier
75+
.fillMaxWidth()
76+
.height(80.dp)
77+
.padding(horizontal = 16.dp, vertical = 8.dp),
78+
horizontalArrangement = Arrangement.spacedBy(4.dp),
79+
verticalAlignment = Alignment.CenterVertically,
80+
) {
81+
Column(
82+
modifier = Modifier.weight(1f),
83+
verticalArrangement = Arrangement.spacedBy(6.dp),
84+
) {
85+
Text(
86+
text = stringResource(R.string.home_my_travel_card_empty_title),
87+
style = NDGLTheme.typography.bodyLgSemiBold,
88+
color = NDGLTheme.colors.black700,
89+
)
90+
Text(
91+
text = stringResource(R.string.home_my_travel_card_empty_description),
92+
style = NDGLTheme.typography.bodyMdMedium,
93+
color = NDGLTheme.colors.black400,
94+
)
95+
}
96+
Image(
97+
painter = painterResource(CoreR.drawable.img_empty_calendar),
98+
contentDescription = null,
99+
modifier = Modifier.size(76.dp),
100+
)
101+
}
102+
}
103+
}
104+
105+
@Composable
106+
private fun UpcomingTravelCard(
107+
modifier: Modifier,
108+
travel: MyTravel.Upcoming,
109+
onCardClick: () -> Unit,
110+
) {
111+
CardContainer(
112+
modifier = modifier,
113+
onCardClick = onCardClick,
114+
) {
115+
Row(
116+
modifier = Modifier
117+
.fillMaxWidth()
118+
.padding(horizontal = 16.dp, vertical = 8.dp),
119+
horizontalArrangement = Arrangement.spacedBy(12.dp),
120+
verticalAlignment = Alignment.CenterVertically,
121+
) {
122+
AsyncImage(
123+
model = travel.imageUrl,
124+
contentDescription = travel.title,
125+
modifier = Modifier
126+
.size(64.dp)
127+
.clip(CircleShape),
128+
contentScale = ContentScale.Crop,
129+
)
130+
Column(
131+
modifier = Modifier.weight(1f),
132+
verticalArrangement = Arrangement.spacedBy(6.dp),
133+
) {
134+
Row(
135+
horizontalArrangement = Arrangement.spacedBy(8.dp),
136+
verticalAlignment = Alignment.CenterVertically,
137+
) {
138+
DayTag(dDay = travel.dDay)
139+
Text(
140+
text = travel.title,
141+
style = NDGLTheme.typography.subtitleMdSemiBold,
142+
color = NDGLTheme.colors.black700,
143+
)
144+
}
145+
val dateFormatter = DateTimeFormatter.ofPattern(
146+
stringResource(R.string.home_my_travel_card_date_format),
147+
)
148+
Text(
149+
text = stringResource(
150+
R.string.home_my_travel_card_travel_duration,
151+
travel.startDate.format(dateFormatter),
152+
travel.endDate.format(dateFormatter),
153+
),
154+
style = NDGLTheme.typography.bodyMdRegular,
155+
color = NDGLTheme.colors.black600,
156+
)
157+
}
158+
}
159+
}
160+
}
161+
162+
@Composable
163+
private fun DayTag(
164+
dDay: Int,
165+
modifier: Modifier = Modifier,
166+
) {
167+
Box(
168+
modifier = modifier
169+
.background(
170+
color = NDGLTheme.colors.black100,
171+
shape = RoundedCornerShape(999.dp),
172+
)
173+
.padding(horizontal = 12.dp, vertical = 4.dp),
174+
contentAlignment = Alignment.Center,
175+
) {
176+
Text(
177+
text = if (dDay <= 0) {
178+
stringResource(R.string.home_my_travel_card_d_day_minus, dDay)
179+
} else {
180+
stringResource(R.string.home_my_travel_card_d_day_plus, dDay)
181+
},
182+
style = NDGLTheme.typography.bodyMdMedium,
183+
color = NDGLTheme.colors.black400,
184+
)
185+
}
186+
}
187+
188+
@Composable
189+
private fun InProgressTravelCard(
190+
travel: MyTravel.InProgress,
191+
onCardClick: () -> Unit,
192+
onPlaceClick: () -> Unit,
193+
modifier: Modifier = Modifier,
194+
) {
195+
CardContainer(
196+
modifier = modifier,
197+
onCardClick = onCardClick,
198+
) {
199+
Column(
200+
modifier = Modifier
201+
.fillMaxWidth()
202+
.padding(16.dp),
203+
verticalArrangement = Arrangement.spacedBy(16.dp),
204+
) {
205+
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
206+
Text(
207+
text = stringResource(
208+
R.string.home_my_travel_card_in_progress_title,
209+
travel.title,
210+
travel.dayCount,
211+
),
212+
style = NDGLTheme.typography.subtitleMdSemiBold,
213+
color = NDGLTheme.colors.black700,
214+
)
215+
val dateFormatter = DateTimeFormatter.ofPattern(
216+
stringResource(R.string.home_my_travel_card_date_format),
217+
)
218+
Text(
219+
text = stringResource(
220+
R.string.home_my_travel_card_travel_duration,
221+
travel.startDate.format(dateFormatter),
222+
travel.endDate.format(dateFormatter),
223+
),
224+
style = NDGLTheme.typography.bodyMdRegular,
225+
color = NDGLTheme.colors.black500,
226+
)
227+
}
228+
229+
PlaceInfoCard(
230+
place = travel.currentPlace,
231+
onPlaceClick = onPlaceClick,
232+
)
233+
}
234+
}
235+
}
236+
237+
@Composable
238+
private fun PlaceInfoCard(
239+
place: TravelPlace,
240+
onPlaceClick: () -> Unit,
241+
modifier: Modifier = Modifier,
242+
) {
243+
Row(
244+
modifier = modifier
245+
.fillMaxWidth()
246+
.clip(RoundedCornerShape(16.dp))
247+
.background(NDGLTheme.colors.white)
248+
.clickable(onClick = onPlaceClick)
249+
.padding(16.dp),
250+
horizontalArrangement = Arrangement.spacedBy(12.dp),
251+
verticalAlignment = Alignment.CenterVertically,
252+
) {
253+
Column(
254+
modifier = Modifier.weight(1f),
255+
verticalArrangement = Arrangement.spacedBy(10.dp),
256+
) {
257+
Row(
258+
horizontalArrangement = Arrangement.spacedBy(4.dp),
259+
verticalAlignment = Alignment.CenterVertically,
260+
) {
261+
Icon(
262+
imageVector = ImageVector.vectorResource(CoreR.drawable.ic_14_car), // FIXME: category icon
263+
contentDescription = null,
264+
modifier = Modifier.size(14.dp),
265+
tint = NDGLTheme.colors.black400,
266+
)
267+
Text(
268+
text = place.category,
269+
color = NDGLTheme.colors.black400,
270+
style = NDGLTheme.typography.bodySmMedium,
271+
)
272+
Text(
273+
text = stringResource(R.string.home_common_dot_separator),
274+
color = NDGLTheme.colors.black400,
275+
style = NDGLTheme.typography.bodyMdMedium,
276+
)
277+
Text(
278+
text = place.estimatedTime,
279+
color = NDGLTheme.colors.black400,
280+
style = NDGLTheme.typography.bodySmMedium,
281+
)
282+
}
283+
Text(
284+
text = place.name,
285+
color = NDGLTheme.colors.black900,
286+
style = NDGLTheme.typography.bodyLgSemiBold,
287+
)
288+
}
289+
290+
AsyncImage(
291+
model = place.thumbnailUrl,
292+
contentDescription = place.name,
293+
modifier = Modifier
294+
.size(56.dp)
295+
.clip(RoundedCornerShape(4.dp)),
296+
contentScale = ContentScale.Crop,
297+
)
298+
}
299+
}
300+
301+
@Composable
302+
private fun CardContainer(
303+
modifier: Modifier,
304+
onCardClick: () -> Unit,
305+
content: @Composable BoxScope.() -> Unit,
306+
) {
307+
Box(
308+
modifier = modifier
309+
.padding(horizontal = 24.dp)
310+
.fillMaxWidth()
311+
.clip(RoundedCornerShape(4.dp))
312+
.background(NDGLTheme.colors.black50)
313+
.clickable(onClick = onCardClick),
314+
content = content,
315+
)
316+
}
317+
318+
@Preview(showBackground = true)
319+
@Composable
320+
private fun EmptyTravelCardPreview() {
321+
NDGLTheme {
322+
MyTravelCardSection(
323+
modifier = Modifier,
324+
myTravel = MyTravel.None,
325+
)
326+
}
327+
}
328+
329+
@Preview(showBackground = true)
330+
@Composable
331+
private fun UpcomingTravelCardPreview() {
332+
NDGLTheme {
333+
MyTravelCardSection(
334+
modifier = Modifier,
335+
myTravel = MyTravel.Upcoming(
336+
title = "도쿄 여행",
337+
dDay = -7,
338+
startDate = LocalDate.of(2025, 2, 15),
339+
endDate = LocalDate.of(2025, 2, 20),
340+
imageUrl = "",
341+
),
342+
)
343+
}
344+
}
345+
346+
@Preview(showBackground = true)
347+
@Composable
348+
private fun InProgressTravelCardPreview() {
349+
NDGLTheme {
350+
MyTravelCardSection(
351+
modifier = Modifier,
352+
myTravel = MyTravel.InProgress(
353+
title = "인도 여행",
354+
dayCount = 3,
355+
startDate = LocalDate.of(2025, 2, 1),
356+
endDate = LocalDate.of(2025, 2, 10),
357+
currentPlace = TravelPlace(
358+
category = "교통수단",
359+
estimatedTime = "1시간 체류 예상",
360+
name = "인도 국제 공항",
361+
thumbnailUrl = "",
362+
),
363+
),
364+
)
365+
}
366+
}

0 commit comments

Comments
 (0)