Skip to content

Commit 2daaeef

Browse files
committed
[NDGL-62] feature: DatePickerScreen 화면 제작 및 관련 컴포넌트 추가
1 parent 75514f3 commit 2daaeef

File tree

4 files changed

+723
-0
lines changed

4 files changed

+723
-0
lines changed
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+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.yapp.ndgl.feature.travel.datepicker
2+
3+
import com.yapp.ndgl.core.base.BaseViewModel
4+
import dagger.assisted.Assisted
5+
import dagger.assisted.AssistedFactory
6+
import dagger.assisted.AssistedInject
7+
import dagger.hilt.android.lifecycle.HiltViewModel
8+
import kotlinx.datetime.LocalDate
9+
10+
@HiltViewModel(assistedFactory = DatePickerViewModel.Factory::class)
11+
class DatePickerViewModel @AssistedInject constructor(
12+
@Assisted private val tripDays: Int,
13+
) : BaseViewModel<DatePickerState, DatePickerIntent, DatePickerSideEffect>(
14+
initialState = DatePickerState(tripDays = tripDays),
15+
) {
16+
override suspend fun handleIntent(intent: DatePickerIntent) {
17+
when (intent) {
18+
is DatePickerIntent.SelectDate -> selectDate(intent.date)
19+
is DatePickerIntent.SelectPreviousMonth -> selectPreviousMonth()
20+
is DatePickerIntent.SelectNextMonth -> selectNextMonth()
21+
is DatePickerIntent.SelectYearMonth -> selectYearMonth(intent.year, intent.month)
22+
is DatePickerIntent.ClickCompleteButton -> clickCompleteButton()
23+
is DatePickerIntent.DismissDialog -> dismissDialog()
24+
is DatePickerIntent.ClickViewTravelButton -> clickViewTravelButton()
25+
}
26+
}
27+
28+
private fun selectDate(date: LocalDate) {
29+
reduce {
30+
when {
31+
startDate == null -> {
32+
copy(
33+
startDate = date,
34+
endDate = null,
35+
isSelectingRange = true,
36+
)
37+
}
38+
39+
isSelectingRange -> {
40+
if (date == startDate) {
41+
copy(startDate = null, isSelectingRange = false)
42+
} else if (date < startDate) {
43+
copy(
44+
startDate = date,
45+
endDate = startDate,
46+
isSelectingRange = false,
47+
)
48+
} else {
49+
copy(
50+
endDate = date,
51+
isSelectingRange = false,
52+
)
53+
}
54+
}
55+
56+
else -> {
57+
copy(
58+
startDate = date,
59+
endDate = null,
60+
isSelectingRange = true,
61+
)
62+
}
63+
}
64+
}
65+
}
66+
67+
private fun selectPreviousMonth() {
68+
reduce {
69+
val newMonth = if (currentMonth == 1) 12 else currentMonth - 1
70+
val newYear = if (currentMonth == 1) currentYear - 1 else currentYear
71+
copy(currentYear = newYear, currentMonth = newMonth)
72+
}
73+
}
74+
75+
private fun selectNextMonth() {
76+
reduce {
77+
val newMonth = if (currentMonth == 12) 1 else currentMonth + 1
78+
val newYear = if (currentMonth == 12) currentYear + 1 else currentYear
79+
copy(currentYear = newYear, currentMonth = newMonth)
80+
}
81+
}
82+
83+
private fun selectYearMonth(year: Int, month: Int) {
84+
reduce { copy(currentYear = year, currentMonth = month) }
85+
}
86+
87+
private fun clickCompleteButton() {
88+
val startDate = state.value.startDate
89+
val endDate = state.value.endDate
90+
91+
if (startDate != null && endDate != null) {
92+
reduce { copy(showDialog = true) }
93+
}
94+
}
95+
96+
private fun dismissDialog() {
97+
reduce { copy(showDialog = false) }
98+
}
99+
100+
private fun clickViewTravelButton() {
101+
val startDate = state.value.startDate
102+
val endDate = state.value.endDate
103+
104+
if (startDate != null && endDate != null) {
105+
postSideEffect(DatePickerSideEffect.NavigateToTravelDetail)
106+
}
107+
}
108+
109+
@AssistedFactory
110+
interface Factory {
111+
fun create(@Assisted tripDays: Int): DatePickerViewModel
112+
}
113+
}

0 commit comments

Comments
 (0)