Skip to content

Commit 712805c

Browse files
authored
Merge pull request #15 from YAPP-Github/design/NDGL-62
[NDGL-62] 날짜 선택 화면 UI/UX 제작
2 parents 6a2d1fb + 12e75c7 commit 712805c

File tree

12 files changed

+768
-1
lines changed

12 files changed

+768
-1
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:pathData="M11.5,14.725L6.55,9.775H16.45L11.5,14.725Z"
8+
android:fillColor="#49454F"/>
9+
</vector>

core/ui/src/main/res/values/strings.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,13 @@
2828
<!-- Follow Travel -->
2929
<string name="follow_travel_button">내 여행에 저장</string>
3030

31+
<!-- Date Picker -->
32+
<string name="date_picker_title">여행 따라가기</string>
33+
<string name="date_picker_complete">완료</string>
34+
<string name="date_picker_modal_title">여행이 준비됐어요</string>
35+
<string name="date_picker_modal_body">이제 선택한 여행을\n그대로 따라갈 수 있어요.</string>
36+
<string name="date_picker_modal_negative">나중에</string>
37+
<string name="date_picker_modal_positive">여행 보러가기</string>
38+
<string name="date_picker_year_month_format">%1$d년 %2$d월</string>
39+
<string name="date_picker_error_insufficient">* 선택한 여행 기간이 따라가기 일정보다 짧아요.\n 기간을 넘는 일정은 지정되지 않습니다.</string>
3140
</resources>

feature/travel/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ dependencies {
1111
implementation(libs.maps.compose)
1212
implementation(libs.play.services.maps)
1313
implementation(libs.coil.compose)
14+
implementation(libs.kotlinx.datetime)
1415
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.yapp.ndgl.feature.travel.datepicker
2+
3+
import com.yapp.ndgl.core.base.UiIntent
4+
import com.yapp.ndgl.core.base.UiSideEffect
5+
import com.yapp.ndgl.core.base.UiState
6+
import kotlinx.datetime.Clock.System.now
7+
import kotlinx.datetime.LocalDate
8+
import kotlinx.datetime.LocalDateTime
9+
import kotlinx.datetime.TimeZone.Companion.currentSystemDefault
10+
import kotlinx.datetime.toLocalDateTime
11+
12+
data class DatePickerState(
13+
val tripDays: Int,
14+
private val initialDateTime: LocalDateTime = now().toLocalDateTime(currentSystemDefault()),
15+
val currentYear: Int = initialDateTime.year,
16+
val currentMonth: Int = initialDateTime.monthNumber,
17+
val startDate: LocalDate? = null,
18+
val endDate: LocalDate? = null,
19+
val isSelectingRange: Boolean = false,
20+
val showDialog: Boolean = false,
21+
) : UiState {
22+
val isDateSelected: Boolean
23+
get() = startDate != null && endDate != null
24+
25+
val isInsufficientDuration: Boolean
26+
get() = if (startDate != null && endDate != null) {
27+
(endDate.toEpochDays() - startDate.toEpochDays() + 1) < tripDays
28+
} else {
29+
false
30+
}
31+
}
32+
33+
sealed interface DatePickerIntent : UiIntent {
34+
data class SelectDate(val date: LocalDate) : DatePickerIntent
35+
data object SelectPreviousMonth : DatePickerIntent
36+
data object SelectNextMonth : DatePickerIntent
37+
data class SelectYearMonth(val year: Int, val month: Int) : DatePickerIntent
38+
data object ClickCompleteButton : DatePickerIntent
39+
data object DismissDialog : DatePickerIntent
40+
data object ClickViewTravelButton : DatePickerIntent
41+
}
42+
43+
sealed interface DatePickerSideEffect : UiSideEffect {
44+
// TODO 실제 로직으로 변경
45+
data object NavigateToTravelDetail : DatePickerSideEffect
46+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package com.yapp.ndgl.feature.travel.datepicker
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.PaddingValues
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.material3.Text
13+
import androidx.compose.runtime.Composable
14+
import androidx.compose.runtime.getValue
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.res.stringResource
17+
import androidx.compose.ui.tooling.preview.Preview
18+
import androidx.compose.ui.unit.dp
19+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
20+
import com.yapp.ndgl.core.ui.R
21+
import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButton
22+
import com.yapp.ndgl.core.ui.designsystem.NDGLCTAButtonAttr
23+
import com.yapp.ndgl.core.ui.designsystem.NDGLModal
24+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBar
25+
import com.yapp.ndgl.core.ui.designsystem.NDGLNavigationBarAttr
26+
import com.yapp.ndgl.core.ui.theme.NDGLTheme
27+
import com.yapp.ndgl.feature.travel.datepicker.component.CalendarView
28+
import kotlinx.datetime.LocalDate
29+
30+
@Composable
31+
internal fun DatePickerRoute(
32+
viewModel: DatePickerViewModel = hiltViewModel(),
33+
navigateBack: () -> Unit = {},
34+
innerPadding: PaddingValues,
35+
) {
36+
val state by viewModel.collectAsState()
37+
38+
fun selectDate(date: LocalDate) {
39+
viewModel.onIntent(DatePickerIntent.SelectDate(date))
40+
}
41+
42+
fun selectPreviousMonth() {
43+
viewModel.onIntent(DatePickerIntent.SelectPreviousMonth)
44+
}
45+
46+
fun selectNextMonth() {
47+
viewModel.onIntent(DatePickerIntent.SelectNextMonth)
48+
}
49+
50+
fun clickCompleteButton() {
51+
viewModel.onIntent(DatePickerIntent.ClickCompleteButton)
52+
}
53+
54+
fun dismissDialog() {
55+
viewModel.onIntent(DatePickerIntent.DismissDialog)
56+
}
57+
58+
fun clickTravelButton() {
59+
viewModel.onIntent(DatePickerIntent.ClickViewTravelButton)
60+
}
61+
62+
DatePickerScreen(
63+
state = state,
64+
selectDate = ::selectDate,
65+
selectPreviousMonth = ::selectPreviousMonth,
66+
selectNextMonth = ::selectNextMonth,
67+
clickCompleteButton = ::clickCompleteButton,
68+
clickBackButton = navigateBack,
69+
dismissDialog = ::dismissDialog,
70+
clickTravelButton = ::clickTravelButton,
71+
innerPadding = innerPadding,
72+
)
73+
74+
viewModel.collectSideEffect { sideEffect ->
75+
when (sideEffect) {
76+
is DatePickerSideEffect.NavigateToTravelDetail -> {
77+
// TODO
78+
}
79+
}
80+
}
81+
}
82+
83+
@Composable
84+
private fun DatePickerScreen(
85+
state: DatePickerState,
86+
selectDate: (LocalDate) -> Unit,
87+
selectPreviousMonth: () -> Unit,
88+
selectNextMonth: () -> Unit,
89+
clickCompleteButton: () -> Unit,
90+
clickBackButton: () -> Unit,
91+
dismissDialog: () -> Unit,
92+
clickTravelButton: () -> Unit,
93+
innerPadding: PaddingValues,
94+
) {
95+
Box(
96+
modifier = Modifier
97+
.fillMaxSize()
98+
.background(NDGLTheme.colors.white)
99+
.padding(innerPadding),
100+
) {
101+
Column(
102+
modifier = Modifier
103+
.fillMaxSize()
104+
.padding(bottom = 16.dp),
105+
) {
106+
NDGLNavigationBar(
107+
modifier = Modifier.fillMaxWidth(),
108+
headline = stringResource(R.string.date_picker_title),
109+
textAlignType = NDGLNavigationBarAttr.TextAlignType.CENTER,
110+
leadingIcon = R.drawable.ic_28_chevron_left,
111+
onLeadingIconClick = clickBackButton,
112+
)
113+
Spacer(Modifier.height(24.dp))
114+
Column(
115+
modifier = Modifier
116+
.fillMaxSize()
117+
.padding(horizontal = 24.dp),
118+
) {
119+
CalendarView(
120+
year = state.currentYear,
121+
month = state.currentMonth,
122+
startDate = state.startDate,
123+
endDate = state.endDate,
124+
onDateSelected = selectDate,
125+
onPreviousMonth = selectPreviousMonth,
126+
onNextMonth = selectNextMonth,
127+
)
128+
129+
if (state.isInsufficientDuration) {
130+
Spacer(Modifier.height(24.dp))
131+
Text(
132+
stringResource(
133+
R.string.date_picker_error_insufficient,
134+
),
135+
color = NDGLTheme.colors.red500,
136+
style = NDGLTheme.typography.bodySmMedium,
137+
)
138+
}
139+
Spacer(Modifier.weight(1f))
140+
NDGLCTAButton(
141+
modifier = Modifier
142+
.fillMaxWidth(),
143+
type = NDGLCTAButtonAttr.Type.PRIMARY,
144+
size = NDGLCTAButtonAttr.Size.LARGE,
145+
status = if (state.isDateSelected) {
146+
NDGLCTAButtonAttr.Status.ACTIVE
147+
} else {
148+
NDGLCTAButtonAttr.Status.DISABLED
149+
},
150+
label = stringResource(R.string.date_picker_complete),
151+
onClick = clickCompleteButton,
152+
)
153+
}
154+
}
155+
156+
if (state.showDialog) {
157+
NDGLModal(
158+
onDismissRequest = dismissDialog,
159+
title = stringResource(R.string.date_picker_modal_title),
160+
body = stringResource(R.string.date_picker_modal_body),
161+
negativeButtonText = stringResource(R.string.date_picker_modal_negative),
162+
positiveButtonText = stringResource(R.string.date_picker_modal_positive),
163+
onPositiveButtonClick = clickTravelButton,
164+
)
165+
}
166+
}
167+
}
168+
169+
@Preview(showBackground = true)
170+
@Composable
171+
private fun DatePickerScreenPreview() {
172+
NDGLTheme {
173+
DatePickerScreen(
174+
state = DatePickerState(
175+
tripDays = 3,
176+
),
177+
selectDate = {},
178+
selectPreviousMonth = {},
179+
selectNextMonth = {},
180+
clickCompleteButton = {},
181+
clickBackButton = {},
182+
dismissDialog = {},
183+
clickTravelButton = {},
184+
innerPadding = PaddingValues(),
185+
)
186+
}
187+
}
188+
189+
@Preview(showBackground = true)
190+
@Composable
191+
private fun DatePickerScreenWithDialogPreview() {
192+
NDGLTheme {
193+
DatePickerScreen(
194+
state = DatePickerState(
195+
currentYear = 2026,
196+
currentMonth = 4,
197+
tripDays = 3,
198+
startDate = LocalDate(2026, 4, 23),
199+
endDate = LocalDate(2026, 4, 27),
200+
showDialog = true,
201+
),
202+
selectDate = {},
203+
selectPreviousMonth = {},
204+
selectNextMonth = {},
205+
clickCompleteButton = {},
206+
clickBackButton = {},
207+
dismissDialog = {},
208+
clickTravelButton = {},
209+
innerPadding = PaddingValues(),
210+
)
211+
}
212+
}

0 commit comments

Comments
 (0)