diff --git a/composeApp/src/commonMain/kotlin/team/aliens/dms/kmp/di/FeatureModule.kt b/composeApp/src/commonMain/kotlin/team/aliens/dms/kmp/di/FeatureModule.kt index bf5c57f..a710094 100644 --- a/composeApp/src/commonMain/kotlin/team/aliens/dms/kmp/di/FeatureModule.kt +++ b/composeApp/src/commonMain/kotlin/team/aliens/dms/kmp/di/FeatureModule.kt @@ -1,6 +1,7 @@ package team.aliens.dms.kmp.di import org.koin.dsl.module +import team.aliens.dms.kmp.feature.home.di.homeModule import team.aliens.dms.kmp.feature.signin.di.signInModule import team.aliens.dms.kmp.feature.signup.di.signUpModule import team.aliens.dms.kmp.feature.splash.di.splashModule @@ -10,5 +11,6 @@ internal val featureModule = module { splashModule, signInModule, signUpModule, + homeModule, ) } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 30afef0..06fc726 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -39,6 +39,7 @@ kotlin { implementation(libs.lifecycle.viewmodel.compose) implementation(compose.runtime) implementation(compose.foundation) + implementation(libs.kotlinx.datetime) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/core/common/src/commonMain/kotlin/team/aliens/dms/kmp/core/common/utils/Date.kt b/core/common/src/commonMain/kotlin/team/aliens/dms/kmp/core/common/utils/Date.kt new file mode 100644 index 0000000..2384d47 --- /dev/null +++ b/core/common/src/commonMain/kotlin/team/aliens/dms/kmp/core/common/utils/Date.kt @@ -0,0 +1,17 @@ +package team.aliens.dms.kmp.core.common.utils + +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +val today: LocalDate + inline get() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + +val now: LocalDateTime + inline get() = Clock.System.now().toLocalDateTime(TimeZone.UTC) + +val timeNow: LocalTime + inline get() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).time diff --git a/core/design-system/build.gradle.kts b/core/design-system/build.gradle.kts index d49a03c..1400c98 100644 --- a/core/design-system/build.gradle.kts +++ b/core/design-system/build.gradle.kts @@ -48,6 +48,7 @@ kotlin { implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.kottie) + implementation(libs.kotlinx.datetime) implementation(projects.core.common) } diff --git a/core/design-system/src/commonMain/composeResources/drawable/ic_backward.xml b/core/design-system/src/commonMain/composeResources/drawable/ic_backward.xml new file mode 100644 index 0000000..b98fa3d --- /dev/null +++ b/core/design-system/src/commonMain/composeResources/drawable/ic_backward.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/design-system/src/commonMain/composeResources/drawable/ic_forward.xml b/core/design-system/src/commonMain/composeResources/drawable/ic_forward.xml new file mode 100644 index 0000000..a2ae395 --- /dev/null +++ b/core/design-system/src/commonMain/composeResources/drawable/ic_forward.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/design-system/src/commonMain/composeResources/drawable/ic_notification.xml b/core/design-system/src/commonMain/composeResources/drawable/ic_notification.xml new file mode 100644 index 0000000..afe8d30 --- /dev/null +++ b/core/design-system/src/commonMain/composeResources/drawable/ic_notification.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/appbar/DmsTopAppBar.kt b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/appbar/DmsTopAppBar.kt index f5fc2db..f41c9a3 100644 --- a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/appbar/DmsTopAppBar.kt +++ b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/appbar/DmsTopAppBar.kt @@ -1,16 +1,20 @@ package team.aliens.dms.kmp.core.designsystem.appbar +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme 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.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import org.jetbrains.compose.resources.painterResource import team.aliens.dms.kmp.core.designsystem.button.DmsIconButton import team.aliens.dms.kmp.core.designsystem.foundation.DmsIcon import team.aliens.dms.kmp.core.designsystem.foundation.DmsTheme @@ -20,8 +24,10 @@ import team.aliens.dms.kmp.core.designsystem.text.DmsText @Composable fun DmsTopAppBar( modifier: Modifier = Modifier, + showLogo: Boolean = false, onBackPressed: (() -> Unit)? = null, - title: String, + actions: (@Composable RowScope.() -> Unit)? = null, + title: String? = null, ) { Box( modifier = modifier @@ -33,8 +39,21 @@ fun DmsTopAppBar( Row( modifier = Modifier .fillMaxWidth(), + horizontalArrangement = if (showLogo || onBackPressed != null) Arrangement.SpaceBetween else Arrangement.End, verticalAlignment = Alignment.CenterVertically, ) { + if (showLogo) { + Image( + painter = painterResource( + if (isSystemInDarkTheme()) { + DmsIcon.SymbolDark + } else { + DmsIcon.SymbolLight + }, + ), + contentDescription = null, + ) + } onBackPressed?.let { DmsIconButton( resource = DmsIcon.ArrowBack, @@ -42,11 +61,20 @@ fun DmsTopAppBar( onClick = it, ) } + actions?.let { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + content = it, + ) + } + } + title?.let { + DmsText( + text = it, + style = DmsTypography.SubtitleSemiBold, + ) } - DmsText( - text = title, - style = DmsTypography.SubtitleSemiBold, - ) } } diff --git a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/calendar/DmsCalendar.kt b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/calendar/DmsCalendar.kt new file mode 100644 index 0000000..dda7810 --- /dev/null +++ b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/calendar/DmsCalendar.kt @@ -0,0 +1,14 @@ +package team.aliens.dms.kmp.core.designsystem.calendar + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import kotlinx.datetime.LocalDate + +@Composable +fun DmsCalendar( + modifier: Modifier = Modifier, + selectDate: LocalDate, + onSelectedDateChange: (newDate: LocalDate) -> Unit, +) { + // TODO: Calender 커스텀 하기 +} diff --git a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/foundation/DmsIcon.kt b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/foundation/DmsIcon.kt index 2f00ee8..9ca9c15 100644 --- a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/foundation/DmsIcon.kt +++ b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/foundation/DmsIcon.kt @@ -5,14 +5,17 @@ import dmskmp.core.design_system.generated.resources.ic_add_notes import dmskmp.core.design_system.generated.resources.ic_add_notes_fill import dmskmp.core.design_system.generated.resources.ic_alarm import dmskmp.core.design_system.generated.resources.ic_arrow_back +import dmskmp.core.design_system.generated.resources.ic_backward import dmskmp.core.design_system.generated.resources.ic_breaking_news import dmskmp.core.design_system.generated.resources.ic_breaking_news_fill import dmskmp.core.design_system.generated.resources.ic_cancel import dmskmp.core.design_system.generated.resources.ic_check import dmskmp.core.design_system.generated.resources.ic_delete import dmskmp.core.design_system.generated.resources.ic_edit +import dmskmp.core.design_system.generated.resources.ic_forward import dmskmp.core.design_system.generated.resources.ic_home import dmskmp.core.design_system.generated.resources.ic_home_fill +import dmskmp.core.design_system.generated.resources.ic_notification import dmskmp.core.design_system.generated.resources.ic_person import dmskmp.core.design_system.generated.resources.ic_person_fill import dmskmp.core.design_system.generated.resources.ic_refresh @@ -41,4 +44,7 @@ object DmsIcon { val AddNotesFill = Res.drawable.ic_add_notes_fill val BreakingNewsFill = Res.drawable.ic_breaking_news_fill val PersonFill = Res.drawable.ic_person_fill + val Notification = Res.drawable.ic_notification + val Backward = Res.drawable.ic_backward + val Forward = Res.drawable.ic_forward } diff --git a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/text/DmsText.kt b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/text/DmsText.kt index ee990b3..98bc114 100644 --- a/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/text/DmsText.kt +++ b/core/design-system/src/commonMain/kotlin/team/aliens/dms/kmp/core/designsystem/text/DmsText.kt @@ -20,6 +20,7 @@ fun DmsText( maxLines: Int = Int.MAX_VALUE, textAlign: TextAlign? = null, textDecoration: TextDecoration = TextDecoration.None, + softWrap: Boolean = true, ) { Text( modifier = modifier, @@ -30,5 +31,6 @@ fun DmsText( maxLines = maxLines, textAlign = textAlign, textDecoration = textDecoration, + softWrap = softWrap, ) } diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index dcf8995..41dd110 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -41,7 +41,16 @@ kotlin { implementation(compose.material) implementation(compose.material3) implementation(compose.ui) + implementation(compose.components.resources) implementation(libs.navigation.compose) + implementation(libs.kotlinx.datetime) + + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.compose.viewmodel) + + implementation(projects.core.designSystem) + implementation(projects.core.common) } commonTest.dependencies { implementation(libs.kotlin.test) diff --git a/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/di/HomeModule.kt b/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/di/HomeModule.kt new file mode 100644 index 0000000..0c9749d --- /dev/null +++ b/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/di/HomeModule.kt @@ -0,0 +1,9 @@ +package team.aliens.dms.kmp.feature.home.di + +import org.koin.core.module.dsl.viewModelOf +import org.koin.dsl.module +import team.aliens.dms.kmp.feature.home.viewmodel.HomeViewModel + +val homeModule = module { + viewModelOf(::HomeViewModel) +} diff --git a/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/ui/HomeScreen.kt b/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/ui/HomeScreen.kt index 39780d3..fa57010 100644 --- a/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/ui/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/ui/HomeScreen.kt @@ -1,14 +1,257 @@ package team.aliens.dms.kmp.feature.home.ui -import androidx.compose.material3.Text +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +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.draw.clip +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import org.koin.compose.koinInject +import team.aliens.dms.kmp.core.designsystem.appbar.DmsTopAppBar +import team.aliens.dms.kmp.core.designsystem.button.DmsIconButton +import team.aliens.dms.kmp.core.designsystem.foundation.DmsIcon +import team.aliens.dms.kmp.core.designsystem.foundation.DmsTheme +import team.aliens.dms.kmp.core.designsystem.foundation.DmsTypography +import team.aliens.dms.kmp.core.designsystem.text.DmsText +import team.aliens.dms.kmp.feature.home.viewmodel.HomeState +import team.aliens.dms.kmp.feature.home.viewmodel.HomeViewModel +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun Home() { - HomeScreen() + val viewModel: HomeViewModel = koinInject() + val state by viewModel.state.collectAsState() + + val (shouldShowCalendar, onShouldShowCalendarChange) = remember { mutableStateOf(false) } + + if (shouldShowCalendar) { + ModalBottomSheet( + onDismissRequest = { + onShouldShowCalendarChange(false) + // onChangeBottomAppBarVisibility(true) + }, + ) { +// DmsCalendar( +// modifier = Modifier.fillMaxWidth(), +// selectedDate = uiState.selectedDate, +// onSelectedDateChange = onSelectedDateChange, +// ) + } + } + HomeScreen( + state = state, + onDateChange = viewModel::updateDate, + ) +} + +@Composable +private fun HomeScreen( + state: HomeState, + onDateChange: (LocalDate) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .background(DmsTheme.colors.background), + ) { + DmsTopAppBar( + showLogo = true, + actions = { + DmsIconButton( + resource = DmsIcon.Notification, + tint = DmsTheme.colors.surfaceContainerLow, + onClick = { }, + ) + }, + ) + MealCards( + modifier = Modifier.padding(top = 16.dp), + onNextDay = { onDateChange(state.selectedDate.plus(DatePeriod(days = 1))) }, + onPreviousDay = { onDateChange(state.selectedDate.minus(DatePeriod(days = 1))) }, + selectDate = state.selectedDate, + ) + } } @Composable -private fun HomeScreen() { - Text("홈") +private fun DateCard( + modifier: Modifier = Modifier, + onNextDay: () -> Unit, + onPreviousDay: () -> Unit, + selectDate: LocalDate, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding( + horizontal = 36.dp, + vertical = 14.dp, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + DmsIconButton( + resource = DmsIcon.Backward, + tint = DmsTheme.colors.surfaceContainerLow, + size = 18.dp, + onClick = onPreviousDay, + ) + DmsText( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .border( + width = 1.dp, + color = DmsTheme.colors.onSurface, + shape = RoundedCornerShape(8.dp), + ) + .clickable( + onClick = { }, + ) + .padding( + horizontal = 14.dp, + vertical = 8.dp, + ), + text = "${selectDate.monthNumber}월 ${selectDate.dayOfMonth}일 ${selectDate.dayOfWeek.text}요일", + color = DmsTheme.colors.surfaceContainerLow, + style = DmsTypography.Body1SemiBold, + ) + DmsIconButton( + resource = DmsIcon.Forward, + tint = DmsTheme.colors.surfaceContainerLow, + size = 18.dp, + onClick = onNextDay, + ) + } } + +@Composable +private fun MealCards( + modifier: Modifier = Modifier, + onNextDay: () -> Unit, + onPreviousDay: () -> Unit, + selectDate: LocalDate, +) { + val pageCount = 5 + val pagerState = rememberPagerState(pageCount = { pageCount }) + var previousPage by remember { mutableStateOf(0) } + val scope = rememberCoroutineScope() + + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage > previousPage) onNextDay() + if (pagerState.currentPage < previousPage) onPreviousDay() + + previousPage = pagerState.currentPage + } + + DateCard( + onNextDay = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + onPreviousDay = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + }, + selectDate = selectDate, + ) + + HorizontalPager( + modifier = Modifier.padding(top = 24.dp), + state = pagerState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + val meal: List = listOf("우동국물", "케이준치킨샐러드", "소불고기주먹밥", "배추김치", "해가득사과주스") + MealCard( + meal = meal, + ) + MealCard( + meal = meal, + ) + MealCard( + meal = meal, + ) + } + } +} + +@Composable +private fun MealCard( + modifier: Modifier = Modifier, + meal: List, +) { + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(DmsTheme.colors.onBackground) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + val formatMeal = meal.chunked(3).joinToString("\n") { it.joinToString(", ") } + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + DmsText( + text = "아침", + style = DmsTypography.HeadlineSemiBold, + ) + DmsText( + text = "430Kal", + color = DmsTheme.colors.onSurface, + style = DmsTypography.Body1Medium, + ) + } + DmsText( + text = formatMeal, + style = DmsTypography.Body1Medium, + ) + } +} + +private val DayOfWeek.text: String + @Composable inline get() = when (this) { + DayOfWeek.SUNDAY -> "일" + DayOfWeek.MONDAY -> "월" + DayOfWeek.TUESDAY -> "화" + DayOfWeek.WEDNESDAY -> "수" + DayOfWeek.THURSDAY -> "목" + DayOfWeek.FRIDAY -> "금" + DayOfWeek.SATURDAY -> "토" + else -> throw IllegalArgumentException() + } diff --git a/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/viewmodel/HomeViewModel.kt b/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/viewmodel/HomeViewModel.kt new file mode 100644 index 0000000..e26fdf0 --- /dev/null +++ b/feature/home/src/commonMain/kotlin/team/aliens/dms/kmp/feature/home/viewmodel/HomeViewModel.kt @@ -0,0 +1,27 @@ +package team.aliens.dms.kmp.feature.home.viewmodel + +import kotlinx.datetime.LocalDate +import team.aliens.dms.kmp.core.common.base.BaseViewModel +import team.aliens.dms.kmp.core.common.utils.today + +internal class HomeViewModel : + BaseViewModel(HomeState.getDefaultState()) { + + internal fun updateDate(date: LocalDate) { + setState { state.value.copy(selectedDate = date) } + } +} + +data class HomeState( + val newNoticesExist: Boolean, + val selectedDate: LocalDate, +) { + companion object { + fun getDefaultState() = HomeState( + newNoticesExist = false, + selectedDate = today, + ) + } +} + +sealed interface HomeSideEffect diff --git a/feature/signin/src/commonMain/kotlin/team/aliens/dms/kmp/feature/signin/viewmodel/SignInViewModel.kt b/feature/signin/src/commonMain/kotlin/team/aliens/dms/kmp/feature/signin/viewmodel/SignInViewModel.kt index f91315b..a682162 100644 --- a/feature/signin/src/commonMain/kotlin/team/aliens/dms/kmp/feature/signin/viewmodel/SignInViewModel.kt +++ b/feature/signin/src/commonMain/kotlin/team/aliens/dms/kmp/feature/signin/viewmodel/SignInViewModel.kt @@ -2,7 +2,7 @@ package team.aliens.dms.kmp.feature.signin.viewmodel import team.aliens.dms.kmp.core.common.base.BaseViewModel -internal class SignInViewModel() : BaseViewModel(SignInState.getDefaultState()) { +internal class SignInViewModel : BaseViewModel(SignInState.getDefaultState()) { internal fun setAccountId(accountId: String) { setState {