From ed986c3dd3b1405a1c243cee38be6dc763cd0eee Mon Sep 17 00:00:00 2001 From: Ray Jang <48707913+ajou4095@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:02:07 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20ScheduleAdd=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../schedule/MockScheduleRepository.kt | 5 +- .../schedule/RealScheduleRepository.kt | 9 +- .../model/feature/schedule/AlarmRepeatType.kt | 6 + .../domain/repository/ScheduleRepository.kt | 5 +- .../feature/schedule/AddScheduleUseCase.kt | 3 +- .../feature/schedule/EditScheduleUseCase.kt | 3 +- .../ui/main/home/HomeDestination.kt | 4 +- .../presentation/ui/main/home/HomeScreen.kt | 6 +- .../common/relation/get/GetRelationScreen.kt | 3 +- .../ui/main/home/schedule/ScheduleScreen.kt | 23 +- .../schedule/add/ScheduleAddDestination.kt | 1 + .../home/schedule/add/ScheduleAddIntent.kt | 4 +- .../home/schedule/add/ScheduleAddScreen.kt | 544 +++++++++++++++++- .../home/schedule/add/ScheduleAddViewModel.kt | 8 + .../home/schedule/add/ScheduleDestination.kt | 10 + .../src/main/res/drawable/ic_clock.xml | 20 + .../src/main/res/drawable/ic_link.xml | 23 +- .../src/main/res/drawable/ic_location.xml | 23 + .../src/main/res/drawable/ic_memo.xml | 20 + .../src/main/res/drawable/ic_repeat.xml | 37 ++ 20 files changed, 733 insertions(+), 24 deletions(-) create mode 100644 domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/feature/schedule/AlarmRepeatType.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleDestination.kt create mode 100644 presentation/src/main/res/drawable/ic_clock.xml create mode 100644 presentation/src/main/res/drawable/ic_location.xml create mode 100644 presentation/src/main/res/drawable/ic_memo.xml create mode 100644 presentation/src/main/res/drawable/ic_repeat.xml diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/MockScheduleRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/MockScheduleRepository.kt index c28572bc..576c19a8 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/MockScheduleRepository.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/MockScheduleRepository.kt @@ -1,5 +1,6 @@ package ac.dnd.bookkeeping.android.data.repository.feature.schedule +import ac.dnd.bookkeeping.android.domain.model.feature.schedule.AlarmRepeatType import ac.dnd.bookkeeping.android.domain.model.feature.schedule.UnrecordedSchedule import ac.dnd.bookkeeping.android.domain.model.feature.schedule.UnrecordedScheduleRelation import ac.dnd.bookkeeping.android.domain.repository.ScheduleRepository @@ -15,7 +16,7 @@ class MockScheduleRepository @Inject constructor() : ScheduleRepository { relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, @@ -32,7 +33,7 @@ class MockScheduleRepository @Inject constructor() : ScheduleRepository { relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/RealScheduleRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/RealScheduleRepository.kt index 1ac5ee8a..0890b167 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/RealScheduleRepository.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/feature/schedule/RealScheduleRepository.kt @@ -4,6 +4,7 @@ import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager import ac.dnd.bookkeeping.android.data.remote.network.api.ScheduleApi import ac.dnd.bookkeeping.android.data.remote.network.util.toDomain import ac.dnd.bookkeeping.android.domain.model.feature.group.Group +import ac.dnd.bookkeeping.android.domain.model.feature.schedule.AlarmRepeatType import ac.dnd.bookkeeping.android.domain.model.feature.schedule.UnrecordedSchedule import ac.dnd.bookkeeping.android.domain.repository.GroupRepository import ac.dnd.bookkeeping.android.domain.repository.ScheduleRepository @@ -20,7 +21,7 @@ class RealScheduleRepository @Inject constructor( relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, @@ -32,7 +33,7 @@ class RealScheduleRepository @Inject constructor( relationId = relationId, day = day, event = event, - repeatType = repeatType, + repeatType = repeatType.value, repeatFinish = repeatFinish, alarm = alarm, time = time, @@ -47,7 +48,7 @@ class RealScheduleRepository @Inject constructor( relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, @@ -60,7 +61,7 @@ class RealScheduleRepository @Inject constructor( relationId = relationId, day = day, event = event, - repeatType = repeatType, + repeatType = repeatType.value, repeatFinish = repeatFinish, alarm = alarm, time = time, diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/feature/schedule/AlarmRepeatType.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/feature/schedule/AlarmRepeatType.kt new file mode 100644 index 00000000..27140c31 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/feature/schedule/AlarmRepeatType.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.domain.model.feature.schedule + +sealed class AlarmRepeatType(val value: String) { + data object Month : AlarmRepeatType("month") + data object Year : AlarmRepeatType("year") +} diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/ScheduleRepository.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/ScheduleRepository.kt index 6a874ff6..e6a0f028 100644 --- a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/ScheduleRepository.kt +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/ScheduleRepository.kt @@ -1,5 +1,6 @@ package ac.dnd.bookkeeping.android.domain.repository +import ac.dnd.bookkeeping.android.domain.model.feature.schedule.AlarmRepeatType import ac.dnd.bookkeeping.android.domain.model.feature.schedule.UnrecordedSchedule import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -11,7 +12,7 @@ interface ScheduleRepository { relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, @@ -25,7 +26,7 @@ interface ScheduleRepository { relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/AddScheduleUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/AddScheduleUseCase.kt index 00dd5a4f..c8f3ef05 100644 --- a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/AddScheduleUseCase.kt +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/AddScheduleUseCase.kt @@ -1,5 +1,6 @@ package ac.dnd.bookkeeping.android.domain.usecase.feature.schedule +import ac.dnd.bookkeeping.android.domain.model.feature.schedule.AlarmRepeatType import ac.dnd.bookkeeping.android.domain.repository.ScheduleRepository import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -13,7 +14,7 @@ class AddScheduleUseCase @Inject constructor( relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/EditScheduleUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/EditScheduleUseCase.kt index c9d3482a..7487722d 100644 --- a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/EditScheduleUseCase.kt +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/schedule/EditScheduleUseCase.kt @@ -1,5 +1,6 @@ package ac.dnd.bookkeeping.android.domain.usecase.feature.schedule +import ac.dnd.bookkeeping.android.domain.model.feature.schedule.AlarmRepeatType import ac.dnd.bookkeeping.android.domain.repository.ScheduleRepository import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -14,7 +15,7 @@ class EditScheduleUseCase @Inject constructor( relationId: Long, day: LocalDate, event: String, - repeatType: String, + repeatType: AlarmRepeatType, repeatFinish: LocalDate, alarm: LocalDateTime, time: LocalTime, diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeDestination.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeDestination.kt index 2805b21c..21b3c4d4 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeDestination.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeDestination.kt @@ -2,8 +2,8 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.home import ac.dnd.bookkeeping.android.presentation.common.util.ErrorObserver import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import ac.dnd.bookkeeping.android.presentation.ui.main.home.schedule.add.scheduleDestination import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavGraphBuilder @@ -35,4 +35,6 @@ fun NavGraphBuilder.homeDestination( handler = viewModel.handler ) } + + scheduleDestination(appState) } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt index cd87080e..d5c08d6b 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/HomeScreen.kt @@ -68,12 +68,12 @@ fun HomeScreen( icon = R.drawable.ic_launcher ), MainBottomBarItem( - route = ScheduleConstant.ROUTE, + route = StatisticsConstant.ROUTE, name = "통계", icon = R.drawable.ic_launcher ), MainBottomBarItem( - route = StatisticsConstant.ROUTE, + route = ScheduleConstant.ROUTE, name = "일정", icon = R.drawable.ic_launcher ), @@ -123,11 +123,13 @@ fun HomeScreen( appState = appState ) } + ScheduleConstant.ROUTE -> { ScheduleScreen( appState = appState ) } + StatisticsConstant.ROUTE -> { StatisticsScreen( appState = appState diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationScreen.kt index 6348aeed..a218da0c 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationScreen.kt @@ -350,8 +350,7 @@ private fun SearchRelationRelation( } } -// TODO : Preview 안보임. -@Preview +@Preview(apiLevel = 33) @Composable fun SearchRelationScreenPreview() { SearchRelationScreen( diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/ScheduleScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/ScheduleScreen.kt index e0626c15..01e310e5 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/ScheduleScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/ScheduleScreen.kt @@ -6,12 +6,20 @@ import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.Event import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.eventObserve import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import ac.dnd.bookkeeping.android.presentation.ui.main.home.schedule.add.ScheduleAddConstant import ac.dnd.bookkeeping.android.presentation.ui.main.rememberApplicationState +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.CoroutineExceptionHandler @@ -50,8 +58,21 @@ private fun ScheduleScreen( ) { val scope = rememberCoroutineScope() - Box { + fun navigateToScheduleAdd() { + appState.navController.navigate(ScheduleAddConstant.ROUTE) + } + Box { + Text( + text = "ScheduleAdd", + fontSize = 20.sp, + color = Color.Black, + modifier = Modifier + .padding(vertical = 10.dp) + .clickable { + navigateToScheduleAdd() + } + ) } LaunchedEffectWithLifecycle(event, handler) { diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddDestination.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddDestination.kt index ec2bbc48..c00b402e 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddDestination.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddDestination.kt @@ -15,6 +15,7 @@ fun NavGraphBuilder.scheduleAddDestination( composable( route = ScheduleAddConstant.ROUTE ) { + // TODO : 수정 argument 추가 val viewModel: ScheduleAddViewModel = hiltViewModel() val model: ScheduleAddModel = let { diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddIntent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddIntent.kt index ba892607..ece38575 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddIntent.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddIntent.kt @@ -1,3 +1,5 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.home.schedule.add -sealed interface ScheduleAddIntent +sealed interface ScheduleAddIntent { + data object OnConfirm : ScheduleAddIntent +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddScreen.kt index 002c9766..2ef454ba 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddScreen.kt @@ -1,16 +1,83 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.home.schedule.add +import ac.dnd.bookkeeping.android.domain.model.feature.relation.RelationDetail +import ac.dnd.bookkeeping.android.domain.model.feature.schedule.AlarmRepeatType +import ac.dnd.bookkeeping.android.presentation.R +import ac.dnd.bookkeeping.android.presentation.common.theme.Body0 +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray200 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline3 +import ac.dnd.bookkeeping.android.presentation.common.theme.Icon24 +import ac.dnd.bookkeeping.android.presentation.common.theme.Negative +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary4 +import ac.dnd.bookkeeping.android.presentation.common.theme.Shapes import ac.dnd.bookkeeping.android.presentation.common.util.LaunchedEffectWithLifecycle import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.eventObserve +import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipItem +import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipType +import ac.dnd.bookkeeping.android.presentation.common.view.component.FieldSelectComponent +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButton +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonProperties +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonSize +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonType +import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextField +import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextFieldType +import ac.dnd.bookkeeping.android.presentation.model.history.HistoryEventType import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import ac.dnd.bookkeeping.android.presentation.ui.main.home.common.notification.NotificationConstant import ac.dnd.bookkeeping.android.presentation.ui.main.rememberApplicationState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.minus +import kotlinx.datetime.number +import kotlinx.datetime.todayIn @Composable fun ScheduleAddScreen( @@ -21,9 +88,484 @@ fun ScheduleAddScreen( handler: CoroutineExceptionHandler ) { val scope = rememberCoroutineScope() + val now = Clock.System.todayIn(TimeZone.currentSystemDefault()) + val eventTypeList: List = HistoryEventType.entries - Box { + // TODO : rememberSaveable 을 위한 Parcelable Presentation Model + var relation: RelationDetail? by remember { mutableStateOf(null) } + var date: LocalDate by remember { mutableStateOf(now) } + var eventName: String by remember { mutableStateOf("") } + var alarm: LocalDateTime? by remember { mutableStateOf(null) } + var repeatType: AlarmRepeatType? by remember { mutableStateOf(null) } + var isRepeatFinish: Boolean by remember { mutableStateOf(false) } + var repeatFinish: LocalDate? by remember { mutableStateOf(null) } + var time: LocalTime? by remember { mutableStateOf(null) } + var location: String by remember { mutableStateOf("") } + var link: String by remember { mutableStateOf("") } + var memo: String by remember { mutableStateOf("") } + var isGetRelationShowing: Boolean by remember { mutableStateOf(false) } + var isDatePickerShowing: Boolean by remember { mutableStateOf(false) } + var isAlarmDatePickerShowing: Boolean by remember { mutableStateOf(false) } + var isAlarmRepeatPickerShowing: Boolean by remember { mutableStateOf(false) } + var isRepeatFinishDatePickerShowing: Boolean by remember { mutableStateOf(false) } + var isTimePickerShowing: Boolean by remember { mutableStateOf(false) } + + val formattedDate = Unit.let { + val format = "%04d / %02d / %02d" + val year = date.year + val month = date.month.number + val day = date.dayOfMonth + runCatching { + String.format(format, year, month, day) + }.onFailure { exception -> + scope.launch(handler) { + throw exception + } + }.getOrDefault("???? / ?? / ??") + } + val currentEvent: HistoryEventType? = eventTypeList.find { + it.eventName == eventName + } + val formattedAlarm = Unit.let { + alarm?.let { + val alarmDay = when ( + val difference = (it.date - now).days + ) { + 0 -> "당일" + in 1..6 -> "${difference}일 전" + else -> "${difference / 7}주 전" + } + val fixedHour = if (it.time.hour == 0) 24 else it.time.hour + val alarmHour = (fixedHour + 1) % 12 - 1 + val alarmMinute = it.time.minute + val alarmAmPm = if (fixedHour < 13) "AM" else "PM" + "$alarmDay $alarmHour:$alarmMinute $alarmAmPm" + } ?: "알림 없음" + } + val formattedTime = Unit.let { + time?.let { + val fixedHour = if (it.hour == 0) 24 else it.hour + val timeHour = (fixedHour + 1) % 12 - 1 + val timeMinute = it.minute + val timeAmPm = if (fixedHour < 13) "오전" else "오후" + "$timeAmPm $timeHour:$timeMinute" + } ?: "시간 없음" + } + val formattedRepeatType = when (repeatType) { + AlarmRepeatType.Month -> "매달" + AlarmRepeatType.Year -> "매년" + null -> "반복 없음" + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Color.White) + .verticalScroll(rememberScrollState()) + ) { + Row( + modifier = Modifier + .height(56.dp) + .fillMaxWidth() + .padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + onClick = { + + } + ) + ) { + Icon( + modifier = Modifier.size(Icon24), + painter = painterResource(id = R.drawable.ic_chevron_left), + contentDescription = null + ) + } + Text( + text = "일정 추가하기", + style = Headline1 + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp) + ) { + Row { + Text( + text = "이름", + style = Body1 + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "*", + style = Body1.merge(color = Negative) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + if (relation == null) { + FieldSelectComponent( + isSelected = isGetRelationShowing, + text = "이름 선택", + onClick = { + isGetRelationShowing = true + } + ) + } else { + Card( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .clickable { + isGetRelationShowing = true + }, + backgroundColor = Primary1, + shape = Shapes.medium, + border = BorderStroke(1.dp, Primary4), + ) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = relation?.name.orEmpty(), + style = Headline3 + ) + Text( + text = "・", + style = Headline3 + ) + Text( + text = relation?.group?.name.orEmpty(), + style = Body0 + ) + Spacer(modifier = Modifier.width(6.dp)) + Icon( + modifier = Modifier.size(Icon24), + painter = painterResource(id = R.drawable.ic_chevron_right), + contentDescription = null, + tint = Gray600 + ) + } + } + } + Spacer(modifier = Modifier.height(32.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "날짜", + style = Body1 + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "*", + style = Body1.merge(color = Negative) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + FieldSelectComponent( + isSelected = isDatePickerShowing, + text = formattedDate, + onClick = { + isDatePickerShowing = true + } + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "경사 종류", + style = Body1 + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "*", + style = Body1.merge(color = Negative) + ) + } + Spacer(modifier = Modifier.height(6.dp)) + TypingTextField( + textType = TypingTextFieldType.Basic, + text = eventName, + onValueChange = { text -> + eventName = text + }, + hintText = "직접 입력" + ) + Spacer(modifier = Modifier.height(10.dp)) + LazyRow( + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + items(eventTypeList.size) { index -> + val item = eventTypeList[index] + ChipItem( + chipType = ChipType.BORDER, + currentSelectedId = currentEvent?.let { setOf(it.id) }.orEmpty(), + chipId = item.id, + chipText = item.eventName, + chipCount = 0, + onSelectChip = { id -> + eventName = eventTypeList.find { it.id == id }?.eventName.orEmpty() + } + ) + } + } + } + Spacer(modifier = Modifier.height(46.dp)) + Divider( + modifier = Modifier.height(8.dp), + color = Gray200 + ) + Spacer(modifier = Modifier.height(46.dp)) + Column( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_notification), + contentDescription = null, + tint = Gray600 + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "미리 알림", + style = Body1 + ) + } + Spacer(modifier = Modifier.height(6.dp)) + FieldSelectComponent( + isSelected = isAlarmDatePickerShowing, + text = formattedAlarm, + onClick = { + isAlarmDatePickerShowing = true + } + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_repeat), + contentDescription = null, + tint = Gray600 + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "반복", + style = Body1 + ) + } + Spacer(modifier = Modifier.height(8.dp)) + FieldSelectComponent( + isSelected = isAlarmRepeatPickerShowing, + text = formattedRepeatType, + onClick = { + isAlarmRepeatPickerShowing = true + } + ) + if (repeatType != null) { + Spacer(modifier = Modifier.height(8.dp)) + Card( + modifier = Modifier + .fillMaxWidth() + .height(43.dp), + backgroundColor = Gray200, + shape = Shapes.medium + ) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "반복 종료", + modifier = Modifier.weight(1f), + style = Body1 + ) + Switch( + checked = isRepeatFinish, + onCheckedChange = { isChecked -> + isRepeatFinish = isChecked + } + ) + } + } + } + if (isRepeatFinish) { + Spacer(modifier = Modifier.height(8.dp)) + Card( + modifier = Modifier + .fillMaxWidth() + .height(43.dp) + .clickable { + isRepeatFinishDatePickerShowing = true + }, + backgroundColor = Gray200, + shape = Shapes.medium + ) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = "종료일", + modifier = Modifier.weight(1f), + style = Body1 + ) + Text( + text = "설정하기", + style = Body1.merge(Gray700) + ) + Spacer(modifier = Modifier.width(4.dp)) + Icon( + modifier = Modifier.size(Icon24), + painter = painterResource(id = R.drawable.ic_chevron_right), + contentDescription = null, + tint = Gray600 + ) + } + } + } + Spacer(modifier = Modifier.height(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_clock), + contentDescription = null, + tint = Gray600 + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "시간", + style = Body1 + ) + } + FieldSelectComponent( + isSelected = isTimePickerShowing, + text = formattedTime, + onClick = { + isTimePickerShowing = true + } + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_location), + contentDescription = null, + tint = Gray600 + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "위치", + style = Body1 + ) + } + Spacer(modifier = Modifier.height(6.dp)) + TypingTextField( + textType = TypingTextFieldType.Basic, + text = location, + onValueChange = { text -> + location = text + }, + hintText = "위치를 입력해주세요" + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_link), + contentDescription = null, + tint = Gray600 + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "초대장 링크", + style = Body1 + ) + } + Spacer(modifier = Modifier.height(6.dp)) + TypingTextField( + textType = TypingTextFieldType.Basic, + text = link, + onValueChange = { text -> + link = text + }, + hintText = "모바일 초대장 링크를 입력해주세요" + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_memo), + contentDescription = null, + tint = Gray600 + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "메모", + style = Body1 + ) + } + Spacer(modifier = Modifier.height(6.dp)) + TypingTextField( + textType = TypingTextFieldType.LongSentence, + text = memo, + onValueChange = { text -> + memo = text + }, + hintText = "경사 관련 메모를 입력해주세요" + ) + } + Spacer(modifier = Modifier.height(46.dp)) + ConfirmButton( + properties = ConfirmButtonProperties( + size = ConfirmButtonSize.Large, + type = ConfirmButtonType.Primary + ), + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 12.dp) + .fillMaxWidth(), + onClick = { + intent(ScheduleAddIntent.OnConfirm) + } + ) { style -> + Text( + text = "저장하기", + style = style + ) + } + } + + fun navigateToAddSchedule() { + appState.navController.navigate(ScheduleAddConstant.ROUTE) + } + + fun navigateToNotification() { + appState.navController.navigate(NotificationConstant.ROUTE) } LaunchedEffectWithLifecycle(event, handler) { diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddViewModel.kt index d1c8cbe2..c5127bd7 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddViewModel.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleAddViewModel.kt @@ -1,5 +1,6 @@ package ac.dnd.bookkeeping.android.presentation.ui.main.home.schedule.add +import ac.dnd.bookkeeping.android.domain.usecase.feature.schedule.AddScheduleUseCase import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow @@ -14,6 +15,7 @@ import javax.inject.Inject @HiltViewModel class ScheduleAddViewModel @Inject constructor( private val savedStateHandle: SavedStateHandle, + private val addScheduleUseCase: AddScheduleUseCase ) : BaseViewModel() { private val _state: MutableStateFlow = MutableStateFlow(ScheduleAddState.Init) @@ -23,6 +25,12 @@ class ScheduleAddViewModel @Inject constructor( val event: EventFlow = _event.asEventFlow() fun onIntent(intent: ScheduleAddIntent) { + when (intent) { + ScheduleAddIntent.OnConfirm -> onConfirm() + } + } + + private fun onConfirm() { } } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleDestination.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleDestination.kt new file mode 100644 index 00000000..dde1fcdb --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/schedule/add/ScheduleDestination.kt @@ -0,0 +1,10 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.schedule.add + +import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import androidx.navigation.NavGraphBuilder + +fun NavGraphBuilder.scheduleDestination( + appState: ApplicationState +) { + scheduleAddDestination(appState) +} diff --git a/presentation/src/main/res/drawable/ic_clock.xml b/presentation/src/main/res/drawable/ic_clock.xml new file mode 100644 index 00000000..fe021a3f --- /dev/null +++ b/presentation/src/main/res/drawable/ic_clock.xml @@ -0,0 +1,20 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_link.xml b/presentation/src/main/res/drawable/ic_link.xml index 2f375b44..b1bda62b 100644 --- a/presentation/src/main/res/drawable/ic_link.xml +++ b/presentation/src/main/res/drawable/ic_link.xml @@ -1,9 +1,20 @@ + android:width="16dp" + android:height="17dp" + android:viewportWidth="16" + android:viewportHeight="17"> + android:fillColor="#00000000" + android:pathData="M6.667,9.167C6.953,9.55 7.318,9.866 7.738,10.096C8.157,10.325 8.621,10.461 9.098,10.495C9.574,10.529 10.053,10.46 10.501,10.293C10.949,10.126 11.355,9.865 11.693,9.527L13.693,7.527C14.3,6.898 14.636,6.056 14.629,5.182C14.621,4.308 14.271,3.472 13.653,2.854C13.035,2.236 12.198,1.886 11.325,1.878C10.45,1.87 9.609,2.206 8.98,2.814L7.833,3.954" + android:strokeWidth="1.5" + android:strokeColor="#A4A6AA" + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + diff --git a/presentation/src/main/res/drawable/ic_location.xml b/presentation/src/main/res/drawable/ic_location.xml new file mode 100644 index 00000000..620bcce4 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_location.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/presentation/src/main/res/drawable/ic_memo.xml b/presentation/src/main/res/drawable/ic_memo.xml new file mode 100644 index 00000000..b60371a2 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_memo.xml @@ -0,0 +1,20 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_repeat.xml b/presentation/src/main/res/drawable/ic_repeat.xml new file mode 100644 index 00000000..f2be59ce --- /dev/null +++ b/presentation/src/main/res/drawable/ic_repeat.xml @@ -0,0 +1,37 @@ + + + + + + + + +