From 238366424d8d45863cf835460d04831d22501bdb Mon Sep 17 00:00:00 2001 From: JaeYoung290 <172613798+JaeYoung290@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:02:33 +0900 Subject: [PATCH 1/3] feature: add callvan create state and viewmodel --- .../callvan/ui/create/CallvanCreateState.kt | 58 +++++ .../ui/create/CallvanCreateViewModel.kt | 200 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateState.kt create mode 100644 feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateState.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateState.kt new file mode 100644 index 0000000000..9ca31786e9 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateState.kt @@ -0,0 +1,58 @@ +package `in`.koreatech.koin.feature.callvan.ui.create + +import `in`.koreatech.koin.feature.callvan.model.CallvanLocationOption +import java.util.Calendar + +data class CallvanCreateState( + val departureLocation: CallvanLocationOption? = null, + val arrivalLocation: CallvanLocationOption? = null, + val departureCustomText: String? = null, + val arrivalCustomText: String? = null, + val selectedYear: Int = Calendar.getInstance().get(Calendar.YEAR), + val selectedMonth: Int = Calendar.getInstance().get(Calendar.MONTH) + 1, + val selectedDay: Int = Calendar.getInstance().get(Calendar.DAY_OF_MONTH), + val selectedHour: Int = 12, + val selectedMinute: Int = 0, + val isAm: Boolean = true, + val maxParticipants: Int = 1, + val isDatePickerVisible: Boolean = false, + val isTimePickerVisible: Boolean = false, + val isLocationPickerVisible: Boolean = false, + val isPickingDeparture: Boolean = true, + val isSubmitting: Boolean = false +) { + val isFormComplete: Boolean + get() { + val departureValid = departureLocation != null && + (departureLocation != CallvanLocationOption.OTHER || !departureCustomText.isNullOrBlank()) + val arrivalValid = arrivalLocation != null && + (arrivalLocation != CallvanLocationOption.OTHER || !arrivalCustomText.isNullOrBlank()) + return departureValid && arrivalValid + } + + val formattedDate: String + get() = "${selectedYear}년 ${selectedMonth}월 ${selectedDay}일" + + val formattedTime: String + get() = "%02d:%02d".format(selectedHour, selectedMinute) + + val amPmText: String + get() = if (isAm) "오전" else "오후" + + val participantsText: String + get() = "$maxParticipants 명" + + val apiDepartureDate: String + get() = "%04d-%02d-%02d".format(selectedYear, selectedMonth, selectedDay) + + val apiDepartureTime: String + get() { + val hour24 = when { + isAm && selectedHour == 12 -> 0 + isAm -> selectedHour + !isAm && selectedHour == 12 -> 12 + else -> selectedHour + 12 + } + return "%02d:%02d".format(hour24, selectedMinute) + } +} diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt new file mode 100644 index 0000000000..1bc0e93c85 --- /dev/null +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt @@ -0,0 +1,200 @@ +package `in`.koreatech.koin.feature.callvan.ui.create + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import `in`.koreatech.koin.domain.usecase.callvan.CreateCallvanPostUseCase +import `in`.koreatech.koin.feature.callvan.model.CallvanLocationOption +import java.util.Calendar +import javax.inject.Inject +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.blockingIntent +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container + +@HiltViewModel +class CallvanCreateViewModel @Inject constructor( + private val createCallvanPostUseCase: CreateCallvanPostUseCase +) : ViewModel(), ContainerHost { + + override val container: Container = container( + CallvanCreateState() + ) + + fun openDepartureLocationPicker() = blockingIntent { + reduce { state.copy(isLocationPickerVisible = true, isPickingDeparture = true) } + } + + fun openArrivalLocationPicker() = blockingIntent { + reduce { state.copy(isLocationPickerVisible = true, isPickingDeparture = false) } + } + + fun closeLocationPicker() = blockingIntent { + reduce { state.copy(isLocationPickerVisible = false) } + } + + fun selectLocation(location: CallvanLocationOption, customText: String? = null) = blockingIntent { + reduce { + if (state.isPickingDeparture) { + state.copy( + departureLocation = location, + departureCustomText = customText, + isLocationPickerVisible = false + ) + } else { + state.copy( + arrivalLocation = location, + arrivalCustomText = customText, + isLocationPickerVisible = false + ) + } + } + } + + fun swapLocations() = blockingIntent { + reduce { + state.copy( + departureLocation = state.arrivalLocation, + arrivalLocation = state.departureLocation, + departureCustomText = state.arrivalCustomText, + arrivalCustomText = state.departureCustomText + ) + } + } + + fun toggleDatePicker() = blockingIntent { + reduce { + state.copy( + isDatePickerVisible = !state.isDatePickerVisible, + isTimePickerVisible = false + ) + } + } + + fun updateYear(yearIndex: Int) = blockingIntent { + val currentYear = Calendar.getInstance().get(Calendar.YEAR) + val newYear = currentYear + yearIndex + reduce { + val maxDay = getDaysInMonth(newYear, state.selectedMonth) + state.copy( + selectedYear = newYear, + selectedDay = state.selectedDay.coerceAtMost(maxDay) + ) + } + } + + fun updateMonth(monthIndex: Int) = blockingIntent { + val newMonth = monthIndex + 1 + reduce { + val maxDay = getDaysInMonth(state.selectedYear, newMonth) + state.copy( + selectedMonth = newMonth, + selectedDay = state.selectedDay.coerceAtMost(maxDay) + ) + } + } + + fun updateDay(dayIndex: Int) = blockingIntent { + reduce { state.copy(selectedDay = dayIndex + 1) } + } + + fun resetDate() = blockingIntent { + val today = Calendar.getInstance() + reduce { + state.copy( + selectedYear = today.get(Calendar.YEAR), + selectedMonth = today.get(Calendar.MONTH) + 1, + selectedDay = today.get(Calendar.DAY_OF_MONTH) + ) + } + } + + fun confirmDate() = blockingIntent { + reduce { state.copy(isDatePickerVisible = false) } + } + + fun toggleTimePicker() = blockingIntent { + reduce { + state.copy( + isTimePickerVisible = !state.isTimePickerVisible, + isDatePickerVisible = false + ) + } + } + + fun updateAmPm(amPmIndex: Int) = blockingIntent { + reduce { state.copy(isAm = amPmIndex == 0) } + } + + fun updateHour(hourIndex: Int) = blockingIntent { + reduce { state.copy(selectedHour = hourIndex + 1) } + } + + fun updateMinute(minuteIndex: Int) = blockingIntent { + reduce { state.copy(selectedMinute = minuteIndex) } + } + + fun resetTime() = blockingIntent { + reduce { state.copy(selectedHour = 12, selectedMinute = 0, isAm = true) } + } + + fun confirmTime() = blockingIntent { + reduce { state.copy(isTimePickerVisible = false) } + } + + fun decrementParticipants() = blockingIntent { + reduce { + if (state.maxParticipants > 1) { + state.copy(maxParticipants = state.maxParticipants - 1) + } else { + state + } + } + } + + fun incrementParticipants() = blockingIntent { + reduce { + if (state.maxParticipants < 8) { + state.copy(maxParticipants = state.maxParticipants + 1) + } else { + state + } + } + } + + fun submit() = intent { + val currentState = state + if (!currentState.isFormComplete || currentState.isSubmitting) return@intent + reduce { state.copy(isSubmitting = true) } + createCallvanPostUseCase( + departureType = if (currentState.departureLocation == CallvanLocationOption.OTHER) { + currentState.departureCustomText ?: "" + } else { + currentState.departureLocation!!.type + }, + arrivalType = if (currentState.arrivalLocation == CallvanLocationOption.OTHER) { + currentState.arrivalCustomText ?: "" + } else { + currentState.arrivalLocation!!.type + }, + departureDate = currentState.apiDepartureDate, + departureTime = currentState.apiDepartureTime, + maxParticipants = currentState.maxParticipants + ).onSuccess { post -> + reduce { state.copy(isSubmitting = false) } + postSideEffect(CallvanCreateSideEffect.NavigateToDetail(post.id)) + }.onFailure { + reduce { state.copy(isSubmitting = false) } + postSideEffect(CallvanCreateSideEffect.ShowSubmitError) + } + } + + private fun getDaysInMonth(year: Int, month: Int): Int { + return Calendar.getInstance().apply { + set(Calendar.YEAR, year) + set(Calendar.MONTH, month - 1) + }.getActualMaximum(Calendar.DAY_OF_MONTH) + } +} From 5a34625de5e270a9e13b9853859badeec1304afb Mon Sep 17 00:00:00 2001 From: JaeYoung290 <172613798+JaeYoung290@users.noreply.github.com> Date: Sat, 7 Mar 2026 21:51:20 +0900 Subject: [PATCH 2/3] fix: suppress TooManyFunctions in CallvanCreateViewModel --- .../koin/feature/callvan/ui/create/CallvanCreateViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt index 1bc0e93c85..d497ef5867 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt @@ -14,6 +14,7 @@ import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container +@Suppress("TooManyFunctions") @HiltViewModel class CallvanCreateViewModel @Inject constructor( private val createCallvanPostUseCase: CreateCallvanPostUseCase From ba69007ef7a8f0a4109612e8e0de1fa1f0c5125d Mon Sep 17 00:00:00 2001 From: JaeYoung290 <172613798+JaeYoung290@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:28:53 +0900 Subject: [PATCH 3/3] refactor: Optimize getDaysInMonth calculation in CallvanCreateViewModel --- .../koin/feature/callvan/ui/create/CallvanCreateViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt index d497ef5867..ddd7f4a466 100644 --- a/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt +++ b/feature/callvan/src/main/java/in/koreatech/koin/feature/callvan/ui/create/CallvanCreateViewModel.kt @@ -194,8 +194,8 @@ class CallvanCreateViewModel @Inject constructor( private fun getDaysInMonth(year: Int, month: Int): Int { return Calendar.getInstance().apply { - set(Calendar.YEAR, year) - set(Calendar.MONTH, month - 1) + clear() + set(year, month - 1, 1) }.getActualMaximum(Calendar.DAY_OF_MONTH) } }