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일
+ 까지
+ 부터