diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/SusuProgressAppBar.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/SusuProgressAppBar.kt index 458407c0..677f0734 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/SusuProgressAppBar.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/SusuProgressAppBar.kt @@ -1,13 +1,9 @@ package com.susu.core.designsystem.component.appbar -import android.util.Log -import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Button -import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,41 +12,22 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.susu.core.designsystem.R +import com.susu.core.designsystem.component.appbar.icon.BackIcon import com.susu.core.designsystem.theme.SusuTheme -import com.susu.core.ui.extension.susuClickable @Composable fun SusuProgressAppBar( modifier: Modifier = Modifier, - @DrawableRes leftIcon: Int? = null, - leftIconContentDescription: String? = null, - leftIconPadding: Dp = SusuTheme.spacing.spacing_xs, + leftIcon: @Composable () -> Unit = {}, currentStep: Int, entireStep: Int, progressBar: ProgressBarStyle = ProgressBarStyle.SusuProgressBar, - onClickBackButton: () -> Unit, ) { BasicAppBar( modifier = modifier, - leftIcon = { - leftIcon?.let { - Icon( - painter = painterResource(id = leftIcon), - contentDescription = leftIconContentDescription, - modifier = Modifier - .susuClickable( - rippleEnabled = true, - onClick = onClickBackButton, - ) - .padding(leftIconPadding), - ) - } - }, + leftIcon = leftIcon, title = { LinearProgressIndicator( progress = { currentStep / entireStep.toFloat() }, @@ -72,19 +49,16 @@ fun SusuProgressAppBar( fun SusuProgressAppBarPreview() { val entireStep = 6 var currentStep by remember { mutableStateOf(1) } - SusuTheme { Column( verticalArrangement = Arrangement.spacedBy(20.dp), ) { SusuProgressAppBar( - leftIcon = R.drawable.ic_arrow_left, - leftIconContentDescription = "뒤로가기", + leftIcon = { + BackIcon() + }, currentStep = currentStep, entireStep = entireStep, - onClickBackButton = { - Log.d("확인", "왼쪽 뒤로가기 클릭") - }, ) Button( onClick = { diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/button/RefreshButton.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/button/RefreshButton.kt new file mode 100644 index 00000000..84d0d27f --- /dev/null +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/button/RefreshButton.kt @@ -0,0 +1,47 @@ +package com.susu.core.designsystem.component.button + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.R +import com.susu.core.designsystem.theme.Gray30 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.susuClickable + +@Composable +fun RefreshButton( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Icon( + modifier = modifier + .border( + width = 1.dp, + color = Gray30, + shape = RoundedCornerShape(size = 100.dp), + ) + .size(44.dp) + .clip(RoundedCornerShape(size = 100.dp)) + .susuClickable(onClick = onClick) + .padding(SusuTheme.spacing.spacing_xs), + painter = painterResource(id = R.drawable.ic_refresh), + contentDescription = stringResource(R.string.content_description_refresh), + ) +} + +@Preview(showBackground = true) +@Composable +fun RefreshButtonPreview() { + SusuTheme { + RefreshButton() + } +} diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/container/SusuRecentSearchContainer.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/container/SusuRecentSearchContainer.kt index 7b92c67a..ccdca079 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/container/SusuRecentSearchContainer.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/container/SusuRecentSearchContainer.kt @@ -59,7 +59,7 @@ fun SusuRecentSearchContainer( .clip(CircleShape) .susuClickable(onClick = onClickCloseIcon), painter = painterResource(id = R.drawable.ic_recent_search_close), - contentDescription = stringResource(R.string.content_description_close_icon), + contentDescription = stringResource(com.susu.core.ui.R.string.content_description_close_icon), ) } } diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt index a2286699..a3ba9f6b 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt @@ -39,6 +39,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.susu.core.designsystem.R import com.susu.core.designsystem.component.button.ClearIconButton +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.LargeButtonStyle +import com.susu.core.designsystem.component.button.SusuFilledButton import com.susu.core.designsystem.component.textfieldbutton.style.InnerButtonStyle import com.susu.core.designsystem.component.textfieldbutton.style.LargeTextFieldButtonStyle import com.susu.core.designsystem.component.textfieldbutton.style.SmallTextFieldButtonStyle @@ -60,6 +63,7 @@ fun SusuTextFieldFillMaxButton( style: @Composable () -> TextFieldButtonStyle, color: TextFieldButtonColor = TextFieldButtonColor.Black, isSaved: Boolean = false, + isFocused: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -67,23 +71,25 @@ fun SusuTextFieldFillMaxButton( showClearIcon: Boolean = true, onClickClearIcon: () -> Unit = {}, onClickCloseIcon: () -> Unit = {}, - onClickButton: () -> Unit = {}, + onClickFilledButton: () -> Unit = {}, + onClickButton: (isFocused: Boolean) -> Unit = {}, ) { val (backgroundColor, textColor) = with(color) { - if (isSaved) { - (savedBackgroundColor to savedTextColor) - } else { - (editBackgroundColor to editTextColor) + when { + isFocused.not() -> (unFocusedBackgroundColor to unFocusedTextColor) + isSaved -> (savedBackgroundColor to savedTextColor) + else -> (editBackgroundColor to editTextColor) } } with(style()) { BasicTextField( modifier = modifier - .fillMaxWidth(), + .fillMaxWidth() + .susuClickable { onClickButton(isFocused) }, value = text, onValueChange = onTextChange, - enabled = isSaved.not(), + enabled = isSaved.not() && isFocused, singleLine = maxLines == 1, maxLines = if (minLines > maxLines) minLines else maxLines, minLines = minLines, @@ -142,13 +148,14 @@ fun SusuTextFieldFillMaxButton( showClearIcon = showClearIcon, isSaved = isSaved, isActive = text.isNotEmpty(), + isFocused = isFocused, color = color.buttonColor, buttonStyle = innerButtonStyle, clearIconSize = clearIconSize, closeIconSize = closeIconSize, onClickClearIcon = onClickClearIcon, onClickCloseIcon = onClickCloseIcon, - onClickButton = onClickButton, + onClickFilledButton = onClickFilledButton, ) } }, @@ -169,6 +176,7 @@ fun SusuTextFieldWrapContentButton( style: @Composable () -> TextFieldButtonStyle, color: TextFieldButtonColor = TextFieldButtonColor.Black, isSaved: Boolean = false, + isFocused: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, @@ -176,13 +184,14 @@ fun SusuTextFieldWrapContentButton( showClearIcon: Boolean = true, onClickClearIcon: () -> Unit = {}, onClickCloseIcon: () -> Unit = {}, - onClickButton: () -> Unit = {}, + onClickFilledButton: () -> Unit = {}, + onClickButton: (isFocused: Boolean) -> Unit = {}, ) { val (backgroundColor, textColor) = with(color) { - if (isSaved) { - (savedBackgroundColor to savedTextColor) - } else { - (editBackgroundColor to editTextColor) + when { + isFocused.not() -> (unFocusedBackgroundColor to unFocusedTextColor) + isSaved -> (savedBackgroundColor to savedTextColor) + else -> (editBackgroundColor to editTextColor) } } @@ -191,6 +200,7 @@ fun SusuTextFieldWrapContentButton( modifier = modifier .clip(shape) .background(backgroundColor) + .susuClickable { onClickButton(isFocused) } .padding(paddingValues), horizontalArrangement = Arrangement.spacedBy(iconSpacing), verticalAlignment = Alignment.CenterVertically, @@ -206,9 +216,10 @@ fun SusuTextFieldWrapContentButton( * BasicTextField의 기본 width를 없애기 위해 IntrinsicSize.Min을 사용함. * see -> https://stackoverflow.com/questions/67719981/resizeable-basictextfield-in-jetpack-compose */ - .width(IntrinsicSize.Min), + .width(IntrinsicSize.Min) + .susuClickable(rippleEnabled = false, runIf = isFocused.not(), onClick = { onClickButton(isFocused) }), value = text, - enabled = isSaved.not(), + enabled = isSaved.not() && isFocused, onValueChange = onTextChange, singleLine = maxLines == 1, maxLines = if (minLines > maxLines) minLines else maxLines, @@ -256,13 +267,14 @@ fun SusuTextFieldWrapContentButton( showClearIcon = showClearIcon, isSaved = isSaved, isActive = text.isNotEmpty(), + isFocused = isFocused, color = color.buttonColor, buttonStyle = innerButtonStyle, clearIconSize = clearIconSize, closeIconSize = closeIconSize, onClickClearIcon = onClickClearIcon, onClickCloseIcon = onClickCloseIcon, - onClickButton = onClickButton, + onClickFilledButton = onClickFilledButton, ) } } @@ -274,6 +286,7 @@ private fun InnerButtons( shape: Shape = RoundedCornerShape(4.dp), isSaved: Boolean, isActive: Boolean, + isFocused: Boolean, showCloseIcon: Boolean = true, showClearIcon: Boolean = true, color: TextButtonInnerButtonColor, @@ -282,13 +295,13 @@ private fun InnerButtons( buttonStyle: @Composable () -> InnerButtonStyle, onClickClearIcon: () -> Unit = {}, onClickCloseIcon: () -> Unit = {}, - onClickButton: () -> Unit = {}, + onClickFilledButton: () -> Unit = {}, ) { val (innerButtonTextColor, innerButtonBackgroundColor) = with(color) { - if (isActive || isSaved) { - (activeContentColor to activeBackgroundColor) - } else { - (inactiveContentColor to inactiveBackgroundColor) + when { + isFocused.not() -> (unFocusedContentColor to unFocusedBackgroundColor) + isActive || isSaved -> (activeContentColor to activeBackgroundColor) + else -> (inactiveContentColor to inactiveBackgroundColor) } } @@ -315,7 +328,7 @@ private fun InnerButtons( .susuClickable( runIf = isActive || isSaved, rippleColor = color.rippleColor, - onClick = onClickButton, + onClick = onClickFilledButton, ) .padding(paddingValues), ) { @@ -333,7 +346,7 @@ private fun InnerButtons( .clip(CircleShape) .size(closeIconSize) .susuClickable(onClick = onClickCloseIcon), - painter = painterResource(id = R.drawable.ic_close), + painter = painterResource(id = com.susu.core.ui.R.drawable.ic_close), contentDescription = "", ) } @@ -351,6 +364,10 @@ fun TextFieldButtonPreview() { mutableStateOf(false) } + var isFocused by remember { + mutableStateOf(true) + } + Column( verticalArrangement = Arrangement.spacedBy(10.dp), ) { @@ -362,8 +379,10 @@ fun TextFieldButtonPreview() { maxLines = 1, minLines = 1, showClearIcon = true, + isFocused = isFocused, + onClickButton = { isFocused = !isFocused }, style = LargeTextFieldButtonStyle.height46, - onClickButton = { isSaved = isSaved.not() }, + onClickFilledButton = { isSaved = isSaved.not() }, onClickClearIcon = { text = "" }, isSaved = isSaved, ) @@ -377,8 +396,10 @@ fun TextFieldButtonPreview() { placeholder = "Button", maxLines = 1, minLines = 1, + isFocused = !isFocused, + onClickButton = { isFocused = !isFocused }, style = LargeTextFieldButtonStyle.height46, - onClickButton = { isSaved = isSaved.not() }, + onClickFilledButton = { isSaved = isSaved.not() }, onClickClearIcon = { text = "" }, isSaved = isSaved, ) @@ -392,11 +413,13 @@ fun TextFieldButtonPreview() { placeholder = "Button", maxLines = 1, minLines = 1, + isFocused = !isFocused, + onClickButton = { isFocused = !isFocused }, showClearIcon = false, showCloseIcon = false, color = TextFieldButtonColor.Orange, style = LargeTextFieldButtonStyle.height46, - onClickButton = { isSaved = isSaved.not() }, + onClickFilledButton = { isSaved = isSaved.not() }, onClickClearIcon = { text = "" }, isSaved = isSaved, ) @@ -410,14 +433,91 @@ fun TextFieldButtonPreview() { overflow = TextOverflow.Ellipsis, onTextChange = { text = it }, placeholder = "Button", + isFocused = !isFocused, + onClickButton = { isFocused = !isFocused }, maxLines = 1, minLines = 1, color = TextFieldButtonColor.Orange, style = SmallTextFieldButtonStyle.height32, - onClickButton = { isSaved = isSaved.not() }, + onClickFilledButton = { isSaved = isSaved.not() }, onClickClearIcon = { text = "" }, isSaved = isSaved, ) } } } + +@Preview(showBackground = true, backgroundColor = 0x000000) +@Composable +fun TextFieldButtonFocusedPreview() { + SusuTheme { + var text by remember { + mutableStateOf("Button") + } + + var isSaved by remember { + mutableStateOf(false) + } + + var isFocusedTextFieldButton by remember { + mutableStateOf(true) + } + + var isFocusedButton1 by remember { + mutableStateOf(false) + } + + var isFocusedButton2 by remember { + mutableStateOf(false) + } + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + Text(text = "텍스트 길이에 딱 맞는 너비 (wrap)") + SusuTextFieldWrapContentButton( + color = TextFieldButtonColor.Orange, + text = text, + onTextChange = { text = it }, + placeholder = "", + maxLines = 1, + minLines = 1, + showClearIcon = true, + isFocused = isFocusedTextFieldButton, + onClickButton = { + isFocusedTextFieldButton = true + isFocusedButton1 = false + isFocusedButton2 = false + }, + style = LargeTextFieldButtonStyle.height46, + onClickFilledButton = { isSaved = isSaved.not() }, + onClickClearIcon = { text = "" }, + isSaved = isSaved, + ) + + SusuFilledButton( + color = FilledButtonColor.Orange, + style = LargeButtonStyle.height46, + text = "테스트1", + isActive = isFocusedButton1, + onClick = { + isFocusedTextFieldButton = false + isFocusedButton1 = true + isFocusedButton2 = false + }, + ) + + SusuFilledButton( + color = FilledButtonColor.Orange, + style = LargeButtonStyle.height46, + text = "테스트2", + isActive = isFocusedButton2, + onClick = { + isFocusedTextFieldButton = false + isFocusedButton1 = false + isFocusedButton2 = true + }, + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/TextFieldButtonColor.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/TextFieldButtonColor.kt index 0426a23f..81a2f245 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/TextFieldButtonColor.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/TextFieldButtonColor.kt @@ -4,12 +4,16 @@ import androidx.compose.ui.graphics.Color import com.susu.core.designsystem.theme.Gray10 import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray30 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.Orange20 import com.susu.core.designsystem.theme.Orange60 enum class TextFieldButtonColor( val buttonColor: TextButtonInnerButtonColor, val savedBackgroundColor: Color, val editBackgroundColor: Color, + val unFocusedBackgroundColor: Color, + val unFocusedTextColor: Color, val editTextColor: Color, val savedTextColor: Color, val placeholderColor: Color, @@ -18,32 +22,40 @@ enum class TextFieldButtonColor( buttonColor = TextButtonInnerButtonColor.Black, savedBackgroundColor = Gray10, editBackgroundColor = Gray10, + unFocusedBackgroundColor = Gray10, editTextColor = Gray100, savedTextColor = Gray100, placeholderColor = Gray30, + unFocusedTextColor = Gray30, ), Orange( buttonColor = TextButtonInnerButtonColor.Black, savedBackgroundColor = Orange60, editBackgroundColor = Gray10, + unFocusedBackgroundColor = Orange20, editTextColor = Gray100, savedTextColor = Gray10, placeholderColor = Gray30, + unFocusedTextColor = Gray10, ), } enum class TextButtonInnerButtonColor( val activeContentColor: Color, val inactiveContentColor: Color, + val unFocusedContentColor: Color, val activeBackgroundColor: Color, val inactiveBackgroundColor: Color, + val unFocusedBackgroundColor: Color, val rippleColor: Color, ) { Black( activeContentColor = Gray10, inactiveContentColor = Gray10, + unFocusedContentColor = Gray10, activeBackgroundColor = Gray100, - inactiveBackgroundColor = Gray30, + inactiveBackgroundColor = Gray40, + unFocusedBackgroundColor = Gray40, rippleColor = Gray10, ), } diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/theme/Spacing.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/theme/Spacing.kt index 0b09b85f..a94dc32b 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/theme/Spacing.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/theme/Spacing.kt @@ -19,6 +19,7 @@ internal val Spacing = SusuSpacing( spacing_xxl = 32.dp, spacing_xxxl = 36.dp, spacing_xxxxl = 40.dp, + spacing_xxxxxxl = 48.dp, ) @Suppress("ConstructorParameterNaming") @@ -37,6 +38,7 @@ data class SusuSpacing( val spacing_xxl: Dp, val spacing_xxxl: Dp, val spacing_xxxxl: Dp, + val spacing_xxxxxxl: Dp, ) val LocalSpacing = staticCompositionLocalOf { @@ -54,5 +56,6 @@ val LocalSpacing = staticCompositionLocalOf { spacing_xxl = Dp.Unspecified, spacing_xxxl = Dp.Unspecified, spacing_xxxxl = Dp.Unspecified, + spacing_xxxxxxl = Dp.Unspecified, ) } diff --git a/core/designsystem/src/main/res/drawable/ic_refresh.xml b/core/designsystem/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..01ba8587 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 863071c0..e1025876 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -8,8 +8,8 @@ %s원 검색 아이콘 - 닫기 아이콘 뒤로가기 아이콘 로고 이미지 알림 아이콘 + 새로고침 diff --git a/core/model/src/main/java/com/susu/core/model/Term.kt b/core/model/src/main/java/com/susu/core/model/Term.kt new file mode 100644 index 00000000..fee956dd --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/Term.kt @@ -0,0 +1,7 @@ +package com.susu.core.model + +data class Term( + val id: Int, + val title: String, + val isEssential: Boolean, +) diff --git a/core/model/src/main/java/com/susu/core/model/TermDetail.kt b/core/model/src/main/java/com/susu/core/model/TermDetail.kt new file mode 100644 index 00000000..d2b9676e --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/TermDetail.kt @@ -0,0 +1,8 @@ +package com.susu.core.model + +data class TermDetail( + val id: Int, + val title: String, + val isEssential: Boolean, + val description: String, +) diff --git a/core/model/src/main/java/com/susu/core/model/User.kt b/core/model/src/main/java/com/susu/core/model/User.kt index 30d34830..750b8e03 100644 --- a/core/model/src/main/java/com/susu/core/model/User.kt +++ b/core/model/src/main/java/com/susu/core/model/User.kt @@ -3,5 +3,6 @@ package com.susu.core.model data class User( val name: String, val gender: String, + val termAgreement: List, val birth: Int, ) diff --git a/core/ui/src/main/java/com/susu/core/ui/Consts.kt b/core/ui/src/main/java/com/susu/core/ui/Consts.kt index 75699cfb..a8b83539 100644 --- a/core/ui/src/main/java/com/susu/core/ui/Consts.kt +++ b/core/ui/src/main/java/com/susu/core/ui/Consts.kt @@ -11,3 +11,6 @@ val alignList stringResource(id = R.string.word_align_high_amount), stringResource(id = R.string.word_align_low_amount), ) + +const val USER_NAME_MAX_LENGTH = 10 +val nameRegex = Regex("[a-zA-Z가-힣]{0,10}") diff --git a/core/ui/src/main/java/com/susu/core/ui/extension/AnimatedContentTransitionScope.kt b/core/ui/src/main/java/com/susu/core/ui/extension/AnimatedContentTransitionScope.kt new file mode 100644 index 00000000..5705a09a --- /dev/null +++ b/core/ui/src/main/java/com/susu/core/ui/extension/AnimatedContentTransitionScope.kt @@ -0,0 +1,21 @@ +package com.susu.core.ui.extension + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith + +fun AnimatedContentTransitionScope.susuDefaultAnimatedContentTransitionSpec(leftDirectionCondition: Boolean): ContentTransform { + val direction = if (leftDirectionCondition) { + AnimatedContentTransitionScope.SlideDirection.Left + } else { + AnimatedContentTransitionScope.SlideDirection.Right + } + return slideIntoContainer( + towards = direction, + animationSpec = tween(500), + ) togetherWith slideOutOfContainer( + towards = direction, + animationSpec = tween(500), + ) +} diff --git a/core/ui/src/main/java/com/susu/core/ui/util/Date.kt b/core/ui/src/main/java/com/susu/core/ui/util/Date.kt new file mode 100644 index 00000000..7a4b2ce2 --- /dev/null +++ b/core/ui/src/main/java/com/susu/core/ui/util/Date.kt @@ -0,0 +1,12 @@ +package com.susu.core.ui.util + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +val currentDate = LocalDateTime.now() + +@Suppress("detekt:FunctionNaming") +fun LocalDateTime.to_yyyy_dot_MM_dot_dd(): String { + val formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd") + return this.format(formatter) +} diff --git a/core/designsystem/src/main/res/drawable/ic_close.xml b/core/ui/src/main/res/drawable/ic_close.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/ic_close.xml rename to core/ui/src/main/res/drawable/ic_close.xml diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 2d6e4333..19e91fa6 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -18,4 +18,12 @@ 카테고리 날짜 더하기 버튼 + 닫기 아이콘 + 직접 입력 + 남성 + 여성 + 출생년도 + 다음 + 완료 + 필터 적용하기 diff --git a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt index 738c59fe..a9b67d77 100644 --- a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt @@ -3,10 +3,12 @@ package com.susu.data.data.di import com.susu.data.data.repository.LedgerRecentSearchRepositoryImpl import com.susu.data.data.repository.LoginRepositoryImpl import com.susu.data.data.repository.SignUpRepositoryImpl +import com.susu.data.data.repository.TermRepositoryImpl import com.susu.data.data.repository.TokenRepositoryImpl import com.susu.domain.repository.LedgerRecentSearchRepository import com.susu.domain.repository.LoginRepository import com.susu.domain.repository.SignUpRepository +import com.susu.domain.repository.TermRepository import com.susu.domain.repository.TokenRepository import dagger.Binds import dagger.Module @@ -36,4 +38,9 @@ abstract class RepositoryModule { abstract fun bindLedgerRecentSearchRepository( ledgerRecentSearchRepositoryImpl: LedgerRecentSearchRepositoryImpl, ): LedgerRecentSearchRepository + + @Binds + abstract fun bindTermRepository( + termRepositoryImpl: TermRepositoryImpl, + ): TermRepository } diff --git a/data/src/main/java/com/susu/data/data/repository/TermRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/TermRepositoryImpl.kt new file mode 100644 index 00000000..a378afc2 --- /dev/null +++ b/data/src/main/java/com/susu/data/data/repository/TermRepositoryImpl.kt @@ -0,0 +1,16 @@ +package com.susu.data.data.repository + +import com.susu.core.model.Term +import com.susu.core.model.TermDetail +import com.susu.data.remote.api.TermService +import com.susu.data.remote.model.response.toModel +import com.susu.domain.repository.TermRepository +import javax.inject.Inject + +class TermRepositoryImpl @Inject constructor( + private val termService: TermService, +) : TermRepository { + override suspend fun getTerms(): List = termService.getTerms().getOrThrow().map { it.toModel() } + + override suspend fun getTermDetail(id: Int): TermDetail = termService.getTermDetail(id).getOrThrow().toModel() +} diff --git a/data/src/main/java/com/susu/data/remote/api/TermService.kt b/data/src/main/java/com/susu/data/remote/api/TermService.kt new file mode 100644 index 00000000..e507eaa5 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/api/TermService.kt @@ -0,0 +1,15 @@ +package com.susu.data.remote.api + +import com.susu.data.remote.model.response.TermDetailResponse +import com.susu.data.remote.model.response.TermResponse +import com.susu.data.remote.retrofit.ApiResult +import retrofit2.http.GET +import retrofit2.http.Path + +interface TermService { + @GET("terms") + suspend fun getTerms(): ApiResult> + + @GET("terms/{id}") + suspend fun getTermDetail(@Path("id") id: Int): ApiResult +} diff --git a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt index 3648ddb7..7ddec6cc 100644 --- a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt +++ b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt @@ -1,6 +1,7 @@ package com.susu.data.remote.di import com.susu.data.remote.api.SignUpService +import com.susu.data.remote.api.TermService import com.susu.data.remote.api.TokenService import com.susu.data.remote.api.UserService import dagger.Module @@ -8,6 +9,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import retrofit2.Retrofit +import retrofit2.create import javax.inject.Singleton @Module @@ -31,4 +33,10 @@ object ApiServiceModule { fun provideTokenService(@AuthRetrofit retrofit: Retrofit): TokenService { return retrofit.create(TokenService::class.java) } + + @Singleton + @Provides + fun providesTermService(retrofit: Retrofit): TermService { + return retrofit.create(TermService::class.java) + } } diff --git a/data/src/main/java/com/susu/data/remote/model/request/UserRequest.kt b/data/src/main/java/com/susu/data/remote/model/request/UserRequest.kt index e2eee3af..b212b35a 100644 --- a/data/src/main/java/com/susu/data/remote/model/request/UserRequest.kt +++ b/data/src/main/java/com/susu/data/remote/model/request/UserRequest.kt @@ -6,12 +6,14 @@ import kotlinx.serialization.Serializable @Serializable data class UserRequest( val name: String, - val gender: String, - val birth: Int, + val gender: String?, + val termAgreement: List, + val birth: Int?, ) fun User.toData() = UserRequest( name = name, - gender = gender, - birth = birth, + gender = gender.ifEmpty { null }, + birth = if (birth < 0) null else birth, + termAgreement = termAgreement, ) diff --git a/data/src/main/java/com/susu/data/remote/model/response/TermResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/TermResponse.kt new file mode 100644 index 00000000..dea1c6e1 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/TermResponse.kt @@ -0,0 +1,33 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.Term +import com.susu.core.model.TermDetail +import kotlinx.serialization.Serializable + +@Serializable +data class TermResponse( + val id: Int, + val title: String, + val isEssential: Boolean, +) + +@Serializable +data class TermDetailResponse( + val id: Int, + val title: String, + val description: String, + val isEssential: Boolean, +) + +fun TermResponse.toModel(): Term = Term( + id = id, + title = title, + isEssential = isEssential, +) + +fun TermDetailResponse.toModel(): TermDetail = TermDetail( + id = id, + title = title, + description = description, + isEssential = isEssential, +) diff --git a/domain/src/main/java/com/susu/domain/repository/TermRepository.kt b/domain/src/main/java/com/susu/domain/repository/TermRepository.kt new file mode 100644 index 00000000..b5f645c8 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/repository/TermRepository.kt @@ -0,0 +1,9 @@ +package com.susu.domain.repository + +import com.susu.core.model.Term +import com.susu.core.model.TermDetail + +interface TermRepository { + suspend fun getTerms(): List + suspend fun getTermDetail(id: Int): TermDetail +} diff --git a/domain/src/main/java/com/susu/domain/usecase/loginsignup/GetTermDetailUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/loginsignup/GetTermDetailUseCase.kt new file mode 100644 index 00000000..f520fb5e --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/loginsignup/GetTermDetailUseCase.kt @@ -0,0 +1,14 @@ +package com.susu.domain.usecase.loginsignup + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.TermRepository +import javax.inject.Inject + +class GetTermDetailUseCase @Inject constructor( + private val termRepository: TermRepository, +) { + + suspend operator fun invoke(termId: Int) = runCatchingIgnoreCancelled { + termRepository.getTermDetail(termId) + } +} diff --git a/domain/src/main/java/com/susu/domain/usecase/loginsignup/GetTermsUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/loginsignup/GetTermsUseCase.kt new file mode 100644 index 00000000..d3b1178e --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/loginsignup/GetTermsUseCase.kt @@ -0,0 +1,14 @@ +package com.susu.domain.usecase.loginsignup + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.TermRepository +import javax.inject.Inject + +class GetTermsUseCase @Inject constructor( + private val termRepository: TermRepository, +) { + + suspend operator fun invoke() = runCatchingIgnoreCancelled { + termRepository.getTerms() + } +} diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/navigation/LoginSignupNavigation.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/navigation/LoginSignupNavigation.kt index 1be813c8..cfceb4d3 100644 --- a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/navigation/LoginSignupNavigation.kt +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/navigation/LoginSignupNavigation.kt @@ -1,12 +1,14 @@ package com.susu.feature.loginsignup.navigation +import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.susu.core.ui.SnackbarToken import com.susu.feature.loginsignup.VoteRoute import com.susu.feature.loginsignup.login.LoginRoute -import com.susu.feature.loginsignup.signup.SignUpScreen +import com.susu.feature.loginsignup.signup.SignUpRoute @Suppress("unused") fun NavController.navigateLoginSignup(navOptions: NavOptions) { @@ -14,39 +16,29 @@ fun NavController.navigateLoginSignup(navOptions: NavOptions) { } fun NavGraphBuilder.loginSignupNavGraph( - navController: NavController, + padding: PaddingValues, + navigateToLogin: () -> Unit, + navigateToSignUp: () -> Unit, navigateToReceived: () -> Unit, + onShowToast: (SnackbarToken) -> Unit, ) { composable(route = LoginSignupRoute.Parent.Vote.route) { VoteRoute( - navigateToLogin = { - navController.navigate(LoginSignupRoute.Parent.Login.route) { - popUpTo( - route = LoginSignupRoute.Parent.Vote.route, - ) { - inclusive = true - } - } - }, + navigateToLogin = navigateToLogin, ) } composable(route = LoginSignupRoute.Parent.Login.route) { LoginRoute( navigateToReceived = navigateToReceived, - navigateToSignUp = { - navController.navigate(LoginSignupRoute.Parent.SignUp.route) { - popUpTo( - route = LoginSignupRoute.Parent.SignUp.route, - ) { - inclusive = true - } - } - }, + navigateToSignUp = navigateToSignUp, ) } composable(route = LoginSignupRoute.Parent.SignUp.route) { - SignUpScreen( + SignUpRoute( + padding = padding, navigateToReceived = navigateToReceived, + navigateToLogin = navigateToLogin, + onShowToast = onShowToast, ) } } diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpContract.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpContract.kt index d0bc2ff9..60d2940c 100644 --- a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpContract.kt +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpContract.kt @@ -1,17 +1,53 @@ package com.susu.feature.loginsignup.signup +import androidx.annotation.StringRes import com.susu.core.ui.base.SideEffect import com.susu.core.ui.base.UiState +import com.susu.feature.loginsignup.R -sealed interface SignUpContract { - sealed class SignUpEffect : SideEffect { - data object NavigateToReceived : SignUpEffect() - data class ShowToast(val msg: String) : SignUpEffect() - } +sealed interface SignUpEffect : SideEffect { + data object NavigateToLogin : SignUpEffect + data object NavigateToReceived : SignUpEffect + data class ShowToast(val msg: String) : SignUpEffect +} + +data class SignUpState( + val isLoading: Boolean = false, + val currentStep: SignUpStep = SignUpStep.TERMS, + val agreedTerms: List = emptyList(), + val name: String = "", + val isNameValid: Boolean = true, + val gender: Gender = Gender.NONE, + val birth: Int = -1, +) : UiState + +enum class SignUpStep( + @StringRes val appBarTitle: Int?, + @StringRes val description: Int?, + @StringRes val bottomButtonText: Int, +) { + TERMS( + appBarTitle = R.string.signup_term_title, + description = R.string.signup_term_description, + bottomButtonText = com.susu.core.ui.R.string.word_next, + ), + TERM_DETAIL( + appBarTitle = R.string.signup_term_detail_title, + description = null, + bottomButtonText = R.string.signup_term_agree, + ), + NAME( + appBarTitle = null, + description = R.string.signup_name_description, + bottomButtonText = com.susu.core.ui.R.string.word_next, + ), + ADDITIONAL( + appBarTitle = null, + description = R.string.signup_additional_description, + bottomButtonText = com.susu.core.ui.R.string.word_done, + ), +} - data class SignUpState( - val name: String = "", - val gender: String = "M", - val birth: String = "0", - ) : UiState +enum class Gender(val content: String) { + NONE(""), MALE("M"), FEMALE("F") } diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpScreen.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpScreen.kt index 6a5aebd6..c96e8c15 100644 --- a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpScreen.kt +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpScreen.kt @@ -1,61 +1,243 @@ package com.susu.feature.loginsignup.signup -import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar +import com.susu.core.designsystem.component.appbar.SusuProgressAppBar +import com.susu.core.designsystem.component.appbar.icon.BackIcon +import com.susu.core.designsystem.component.bottomsheet.datepicker.SusuYearPickerBottomSheet +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.MediumButtonStyle +import com.susu.core.designsystem.component.button.SusuFilledButton +import com.susu.core.designsystem.component.screen.LoadingScreen +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.SnackbarToken +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.feature.loginsignup.signup.content.AdditionalContent +import com.susu.feature.loginsignup.signup.content.NameContent +import com.susu.feature.loginsignup.signup.content.TermsContent +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun SignUpScreen( +fun SignUpRoute( + padding: PaddingValues, viewModel: SignUpViewModel = hiltViewModel(), + termViewModel: TermViewModel = hiltViewModel(), navigateToReceived: () -> Unit, + navigateToLogin: () -> Unit, + onShowToast: (SnackbarToken) -> Unit = {}, ) { - val uiState by viewModel.uiState.collectAsState() - val context = LocalContext.current - - LaunchedEffect(key1 = viewModel.sideEffect) { - viewModel.sideEffect.collect { sideEffect -> - when (sideEffect) { - SignUpContract.SignUpEffect.NavigateToReceived -> { - Toast.makeText(context, "가입 성공", Toast.LENGTH_SHORT).show() - navigateToReceived() + val uiState: SignUpState by viewModel.uiState.collectAsStateWithLifecycle() + val termState: TermState by termViewModel.uiState.collectAsStateWithLifecycle() + + var showDatePicker by remember { mutableStateOf(false) } + + BackHandler { + viewModel.goPreviousStep() + } + + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + SignUpEffect.NavigateToLogin -> navigateToLogin() + SignUpEffect.NavigateToReceived -> navigateToReceived() + is SignUpEffect.ShowToast -> onShowToast(SnackbarToken(message = sideEffect.msg)) + } + } + + Box(modifier = Modifier.fillMaxSize().padding(padding)) { + SignUpScreen( + uiState = uiState, + isNextStepActive = when (uiState.currentStep) { + SignUpStep.TERMS -> uiState.agreedTerms.containsAll(termState.terms.filter { it.isEssential }.map { it.id }) + SignUpStep.TERM_DETAIL -> true + SignUpStep.NAME -> uiState.isNameValid && uiState.name.isNotEmpty() + SignUpStep.ADDITIONAL -> true + }, + onPreviousPressed = viewModel::goPreviousStep, + onNextPressed = { + if (uiState.currentStep == SignUpStep.TERM_DETAIL) { + viewModel.agreeTerm(termState.currentTerm.id) } + viewModel.goNextStep() + }, + ) { + AnimatedContent( + modifier = Modifier.weight(1f), + targetState = uiState.currentStep, + label = "SignUpContent", + transitionSpec = { + val direction = if (targetState.ordinal > initialState.ordinal) { + AnimatedContentTransitionScope.SlideDirection.Left + } else { + AnimatedContentTransitionScope.SlideDirection.Right + } + slideIntoContainer( + towards = direction, + animationSpec = tween(500), + ) togetherWith slideOutOfContainer( + towards = direction, + animationSpec = tween(500), + ) + }, + ) { targetState -> + when (targetState) { + SignUpStep.TERMS -> { + TermsContent( + modifier = Modifier.fillMaxSize(), + descriptionText = targetState.description?.let { stringResource(id = it) } ?: "", + terms = termState.terms, + agreedTerms = uiState.agreedTerms, + onDetailClick = { + termViewModel.updateCurrentTerm(it) + viewModel.goTermDetail() + }, + onSelectAll = { agree -> + if (agree) { + viewModel.agreeAllTerms(termState.terms.map { it.id }) + } else { + viewModel.disagreeAllTerms() + } + }, + onTermChecked = { agree, id -> + if (agree) viewModel.agreeTerm(id) else viewModel.disagreeTerm(id) + }, + ) + } + + SignUpStep.NAME -> { + NameContent( + modifier = Modifier.fillMaxSize(), + description = targetState.description?.let { stringResource(id = it) } ?: "", + text = uiState.name, + isError = uiState.isNameValid.not(), + onTextChange = viewModel::updateName, + onClickClearIcon = { viewModel.updateName("") }, + ) + } - is SignUpContract.SignUpEffect.ShowToast -> { - Toast.makeText(context, sideEffect.msg, Toast.LENGTH_SHORT).show() + SignUpStep.ADDITIONAL -> { + AdditionalContent( + modifier = Modifier.fillMaxSize(), + description = targetState.description?.let { stringResource(id = it) } ?: "", + selectedGender = uiState.gender, + selectedYear = uiState.birth, + onGenderSelect = viewModel::updateGender, + onYearClick = { showDatePicker = true }, + ) + } + + SignUpStep.TERM_DETAIL -> { + Text( + modifier = Modifier + .fillMaxSize() + .padding(SusuTheme.spacing.spacing_m), + text = termState.currentTerm.description, + style = SusuTheme.typography.text_xxxs, + ) + } } } } + + if (showDatePicker) { + SusuYearPickerBottomSheet( + maximumContainerHeight = 322.dp, + onDismissRequest = { + viewModel.updateBirth(it) + showDatePicker = false + }, + ) + } + + if (uiState.isLoading || termState.isLoading) { + LoadingScreen(modifier = Modifier.align(Alignment.Center)) + } } +} - Column { - TextField( - value = uiState.name, - onValueChange = viewModel::updateName, - label = { Text(text = "이름") }, - ) - TextField( - value = uiState.gender, - onValueChange = viewModel::updateGender, - label = { - Text(text = "성별 (M/F)") - }, +@Composable +fun SignUpScreen( + modifier: Modifier = Modifier, + uiState: SignUpState = SignUpState(), + isNextStepActive: Boolean = false, + onPreviousPressed: () -> Unit = {}, + onNextPressed: () -> Unit = {}, + content: @Composable ColumnScope.() -> Unit = {}, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + if (uiState.currentStep == SignUpStep.TERMS || uiState.currentStep == SignUpStep.TERM_DETAIL) { + SusuDefaultAppBar( + leftIcon = { + BackIcon( + onClick = onPreviousPressed, + ) + }, + title = uiState.currentStep.appBarTitle?.let { stringResource(id = it) } ?: "", + ) + } else { + SusuProgressAppBar( + leftIcon = { + BackIcon( + onClick = onPreviousPressed, + ) + }, + currentStep = SignUpStep.entries.indexOf(uiState.currentStep) - 1, + entireStep = SignUpStep.entries.size - 2, + ) + } + content() + SusuFilledButton( + modifier = Modifier.fillMaxWidth().imePadding(), + shape = RectangleShape, + color = FilledButtonColor.Black, + style = MediumButtonStyle.height60, + text = stringResource(id = uiState.currentStep.bottomButtonText), + isActive = isNextStepActive, + isClickable = isNextStepActive, + onClick = onNextPressed, ) - TextField( - value = uiState.birth, - onValueChange = { viewModel.updateBirth(it) }, - label = { Text(text = "출생년도 (1930 ~ 2030)") }, + } +} - ) - Button(onClick = viewModel::signUp) { - Text(text = "회원가입") +@Preview +@Composable +fun SignUpScreenPreview() { + SusuTheme { + SignUpScreen { + Text( + "hello", + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) } } } diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpViewModel.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpViewModel.kt index 7bb4f8f8..321f91cf 100644 --- a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpViewModel.kt +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/SignUpViewModel.kt @@ -2,7 +2,9 @@ package com.susu.feature.loginsignup.signup import androidx.lifecycle.viewModelScope import com.susu.core.model.User +import com.susu.core.ui.USER_NAME_MAX_LENGTH import com.susu.core.ui.base.BaseViewModel +import com.susu.core.ui.nameRegex import com.susu.domain.usecase.loginsignup.SignUpUseCase import com.susu.feature.loginsignup.social.KakaoLoginHelper import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,39 +14,83 @@ import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( private val signUpUseCase: SignUpUseCase, -) : BaseViewModel(SignUpContract.SignUpState()) { +) : BaseViewModel(SignUpState()) { fun updateName(name: String) { - intent { copy(name = name) } + val trimmedName = name.trim() + if (trimmedName.length > USER_NAME_MAX_LENGTH) return + + intent { copy(name = trimmedName, isNameValid = nameRegex.matches(trimmedName)) } } - fun updateGender(gender: String) { + fun updateGender(gender: Gender) { intent { copy(gender = gender) } } - fun updateBirth(birth: String) { + fun updateBirth(birth: Int) { intent { copy(birth = birth) } } - fun signUp() { + fun agreeTerm(termId: Int) { + intent { copy(agreedTerms = agreedTerms + termId) } + } + + fun disagreeTerm(termId: Int) { + intent { copy(agreedTerms = agreedTerms - termId) } + } + + fun agreeAllTerms(entireTermIds: List) { + intent { copy(agreedTerms = entireTermIds) } + } + + fun disagreeAllTerms() { + intent { copy(agreedTerms = emptyList()) } + } + + fun goNextStep() { + when (uiState.value.currentStep) { + SignUpStep.TERMS -> intent { copy(currentStep = SignUpStep.NAME) } + SignUpStep.TERM_DETAIL -> intent { copy(currentStep = SignUpStep.TERMS) } + SignUpStep.NAME -> intent { copy(currentStep = SignUpStep.ADDITIONAL) } + SignUpStep.ADDITIONAL -> signUp() + } + } + + fun goPreviousStep() { + when (uiState.value.currentStep) { + SignUpStep.TERMS -> postSideEffect(SignUpEffect.NavigateToLogin) + SignUpStep.TERM_DETAIL -> intent { copy(currentStep = SignUpStep.TERMS) } + SignUpStep.NAME -> intent { copy(currentStep = SignUpStep.TERMS) } + SignUpStep.ADDITIONAL -> intent { copy(currentStep = SignUpStep.NAME) } + } + } + + fun goTermDetail() { + intent { copy(currentStep = SignUpStep.TERM_DETAIL) } + } + + private fun signUp() { KakaoLoginHelper.getAccessToken { oauthAccessToken -> viewModelScope.launch { + intent { copy(isLoading = true) } if (oauthAccessToken != null) { signUpUseCase( oauthAccessToken = oauthAccessToken, user = User( name = uiState.value.name, - gender = uiState.value.gender, - birth = uiState.value.birth.toInt(), + gender = uiState.value.gender.content, + birth = uiState.value.birth, + termAgreement = uiState.value.agreedTerms, ), ).onSuccess { - postSideEffect(SignUpContract.SignUpEffect.NavigateToReceived) + postSideEffect(SignUpEffect.NavigateToReceived) }.onFailure { - postSideEffect(SignUpContract.SignUpEffect.ShowToast(it.message ?: "에러 발생")) + postSideEffect(SignUpEffect.ShowToast(it.message ?: "에러 발생")) } } else { - postSideEffect(SignUpContract.SignUpEffect.ShowToast("카카오톡 로그인 에러 발생")) + postSideEffect(SignUpEffect.ShowToast("카카오톡 로그인 에러 발생")) } + intent { copy(isLoading = false) } } } } diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/TermContract.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/TermContract.kt new file mode 100644 index 00000000..c832749b --- /dev/null +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/TermContract.kt @@ -0,0 +1,16 @@ +package com.susu.feature.loginsignup.signup + +import com.susu.core.model.Term +import com.susu.core.model.TermDetail +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +sealed interface TermEffect : SideEffect { + data class ShowToast(val msg: String) : TermEffect +} + +data class TermState( + val isLoading: Boolean = false, + val terms: List = emptyList(), + val currentTerm: TermDetail = TermDetail(0, "", false, ""), +) : UiState diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/TermViewModel.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/TermViewModel.kt new file mode 100644 index 00000000..01d3933e --- /dev/null +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/TermViewModel.kt @@ -0,0 +1,40 @@ +package com.susu.feature.loginsignup.signup + +import androidx.lifecycle.viewModelScope +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.loginsignup.GetTermDetailUseCase +import com.susu.domain.usecase.loginsignup.GetTermsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class TermViewModel @Inject constructor( + private val getTermsUseCase: GetTermsUseCase, + private val getTermDetailUseCase: GetTermDetailUseCase, +) : BaseViewModel(TermState()) { + + init { + viewModelScope.launch { + intent { copy(isLoading = true) } + getTermsUseCase().onSuccess { + intent { copy(terms = it, isLoading = false) } + }.onFailure { + postSideEffect(TermEffect.ShowToast(it.message ?: "약관을 불러오지 못했어요")) + } + intent { copy(isLoading = false) } + } + } + + fun updateCurrentTerm(termId: Int) { + viewModelScope.launch { + intent { copy(isLoading = true) } + getTermDetailUseCase(termId).onSuccess { + intent { copy(currentTerm = it, isLoading = false) } + }.onFailure { + postSideEffect(TermEffect.ShowToast(it.message ?: "약관 내용을 불러오지 못했어요")) + } + intent { copy(isLoading = false) } + } + } +} diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/AdditionalContent.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/AdditionalContent.kt new file mode 100644 index 00000000..05094a97 --- /dev/null +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/AdditionalContent.kt @@ -0,0 +1,90 @@ +package com.susu.feature.loginsignup.signup.content + +import androidx.compose.foundation.layout.Arrangement +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.component.button.GhostButtonColor +import com.susu.core.designsystem.component.button.MediumButtonStyle +import com.susu.core.designsystem.component.button.SusuGhostButton +import com.susu.core.designsystem.theme.Gray60 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.loginsignup.signup.Gender +import java.time.LocalDate + +@Composable +fun AdditionalContent( + modifier: Modifier = Modifier, + description: String = "", + selectedGender: Gender = Gender.NONE, + selectedYear: Int = -1, + onGenderSelect: (Gender) -> Unit = {}, + onYearClick: () -> Unit = {}, +) { + Column( + modifier = modifier.padding(horizontal = 16.dp, vertical = 24.dp), + ) { + Text(text = description, style = SusuTheme.typography.title_m) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxl)) + Text(text = "성별", style = SusuTheme.typography.title_xxxs, color = Gray60) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxs)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + SusuGhostButton( + modifier = Modifier.weight(1f), + text = stringResource(com.susu.core.ui.R.string.word_male), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + isClickable = true, + isActive = selectedGender == Gender.MALE, + onClick = { onGenderSelect(Gender.MALE) }, + ) + SusuGhostButton( + modifier = Modifier.weight(1f), + text = stringResource(com.susu.core.ui.R.string.word_female), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + isClickable = true, + isActive = selectedGender == Gender.FEMALE, + onClick = { onGenderSelect(Gender.FEMALE) }, + ) + } + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xl)) + Text(text = stringResource(com.susu.core.ui.R.string.word_birth), style = SusuTheme.typography.title_xxxs, color = Gray60) + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource( + id = com.susu.core.designsystem.R.string.word_year_format, + if (selectedYear < 0) LocalDate.now().year else selectedYear, + ), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + isClickable = true, + isActive = selectedYear > 0, + onClick = onYearClick, + ) + } +} + +@Preview(showSystemUi = true) +@Composable +fun AdditionalContentPreview() { + SusuTheme { + AdditionalContent( + modifier = Modifier.fillMaxSize(), + selectedGender = Gender.MALE, + ) + } +} diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/NameContent.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/NameContent.kt new file mode 100644 index 00000000..3ca94d63 --- /dev/null +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/NameContent.kt @@ -0,0 +1,51 @@ +package com.susu.feature.loginsignup.signup.content + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.susu.core.designsystem.component.textfield.SusuUnderlineTextField +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.loginsignup.R + +@Composable +fun NameContent( + modifier: Modifier = Modifier, + description: String = "", + text: String = "", + isError: Boolean = false, + onTextChange: (String) -> Unit = {}, + onClickClearIcon: () -> Unit = {}, +) { + Column( + modifier = modifier.padding(horizontal = SusuTheme.spacing.spacing_m, vertical = SusuTheme.spacing.spacing_xl), + ) { + Text(text = description, style = SusuTheme.typography.title_m) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxl)) + SusuUnderlineTextField( + text = text, + placeholder = stringResource(R.string.signup_name_sample), + isError = isError, + lengthLimit = 10, + description = if (isError && text.isNotEmpty()) stringResource(R.string.signup_name_limitation) else null, + onTextChange = onTextChange, + onClickClearIcon = onClickClearIcon, + ) + } +} + +@Preview(showSystemUi = true) +@Composable +fun NameContentPreview() { + SusuTheme { + NameContent( + modifier = Modifier.fillMaxSize(), + ) + } +} diff --git a/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/TermsContent.kt b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/TermsContent.kt new file mode 100644 index 00000000..87050445 --- /dev/null +++ b/feature/loginsignup/src/main/java/com/susu/feature/loginsignup/signup/content/TermsContent.kt @@ -0,0 +1,181 @@ +package com.susu.feature.loginsignup.signup.content + +import androidx.compose.foundation.Image +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.component.badge.BadgeColor +import com.susu.core.designsystem.component.badge.BadgeStyle +import com.susu.core.designsystem.component.badge.SusuBadge +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray15 +import com.susu.core.designsystem.theme.Gray30 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Term +import com.susu.core.ui.R +import com.susu.core.ui.extension.susuClickable + +@Composable +fun TermsContent( + modifier: Modifier = Modifier, + descriptionText: String = "", + terms: List = emptyList(), + agreedTerms: List = emptyList(), + onDetailClick: (termId: Int) -> Unit = {}, + onSelectAll: (agree: Boolean) -> Unit = {}, + onTermChecked: (agree: Boolean, id: Int) -> Unit = { _, _ -> }, +) { + Column( + modifier = modifier.padding(SusuTheme.spacing.spacing_m), + ) { + Text( + text = descriptionText, + style = SusuTheme.typography.title_m, + ) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xl)) + TermListItem( + title = stringResource(com.susu.feature.loginsignup.R.string.signup_term_agree_all), + checked = agreedTerms.containsAll(terms.map { it.id }), + isEssential = false, + canRead = false, + onCheckClick = { onSelectAll(it) }, + ) + HorizontalDivider( + thickness = 1.dp, + color = Gray30, + ) + terms.forEach { term -> + TermListItem( + title = term.title, + checked = agreedTerms.contains(term.id), + isEssential = term.isEssential, + onDetailClick = { onDetailClick(term.id) }, + onCheckClick = { + onTermChecked(it, term.id) + }, + ) + } + } +} + +@Composable +fun TermListItem( + modifier: Modifier = Modifier, + title: String = "", + checked: Boolean = false, + isEssential: Boolean = true, + canRead: Boolean = true, + onCheckClick: (Boolean) -> Unit = {}, + onDetailClick: () -> Unit = {}, +) { + Row( + modifier = modifier.padding(vertical = SusuTheme.spacing.spacing_m), + verticalAlignment = Alignment.CenterVertically, + ) { + TermCheckCircle(isChecked = checked, onCheckedChange = onCheckClick) + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_m)) + if (isEssential) { + SusuBadge( + color = BadgeColor.Blue60, + text = stringResource(com.susu.feature.loginsignup.R.string.signup_essential), + padding = BadgeStyle.smallBadge, + ) + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) + } + Text( + modifier = Modifier.weight(1f), + text = title, + style = SusuTheme.typography.title_xxs, + ) + if (canRead) { + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_m)) + Image( + modifier = Modifier + .size(20.dp) + .susuClickable(rippleEnabled = false, onClick = onDetailClick), + painter = painterResource(id = R.drawable.ic_arrow_right), + contentDescription = "약관 보기", + ) + } + } +} + +@Composable +fun TermCheckCircle( + modifier: Modifier = Modifier, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit = {}, +) { + Box( + modifier = modifier + .size(20.dp) + .drawBehind { + if (isChecked) { + drawCircle( + color = Gray100, + radius = 8.dp.toPx(), + ) + } else { + drawCircle( + color = Gray40, + radius = 8.dp.toPx(), + style = Stroke(width = 1.dp.toPx()), + ) + } + } + .susuClickable( + rippleEnabled = false, + onClick = { onCheckedChange(isChecked.not()) }, + ), + ) { + if (isChecked) { + Icon( + modifier = Modifier.padding(2.dp), + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = Gray15, + ) + } + } +} + +@Preview(showSystemUi = true) +@Composable +fun TermsContentPreview() { + SusuTheme { + TermsContent( + modifier = Modifier.fillMaxSize(), + descriptionText = "어쩌구저쩌구\n뭐를해주세요", + terms = listOf(Term(1, "노예 계약", true), Term(2, "농노 계약", true)), + ) + } +} + +@Preview +@Composable +fun TermCheckCirclePreview() { + SusuTheme { + TermCheckCircle(isChecked = true) + } +} diff --git a/feature/loginsignup/src/main/res/values/strings.xml b/feature/loginsignup/src/main/res/values/strings.xml index 1c37b08c..5ed3527d 100644 --- a/feature/loginsignup/src/main/res/values/strings.xml +++ b/feature/loginsignup/src/main/res/values/strings.xml @@ -15,4 +15,14 @@ "은 " 얼마가 적당하다 고\n생각하시나요 + 김수수 + 한글과 영문 10자 이내로 입력해주세요 + 전체 동의하기 + 필수 + 약관 동의 + 서비스 약관을 위해\n약관에 동의해주세요 + 서비스 이용 약관 + 동의하기 + 반가워요!\n이름을 알려주세요 + 아래 정보들을 알려주시면\n통계를 알려드릴 수 있어요 diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt index e00c776d..b9ef735b 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt @@ -14,8 +14,10 @@ import com.susu.feature.community.navigation.navigateCommunity import com.susu.feature.loginsignup.navigation.LoginSignupRoute import com.susu.feature.mypage.navigation.navigateMyPage import com.susu.feature.received.navigation.ReceivedRoute +import com.susu.feature.received.navigation.navigateLedgerAdd import com.susu.feature.received.navigation.navigateLedgerDetail import com.susu.feature.received.navigation.navigateLedgerEdit +import com.susu.feature.received.navigation.navigateLedgerFilter import com.susu.feature.received.navigation.navigateLedgerSearch import com.susu.feature.received.navigation.navigateReceived import com.susu.feature.sent.navigation.SentRoute @@ -42,6 +44,7 @@ internal class MainNavigator( get() = when (currentDestination?.route) { in listOf( ReceivedRoute.ledgerSearchRoute, + ReceivedRoute.ledgerFilterRoute, SentRoute.sentEnvelopeRoute, SentRoute.sentEnvelopeDetailRoute, ), @@ -85,7 +88,11 @@ internal class MainNavigator( } fun navigateLogin() { - navController.navigate(LoginSignupRoute.Parent.Login.route) + navController.navigate(LoginSignupRoute.Parent.Login.route) { + popUpTo(id = navController.graph.id) { + inclusive = true + } + } } fun navigateSignup() { @@ -108,6 +115,14 @@ internal class MainNavigator( navController.navigateLedgerEdit() } + fun navigateLedgerFilter() { + navController.navigateLedgerFilter() + } + + fun navigateLedgerAdd() { + navController.navigateLedgerAdd() + } + fun popBackStackIfNotHome() { if (!isSameCurrentDestination(SentRoute.route)) { navController.popBackStack() diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index ed63e6d3..d7367a8a 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -53,8 +53,11 @@ internal fun MainScreen( startDestination = navigator.startDestination, ) { loginSignupNavGraph( - navController = navigator.navController, navigateToReceived = navigator::navigateSent, + navigateToLogin = navigator::navigateLogin, + navigateToSignUp = navigator::navigateSignup, + onShowToast = viewModel::onShowToast, + padding = innerPadding, ) sentNavGraph( @@ -70,6 +73,8 @@ internal fun MainScreen( navigateLedgerSearch = navigator::navigateLedgerSearch, navigateLedgerDetail = navigator::navigateLedgerDetail, navigateLedgerEdit = navigator::navigateLedgerEdit, + navigateLedgerFilter = navigator::navigateLedgerFilter, + navigateLedgerAdd = navigator::navigateLedgerAdd, ) statisticsNavGraph( diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddScreen.kt new file mode 100644 index 00000000..0902138a --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/LedgerAddScreen.kt @@ -0,0 +1,116 @@ +package com.susu.feature.received.ledgeradd + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.susu.core.designsystem.component.appbar.SusuProgressAppBar +import com.susu.core.designsystem.component.appbar.icon.BackIcon +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.MediumButtonStyle +import com.susu.core.designsystem.component.button.SusuFilledButton +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.R +import com.susu.core.ui.extension.susuDefaultAnimatedContentTransitionSpec +import com.susu.feature.received.ledgeradd.content.CategoryContent +import com.susu.feature.received.ledgeradd.content.DateContent +import com.susu.feature.received.ledgeradd.content.NameContent + +enum class LedgerAddStep { + CATEGORY, + NAME, + DATE, +} + +@Composable +fun LedgerAddRoute( + popBackStack: () -> Unit, +) { + var currentStep by remember { + mutableStateOf(LedgerAddStep.CATEGORY) + } + + LedgerAddScreen( + currentStep = currentStep, + onClickBack = popBackStack, + onClickNextButton = { + // TODO 임시 코드 입니다. + currentStep = when (currentStep) { + LedgerAddStep.CATEGORY -> LedgerAddStep.NAME + LedgerAddStep.NAME -> LedgerAddStep.DATE + LedgerAddStep.DATE -> LedgerAddStep.DATE + } + }, + ) +} + +@Composable +fun LedgerAddScreen( + currentStep: LedgerAddStep = LedgerAddStep.CATEGORY, + onClickBack: () -> Unit = {}, + onClickNextButton: () -> Unit = {}, +) { + Box( + modifier = Modifier + .background(SusuTheme.colorScheme.background15) + .fillMaxSize(), + ) { + Column { + SusuProgressAppBar( + leftIcon = { + BackIcon(onClickBack) + }, + entireStep = LedgerAddStep.entries.size, + currentStep = currentStep.ordinal + 1, + ) + + AnimatedContent( + modifier = Modifier.weight(1f), + targetState = currentStep, + label = "LedgerAddScreen", + transitionSpec = { + susuDefaultAnimatedContentTransitionSpec( + leftDirectionCondition = targetState.ordinal > initialState.ordinal, + ) + }, + ) { targetState -> + when (targetState) { + LedgerAddStep.CATEGORY -> CategoryContent() + LedgerAddStep.NAME -> NameContent() + LedgerAddStep.DATE -> DateContent() + } + } + + SusuFilledButton( + modifier = Modifier + .fillMaxWidth() + .imePadding(), + shape = RectangleShape, + color = FilledButtonColor.Black, + style = MediumButtonStyle.height60, + text = stringResource(id = R.string.word_save), + onClick = onClickNextButton, + ) + } + } +} + +@Preview +@Composable +fun ReceivedScreenPreview() { + SusuTheme { + LedgerAddScreen() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/CategoryContent.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/CategoryContent.kt new file mode 100644 index 00000000..1fc7b0e1 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/CategoryContent.kt @@ -0,0 +1,91 @@ +package com.susu.feature.received.ledgeradd.content + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.susu.core.designsystem.component.button.GhostButtonColor +import com.susu.core.designsystem.component.button.MediumButtonStyle +import com.susu.core.designsystem.component.button.SusuGhostButton +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.received.R + +@Composable +fun CategoryContent() { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = SusuTheme.spacing.spacing_xl, + start = SusuTheme.spacing.spacing_m, + end = SusuTheme.spacing.spacing_m, + ) + .verticalScroll(scrollState), + ) { + Text( + text = stringResource(R.string.select_category_screen_title), + style = SusuTheme.typography.title_m, + ) + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxl)) + + Column( + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = "결혼식", + ) + + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = "돌잔치", + ) + + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = "장례식", + ) + + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = "생일 기념일", + ) + + SusuGhostButton( + modifier = Modifier.fillMaxWidth(), + color = GhostButtonColor.Black, + style = MediumButtonStyle.height60, + text = stringResource(com.susu.core.ui.R.string.word_input_yourself), + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun CategoryContentPreview() { + SusuTheme { + CategoryContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/DateContent.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/DateContent.kt new file mode 100644 index 00000000..8d999704 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/DateContent.kt @@ -0,0 +1,60 @@ +package com.susu.feature.received.ledgeradd.content + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.component.bottomsheet.datepicker.SusuDatePickerBottomSheet +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.received.R +import com.susu.feature.received.ledgeradd.content.component.SelectDateRow + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DateContent() { + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = SusuTheme.spacing.spacing_xl, + start = SusuTheme.spacing.spacing_m, + end = SusuTheme.spacing.spacing_m, + ), + ) { + // TODO Annotated Text 사용 + Text( + text = "고모부의 장례식은 언제인가요", + style = SusuTheme.typography.title_m, + ) + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) + + SelectDateRow( + suffix = stringResource(R.string.ledger_add_screen_from), + ) + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxs)) + + SelectDateRow( + suffix = stringResource(R.string.ledger_add_screen_until), + ) + } + + SusuDatePickerBottomSheet(maximumContainerHeight = 346.dp) +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun DateContentPreview() { + SusuTheme { + DateContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/NameContent.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/NameContent.kt new file mode 100644 index 00000000..9712b652 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/NameContent.kt @@ -0,0 +1,47 @@ +package com.susu.feature.received.ledgeradd.content + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.susu.core.designsystem.component.textfield.SusuBasicTextField +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.received.R + +@Composable +fun NameContent() { + Column( + modifier = Modifier + .fillMaxSize() + .padding( + top = SusuTheme.spacing.spacing_xl, + start = SusuTheme.spacing.spacing_m, + end = SusuTheme.spacing.spacing_m, + ), + ) { + Text( + text = stringResource(R.string.input_name_screen_title), + style = SusuTheme.typography.title_m, + ) + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) + + SusuBasicTextField( + placeholder = stringResource(R.string.input_name_screen_textfield_placeholder), + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFF6F6F6) +@Composable +fun NameContentPreview() { + SusuTheme { + NameContent() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/component/SelectDateRow.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/component/SelectDateRow.kt new file mode 100644 index 00000000..0bf71509 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeradd/content/component/SelectDateRow.kt @@ -0,0 +1,69 @@ +package com.susu.feature.received.ledgeradd.content.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.susu.core.designsystem.theme.Gray30 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.util.AnnotatedText +import com.susu.feature.received.R +import java.time.LocalDateTime + +private val currentDate = LocalDateTime.now() + +@Composable +fun SelectDateRow( + year: Int? = null, + month: Int? = null, + day: Int? = null, + suffix: String? = null, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + verticalAlignment = Alignment.Bottom, + ) { + if (year == null || month == null || day == null) { + AnnotatedText( + originalText = stringResource( + R.string.ledger_add_screen_date, + currentDate.year, + currentDate.month.value, + currentDate.dayOfMonth, + ), + targetTextList = listOf( + currentDate.year.toString(), + currentDate.month.value.toString(), + currentDate.dayOfMonth.toString(), + ), + originalTextStyle = SusuTheme.typography.title_xl, + spanStyle = SusuTheme.typography.title_xl.copy(Gray30).toSpanStyle(), + ) + } else { + Text( + text = stringResource( + R.string.ledger_add_screen_date, + year, + month, + day, + ), + style = SusuTheme.typography.title_xl, + ) + } + + if (suffix != null) { + Text(text = suffix, style = SusuTheme.typography.title_l) + } + } +} + +@Preview +@Composable +fun SelectDateRowPreview() { + SusuTheme { + SelectDateRow() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt index 32e65e98..75472dbe 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt @@ -186,7 +186,7 @@ fun LedgerDetailScreen( @Preview @Composable -fun ReceivedScreenPreview() { +fun LedgerDetailScreenPreview() { SusuTheme { LedgerDetailScreen() } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt index 38f271d4..f8c9f64d 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt @@ -170,7 +170,7 @@ fun LedgerEditScreen() { @Preview @Composable -fun ReceivedScreenPreview() { +fun LedgerEditScreenPreview() { SusuTheme { LedgerEditScreen() } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerfilter/LedgerFilterScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerfilter/LedgerFilterScreen.kt new file mode 100644 index 00000000..823d8f4b --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerfilter/LedgerFilterScreen.kt @@ -0,0 +1,185 @@ +package com.susu.feature.received.ledgerfilter + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar +import com.susu.core.designsystem.component.appbar.icon.BackIcon +import com.susu.core.designsystem.component.button.FilledButtonColor +import com.susu.core.designsystem.component.button.LinedButtonColor +import com.susu.core.designsystem.component.button.RefreshButton +import com.susu.core.designsystem.component.button.SmallButtonStyle +import com.susu.core.designsystem.component.button.SusuFilledButton +import com.susu.core.designsystem.component.button.SusuLinedButton +import com.susu.core.designsystem.component.button.XSmallButtonStyle +import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.susuClickable +import com.susu.feature.received.R +import com.susu.feature.received.ledgerfilter.component.DateText + +@Composable +fun LedgerFilterRoute( + @Suppress("unused") + popBackStack: () -> Unit, +) { + LedgerFilterScreen( + onClickBackIcon = popBackStack, + ) +} + +@Composable +fun LedgerFilterScreen( + onClickBackIcon: () -> Unit = {}, +) { + Column( + modifier = Modifier + .background(SusuTheme.colorScheme.background10) + .fillMaxSize(), + ) { + SusuDefaultAppBar( + leftIcon = { + BackIcon(onClickBackIcon) + }, + title = stringResource(id = com.susu.core.ui.R.string.word_filter), + ) + + Column( + modifier = Modifier.padding( + top = SusuTheme.spacing.spacing_xl, + start = SusuTheme.spacing.spacing_m, + end = SusuTheme.spacing.spacing_m, + bottom = SusuTheme.spacing.spacing_xxs, + ), + ) { + Text(text = stringResource(R.string.ledger_filter_screen_event_category), style = SusuTheme.typography.title_xs) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) + Row( + horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + SusuLinedButton( + color = LinedButtonColor.Black, + style = XSmallButtonStyle.height28, + isActive = false, + text = "결혼식", + ) + + SusuLinedButton( + color = LinedButtonColor.Black, + style = XSmallButtonStyle.height28, + isActive = false, + text = "돌잔치", + ) + + SusuLinedButton( + color = LinedButtonColor.Black, + style = XSmallButtonStyle.height28, + isActive = false, + text = "장례식", + ) + + SusuLinedButton( + color = LinedButtonColor.Black, + style = XSmallButtonStyle.height28, + isActive = true, + text = "생일 기념일", + ) + } + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxxxxxl)) + Text( + text = stringResource(id = com.susu.core.ui.R.string.word_date), + style = SusuTheme.typography.title_xs, + ) + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + DateText() + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxxxs)) + + Text( + text = stringResource(R.string.ledger_filter_screen_from), + style = SusuTheme.typography.title_xxs, + ) + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) + + DateText() + + Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_xxxxs)) + + Text( + text = stringResource(R.string.ledger_filter_screen_until), + style = SusuTheme.typography.title_xxs, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Column( + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + SusuFilledButton( + color = FilledButtonColor.Orange, + style = XSmallButtonStyle.height28, + text = "결혼식", + isClickable = false, + rightIcon = { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(12.dp) + .susuClickable { /* TODO */ }, + painter = painterResource(id = com.susu.core.ui.R.drawable.ic_close), + contentDescription = stringResource(id = com.susu.core.ui.R.string.content_description_close_icon), + tint = Gray10, + ) + }, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_m), + ) { + RefreshButton() + + SusuFilledButton( + modifier = Modifier.fillMaxWidth(), + color = FilledButtonColor.Black, + style = SmallButtonStyle.height48, + isActive = true, + text = stringResource(com.susu.core.ui.R.string.word_apply_filter), + ) + } + } + } + } +} + +@Preview +@Composable +fun LedgerFilterScreenPreview() { + SusuTheme { + LedgerFilterScreen() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerfilter/component/DateText.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerfilter/component/DateText.kt new file mode 100644 index 00000000..5ecdd379 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerfilter/component/DateText.kt @@ -0,0 +1,45 @@ +package com.susu.feature.received.ledgerfilter.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.theme.Gray15 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.susuClickable +import com.susu.core.ui.util.currentDate +import com.susu.core.ui.util.to_yyyy_dot_MM_dot_dd + +@Composable +fun DateText( + text: String? = null, + onClick: () -> Unit = {}, +) { + Text( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .background(Gray15) + .padding( + horizontal = SusuTheme.spacing.spacing_m, + vertical = SusuTheme.spacing.spacing_xxxxs, + ) + .susuClickable(rippleEnabled = false, onClick = onClick), + text = text ?: currentDate.to_yyyy_dot_MM_dot_dd(), + style = SusuTheme.typography.title_xs, + color = Gray40, + ) +} + +@Preview +@Composable +fun DateTextPreview() { + SusuTheme { + DateText() + } +} diff --git a/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt b/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt index d0e20e5c..b83df67b 100644 --- a/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt +++ b/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt @@ -7,8 +7,10 @@ import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument +import com.susu.feature.received.ledgeradd.LedgerAddRoute import com.susu.feature.received.ledgerdetail.LedgerDetailRoute import com.susu.feature.received.ledgeredit.LedgerEditRoute +import com.susu.feature.received.ledgerfilter.LedgerFilterRoute import com.susu.feature.received.received.ReceivedRoute import com.susu.feature.received.search.LedgerSearchRoute @@ -28,18 +30,30 @@ fun NavController.navigateLedgerEdit() { navigate(ReceivedRoute.ledgerEditRoute) } +fun NavController.navigateLedgerFilter() { + navigate(ReceivedRoute.ledgerFilterRoute) +} + +fun NavController.navigateLedgerAdd() { + navigate(ReceivedRoute.ledgerAddRoute) +} + fun NavGraphBuilder.receivedNavGraph( padding: PaddingValues, navigateLedgerDetail: (Int) -> Unit, popBackStack: () -> Unit, navigateLedgerSearch: () -> Unit, navigateLedgerEdit: () -> Unit, + navigateLedgerFilter: () -> Unit, + navigateLedgerAdd: () -> Unit, ) { composable(route = ReceivedRoute.route) { ReceivedRoute( padding = padding, navigateLedgerDetail = navigateLedgerDetail, navigateLedgerSearch = navigateLedgerSearch, + navigateLedgerFilter = navigateLedgerFilter, + navigateLedgerAdd = navigateLedgerAdd, ) } @@ -67,6 +81,20 @@ fun NavGraphBuilder.receivedNavGraph( ) { LedgerEditRoute(popBackStack = popBackStack) } + + composable( + route = ReceivedRoute.ledgerFilterRoute, + ) { + LedgerFilterRoute(popBackStack = popBackStack) + } + + composable( + route = ReceivedRoute.ledgerAddRoute, + ) { + LedgerAddRoute( + popBackStack = popBackStack, + ) + } } object ReceivedRoute { @@ -76,4 +104,6 @@ object ReceivedRoute { const val ledgerSearchRoute = "ledger-search" const val ledgerEditRoute = "ledger-edit" // TODO 파라미터 넘기는 방식으로 수정해야함. + const val ledgerAddRoute = "ledger-add" // TODO 파라미터 넘기는 방식으로 수정해야함. + const val ledgerFilterRoute = "ledger-filter" // TODO 파라미터 넘기는 방식으로 수정해야함. } diff --git a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt index 9950e11f..16e1fe88 100644 --- a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt @@ -47,11 +47,16 @@ fun ReceivedRoute( padding: PaddingValues, navigateLedgerDetail: (Int) -> Unit, navigateLedgerSearch: () -> Unit, + navigateLedgerFilter: () -> Unit, + navigateLedgerAdd: () -> Unit, ) { ReceiveScreen( padding = padding, onClickLedgerCard = navigateLedgerDetail, + onClickLedgerAddCard = navigateLedgerAdd, onClickSearchIcon = navigateLedgerSearch, // TODO SideEffect로 변경 + onClickFloatingAddButton = navigateLedgerAdd, + onClickFilterButton = navigateLedgerFilter, ) } diff --git a/feature/received/src/main/res/values/strings.xml b/feature/received/src/main/res/values/strings.xml index 40d44585..953a889e 100644 --- a/feature/received/src/main/res/values/strings.xml +++ b/feature/received/src/main/res/values/strings.xml @@ -15,4 +15,13 @@ 일 부터 일 까지 + 부터 + 까지 + 경조사 카테고리 + 경조사명을 알려주세요 + 경조사명을 입력해주세요 + 어떤 경조사였나요 + %d년 %d월 %d일 + 까지 + 부터