diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt index 57db2455..c003390b 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt @@ -7,11 +7,12 @@ import com.ninecraft.booket.core.designsystem.theme.Kakao import com.ninecraft.booket.core.designsystem.theme.ReedTheme enum class ReedButtonColorStyle { - PRIMARY, SECONDARY, TERTIARY, STROKE, KAKAO; + PRIMARY, PRIMARY_INVERSE_TEXT, SECONDARY, TERTIARY, STROKE, KAKAO; @Composable fun containerColor(isPressed: Boolean) = when (this) { PRIMARY -> if (isPressed) ReedTheme.colors.bgPrimaryPressed else ReedTheme.colors.bgPrimary + PRIMARY_INVERSE_TEXT -> if (isPressed) ReedTheme.colors.bgPrimaryPressed else ReedTheme.colors.bgPrimary SECONDARY -> if (isPressed) ReedTheme.colors.bgSecondaryPressed else ReedTheme.colors.bgSecondary TERTIARY -> if (isPressed) ReedTheme.colors.bgTertiaryPressed else ReedTheme.colors.bgTertiary STROKE -> if (isPressed) ReedTheme.colors.basePrimary else ReedTheme.colors.basePrimary @@ -21,6 +22,7 @@ enum class ReedButtonColorStyle { @Composable fun contentColor() = when (this) { PRIMARY -> ReedTheme.colors.contentInverse + PRIMARY_INVERSE_TEXT -> ReedTheme.colors.contentInverse SECONDARY -> ReedTheme.colors.contentPrimary TERTIARY -> ReedTheme.colors.contentBrand STROKE -> ReedTheme.colors.contentBrand @@ -31,7 +33,7 @@ enum class ReedButtonColorStyle { fun disabledContainerColor() = ReedTheme.colors.bgDisabled @Composable - fun disabledContentColor() = ReedTheme.colors.contentDisabled + fun disabledContentColor() = if (this == PRIMARY_INVERSE_TEXT) ReedTheme.colors.contentInverse else ReedTheme.colors.contentDisabled @Composable fun borderStroke() = when (this) { diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/InputTransformation.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/InputTransformation.kt new file mode 100644 index 00000000..9d25b4ae --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/InputTransformation.kt @@ -0,0 +1,18 @@ +package com.ninecraft.booket.core.designsystem.component.textfield + +import androidx.compose.foundation.text.input.TextFieldBuffer + +/** + * 숫자만 허용하고, 01, 00 같은 형식을 막는 InputTransformation + */ +val digitOnlyInputTransformation = { text: TextFieldBuffer -> + val filtered = text.toString().filter { it.isDigit() } + + val transformed = when { + filtered.isEmpty() -> "" + filtered == "0" -> "0" // 0 하나만 허용 + filtered.startsWith("0") -> filtered.trimStart('0') // 선행 0 제거 + else -> filtered + } + text.replace(0, text.length, transformed) +} diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/ReedRecordTextField.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/ReedRecordTextField.kt index 303a96d0..ca3535b4 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/ReedRecordTextField.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/textfield/ReedRecordTextField.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable 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.fillMaxWidth @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.selection.LocalTextSelectionColors @@ -41,11 +43,14 @@ fun ReedRecordTextField( recordState: TextFieldState, @StringRes recordHintRes: Int, modifier: Modifier = Modifier, + inputTransformation: InputTransformation? = null, keyboardOptions: KeyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Done, ), lineLimits: TextFieldLineLimits = TextFieldLineLimits.MultiLine(), + isError: Boolean = false, + errorMessage: String = "", onClear: (() -> Unit)? = null, onNext: () -> Unit = {}, backgroundColor: Color = ReedTheme.colors.baseSecondary, @@ -54,58 +59,70 @@ fun ReedRecordTextField( borderStroke: BorderStroke = BorderStroke(width = 1.dp, color = ReedTheme.colors.baseSecondary), ) { val keyboardController = LocalSoftwareKeyboardController.current + val errorBorderStroke = BorderStroke(width = 1.dp, color = ReedTheme.colors.borderError) CompositionLocalProvider(LocalTextSelectionColors provides reedTextSelectionColors) { - BasicTextField( - state = recordState, - modifier = Modifier.fillMaxWidth(), - textStyle = ReedTheme.typography.body2Medium.copy(color = textColor), - keyboardOptions = keyboardOptions, - onKeyboardAction = { - if (keyboardOptions.imeAction == ImeAction.Next) { - onNext() - } else { - keyboardController?.hide() - } - }, - lineLimits = lineLimits, - decorator = { innerTextField -> - Row( - modifier = modifier - .background(color = backgroundColor, shape = cornerShape) - .border( - border = borderStroke, - shape = cornerShape, - ) - .padding(vertical = ReedTheme.spacing.spacing3), - verticalAlignment = if (lineLimits is TextFieldLineLimits.MultiLine) Alignment.Top else Alignment.CenterVertically, - ) { - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) - Box(modifier = Modifier.weight(1f)) { - if (recordState.text.isEmpty()) { - Text( - text = stringResource(id = recordHintRes), - color = ReedTheme.colors.contentTertiary, - style = ReedTheme.typography.body2Regular, + Column { + BasicTextField( + state = recordState, + modifier = modifier.fillMaxWidth(), + inputTransformation = inputTransformation, + textStyle = ReedTheme.typography.body2Medium.copy(color = textColor), + keyboardOptions = keyboardOptions, + onKeyboardAction = { + if (keyboardOptions.imeAction == ImeAction.Next) { + onNext() + } else { + keyboardController?.hide() + } + }, + lineLimits = lineLimits, + decorator = { innerTextField -> + Row( + modifier = modifier + .background(color = backgroundColor, shape = cornerShape) + .border( + border = if (isError) errorBorderStroke else borderStroke, + shape = cornerShape, ) + .padding(vertical = ReedTheme.spacing.spacing3), + verticalAlignment = if (lineLimits is TextFieldLineLimits.MultiLine) Alignment.Top else Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) + Box(modifier = Modifier.weight(1f)) { + if (recordState.text.isEmpty()) { + Text( + text = stringResource(id = recordHintRes), + color = ReedTheme.colors.contentTertiary, + style = ReedTheme.typography.body2Regular, + ) + } + innerTextField() } - innerTextField() - } - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) - if (recordState.text.toString().isNotEmpty() && onClear != null) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_x_circle), - contentDescription = "Clear Icon", - modifier = Modifier.clickable { - onClear() - }, - tint = Color.Unspecified, - ) + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing2)) + if (recordState.text.toString().isNotEmpty() && onClear != null) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_x_circle), + contentDescription = "Clear Icon", + modifier = Modifier.clickable { + onClear() + }, + tint = Color.Unspecified, + ) + } + Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) } - Spacer(modifier = Modifier.width(ReedTheme.spacing.spacing4)) - } - }, - ) + }, + ) + if (isError) { + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + Text( + text = errorMessage, + color = ReedTheme.colors.contentError, + style = ReedTheme.typography.label2Regular, + ) + } + } } } diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt index caadb9a5..9f170927 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt @@ -64,6 +64,7 @@ val Blue800 = Color(0xFF1269EC) val Blue900 = Color(0xFF1F47CD) val Kakao = Color(0xFFFBD300) +val Blank = Color(0xFFD6D6D6) val HomeBg = Color(0xFFF0F9E8) // Emotion Color diff --git a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedFullScreen.kt b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedFullScreen.kt index 5f968b9d..9cecd2b0 100644 --- a/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedFullScreen.kt +++ b/core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/ReedFullScreen.kt @@ -1,13 +1,17 @@ package com.ninecraft.booket.core.ui.component import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import com.ninecraft.booket.core.designsystem.theme.White /** @@ -22,11 +26,19 @@ fun ReedFullScreen( backgroundColor: Color = White, content: @Composable ColumnScope.() -> Unit, ) { + val focusManager = LocalFocusManager.current + Column( modifier = modifier .fillMaxSize() .background(backgroundColor) - .systemBarsPadding(), + .systemBarsPadding() + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + ) { + focusManager.clearFocus() + }, ) { content() } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt index f0b4336f..6f7c2213 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/component/FilterChipGroup.kt @@ -1,16 +1,18 @@ package com.ninecraft.booket.feature.library.component import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme -import com.ninecraft.booket.feature.library.LibraryFilterOption import com.ninecraft.booket.feature.library.LibraryFilterChip +import com.ninecraft.booket.feature.library.LibraryFilterOption import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -21,19 +23,18 @@ fun FilterChipGroup( onChipClick: (LibraryFilterOption) -> Unit, modifier: Modifier = Modifier, ) { - Row( + LazyRow( modifier = modifier .fillMaxWidth() .padding( - start = ReedTheme.spacing.spacing5, top = ReedTheme.spacing.spacing3, - end = ReedTheme.spacing.spacing5, bottom = ReedTheme.spacing.spacing3, ), + contentPadding = PaddingValues(horizontal = ReedTheme.spacing.spacing5), horizontalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), verticalAlignment = Alignment.CenterVertically, ) { - filterList.forEach { item -> + items(filterList) { item -> FilterChip( option = item.option, count = item.count, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt index 8a4e41ff..017eb54c 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBottomSheet.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.SheetState @@ -36,12 +37,19 @@ import com.ninecraft.booket.core.designsystem.R as designR fun ImpressionGuideBottomSheet( onDismissRequest: () -> Unit, sheetState: SheetState, + impressionState: TextFieldState, impressionGuideList: ImmutableList, + beforeSelectedImpressionGuide: String, selectedImpressionGuide: String, onGuideClick: (Int) -> Unit, onCloseButtonClick: () -> Unit, onSelectionConfirmButtonClick: () -> Unit, ) { + val isImpressionEmpty = impressionState.text.isEmpty() + + val description = if (isImpressionEmpty) R.string.impression_guide_description else R.string.impression_guide_warning + val descriptionColor = if (isImpressionEmpty) ReedTheme.colors.contentSecondary else ReedTheme.colors.contentError + ReedBottomSheet( onDismissRequest = { onDismissRequest() @@ -62,7 +70,7 @@ fun ImpressionGuideBottomSheet( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - text = stringResource(R.string.impression_guide_bottomsheet_title), + text = stringResource(R.string.impression_step_guide), color = ReedTheme.colors.contentPrimary, textAlign = TextAlign.Center, style = ReedTheme.typography.heading2SemiBold, @@ -75,16 +83,17 @@ fun ImpressionGuideBottomSheet( }, ) } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing1)) Text( - text = stringResource(R.string.impression_guide_bottomsheet_description), + text = stringResource(description), modifier = Modifier.fillMaxWidth(), - color = ReedTheme.colors.contentPrimary, + color = descriptionColor, style = ReedTheme.typography.label1Medium, ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = ReedTheme.spacing.spacing5), verticalArrangement = Arrangement.spacedBy(ReedTheme.spacing.spacing2), ) { impressionGuideList.forEachIndexed { index, guide -> @@ -97,18 +106,30 @@ fun ImpressionGuideBottomSheet( ) } } - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) - ReedButton( - onClick = { - onSelectionConfirmButtonClick() - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.PRIMARY, - modifier = Modifier.fillMaxWidth(), - enabled = selectedImpressionGuide.isNotEmpty(), - text = stringResource(R.string.impression_guide_bottomsheet_selection_confirm), - ) + if (impressionState.text.isEmpty()) { + ReedButton( + onClick = { + onSelectionConfirmButtonClick() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.PRIMARY, + modifier = Modifier.fillMaxWidth(), + enabled = selectedImpressionGuide.isNotEmpty(), + text = stringResource(R.string.impression_guide_selection_done), + ) + } else { + ReedButton( + onClick = { + onSelectionConfirmButtonClick() + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.PRIMARY_INVERSE_TEXT, + modifier = Modifier.fillMaxWidth(), + enabled = beforeSelectedImpressionGuide != selectedImpressionGuide, + text = stringResource(R.string.impression_guide_change_done), + ) + } Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) } } @@ -138,7 +159,9 @@ private fun ImpressionGuideBottomSheetPreview() { ImpressionGuideBottomSheet( onDismissRequest = {}, sheetState = sheetState, + impressionState = TextFieldState(), impressionGuideList = impressionGuideList, + beforeSelectedImpressionGuide = "", selectedImpressionGuide = "", onGuideClick = {}, onCloseButtonClick = {}, diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt index b6bf1f4c..bc0f85b0 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/ImpressionGuideBox.kt @@ -12,11 +12,11 @@ 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.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.extensions.clickableSingle +import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.theme.Blank import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.feature.record.R @@ -44,7 +44,7 @@ fun ImpressionGuideBox( shape = RoundedCornerShape(ReedTheme.radius.sm), ) .clip(RoundedCornerShape(ReedTheme.radius.sm)) - .clickableSingle { + .noRippleClickable { onClick() } .padding( @@ -55,7 +55,7 @@ fun ImpressionGuideBox( Row(verticalAlignment = Alignment.Bottom) { Text( text = stringResource(R.string.impression_guide_blank), - color = Color(0xFFD6D6D6), + color = Blank, style = ReedTheme.typography.label1SemiBold, ) Text( diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt index ab30487a..2ecdb7a0 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/ocr/component/SentenceBox.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.common.extensions.clickableSingle +import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.ComponentPreview import com.ninecraft.booket.core.designsystem.theme.ReedTheme @@ -40,7 +40,7 @@ fun SentenceBox( shape = RoundedCornerShape(ReedTheme.radius.sm), ) .clip(RoundedCornerShape(ReedTheme.radius.sm)) - .clickableSingle { + .noRippleClickable { onClick() } .padding( diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt index 9ca4a4e5..e2e5b5a7 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterPresenter.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.text.TextRange import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.RecordRepository import com.ninecraft.booket.core.designsystem.EmotionTag @@ -59,15 +60,22 @@ class RecordRegisterPresenter @AssistedInject constructor( val emotionTags by rememberRetained { mutableStateOf(EmotionTag.entries.toPersistentList()) } var selectedEmotion by rememberRetained { mutableStateOf(null) } var selectedImpressionGuide by rememberRetained { mutableStateOf("") } + var beforeSelectedImpressionGuide by rememberRetained { mutableStateOf(selectedImpressionGuide) } val impressionState = rememberTextFieldState() var isImpressionGuideBottomSheetVisible by rememberRetained { mutableStateOf(false) } var isExitDialogVisible by rememberRetained { mutableStateOf(false) } var isRecordSavedDialogVisible by rememberRetained { mutableStateOf(false) } + val isPageError by remember { + derivedStateOf { + val page = recordPageState.text.toString().toIntOrNull() ?: 0 + page > MAX_PAGE + } + } val isNextButtonEnabled by remember { derivedStateOf { when (currentStep) { RecordStep.QUOTE -> { - recordPageState.text.isNotEmpty() && recordSentenceState.text.isNotEmpty() + recordPageState.text.isNotEmpty() && recordSentenceState.text.isNotEmpty() && !isPageError } RecordStep.EMOTION -> { selectedEmotion != null @@ -159,9 +167,9 @@ class RecordRegisterPresenter @AssistedInject constructor( } is RecordRegisterUiEvent.OnImpressionGuideButtonClick -> { - selectedImpressionGuide = "" - impressionState.edit { - replace(0, length, "") + beforeSelectedImpressionGuide = selectedImpressionGuide + if (impressionState.text.isEmpty()) { + selectedImpressionGuide = "" } isImpressionGuideBottomSheetVisible = true } @@ -170,14 +178,17 @@ class RecordRegisterPresenter @AssistedInject constructor( val index = event.index if (index in impressionGuideList.indices) { selectedImpressionGuide = impressionGuideList[index] - impressionState.edit { - replace(0, length, "") - append(selectedImpressionGuide) - } } } - is RecordRegisterUiEvent.OnSelectionConfirmed -> {} + is RecordRegisterUiEvent.OnImpressionGuideConfirmed -> { + impressionState.edit { + replace(0, length, "") + append(selectedImpressionGuide) + this.selection = TextRange(0) // 커서를 문장 맨 앞에 위치 + } + isImpressionGuideBottomSheetVisible = false + } is RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss -> { isImpressionGuideBottomSheetVisible = false @@ -222,11 +233,13 @@ class RecordRegisterPresenter @AssistedInject constructor( currentStep = currentStep, recordPageState = recordPageState, recordSentenceState = recordSentenceState, + isPageError = isPageError, emotionTags = emotionTags, selectedEmotion = selectedEmotion, impressionState = impressionState, impressionGuideList = impressionGuideList, selectedImpressionGuide = selectedImpressionGuide, + beforeSelectedImpressionGuide = beforeSelectedImpressionGuide, isNextButtonEnabled = isNextButtonEnabled, isImpressionGuideBottomSheetVisible = isImpressionGuideBottomSheetVisible, isExitDialogVisible = isExitDialogVisible, @@ -244,4 +257,8 @@ class RecordRegisterPresenter @AssistedInject constructor( navigator: Navigator, ): RecordRegisterPresenter } + + companion object { + const val MAX_PAGE = 1000 + } } diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt index 02f245ed..829da04e 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUi.kt @@ -81,6 +81,7 @@ internal fun RecordRegister( .padding(horizontal = ReedTheme.spacing.spacing5), enabled = state.isNextButtonEnabled, text = stringResource(R.string.record_next_button), + multipleEventsCutterEnabled = state.currentStep == RecordStep.IMPRESSION, ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing4)) } diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt index dd7c506c..ba81fc8c 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/register/RecordRegisterUiState.kt @@ -13,11 +13,13 @@ data class RecordRegisterUiState( val currentStep: RecordStep = RecordStep.QUOTE, val recordPageState: TextFieldState = TextFieldState(), val recordSentenceState: TextFieldState = TextFieldState(), + val isPageError: Boolean = false, val emotionTags: ImmutableList = persistentListOf(), val selectedEmotion: EmotionTag? = null, val impressionState: TextFieldState = TextFieldState(), val impressionGuideList: ImmutableList = persistentListOf(), val selectedImpressionGuide: String = "", + val beforeSelectedImpressionGuide: String = "", val isNextButtonEnabled: Boolean = false, val isImpressionGuideBottomSheetVisible: Boolean = false, val isExitDialogVisible: Boolean = false, @@ -42,7 +44,7 @@ sealed interface RecordRegisterUiEvent : CircuitUiEvent { data object OnImpressionGuideButtonClick : RecordRegisterUiEvent data object OnImpressionGuideBottomSheetDismiss : RecordRegisterUiEvent data class OnSelectImpressionGuide(val index: Int) : RecordRegisterUiEvent - data object OnSelectionConfirmed : RecordRegisterUiEvent + data object OnImpressionGuideConfirmed : RecordRegisterUiEvent data object OnExitDialogConfirm : RecordRegisterUiEvent data object OnExitDialogDismiss : RecordRegisterUiEvent data object OnRecordSavedDialogConfirm : RecordRegisterUiEvent diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt index b1f91fa3..3cd15717 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/ImpressionStep.kt @@ -11,10 +11,15 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp @@ -41,6 +46,16 @@ fun ImpressionStep( val impressionGuideBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + if (state.impressionState.text.isEmpty()) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + Column( modifier = modifier .background(White) @@ -63,6 +78,7 @@ fun ImpressionStep( recordHintRes = R.string.impression_step_hint, modifier = Modifier .fillMaxWidth() + .focusRequester(focusRequester) .height(140.dp), ) Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) @@ -89,7 +105,9 @@ fun ImpressionStep( state.eventSink(RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss) }, sheetState = impressionGuideBottomSheetState, + impressionState = state.impressionState, impressionGuideList = state.impressionGuideList, + beforeSelectedImpressionGuide = state.beforeSelectedImpressionGuide, selectedImpressionGuide = state.selectedImpressionGuide, onGuideClick = { state.eventSink(RecordRegisterUiEvent.OnSelectImpressionGuide(it)) @@ -103,7 +121,7 @@ fun ImpressionStep( onSelectionConfirmButtonClick = { coroutineScope.launch { impressionGuideBottomSheetState.hide() - state.eventSink(RecordRegisterUiEvent.OnImpressionGuideBottomSheetDismiss) + state.eventSink(RecordRegisterUiEvent.OnImpressionGuideConfirmed) } }, ) diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt index 27ebb03e..23888c94 100644 --- a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/step/QuoteStep.kt @@ -1,24 +1,25 @@ package com.ninecraft.booket.feature.record.step import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.TextFieldLineLimits 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.focus.FocusDirection import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.ninecraft.booket.core.designsystem.ComponentPreview @@ -26,11 +27,13 @@ import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle import com.ninecraft.booket.core.designsystem.component.button.smallRoundedButtonStyle import com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField +import com.ninecraft.booket.core.designsystem.component.textfield.digitOnlyInputTransformation import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White import com.ninecraft.booket.feature.record.R import com.ninecraft.booket.feature.record.register.RecordRegisterUiEvent import com.ninecraft.booket.feature.record.register.RecordRegisterUiState +import com.ninecraft.booket.core.designsystem.R as designR @Composable internal fun QuoteStep( @@ -39,71 +42,78 @@ internal fun QuoteStep( ) { val focusManager = LocalFocusManager.current - Column( + LazyColumn( modifier = modifier .background(White) + .imePadding() .padding(horizontal = ReedTheme.spacing.spacing5), ) { - Text( - text = stringResource(R.string.quote_step_title), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.heading1Bold, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing10)) - Text( - text = stringResource(R.string.quote_step_page_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordPageState, - recordHintRes = R.string.quote_step_page_hint, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Next, - ), - lineLimits = TextFieldLineLimits.SingleLine, - onClear = { - state.eventSink(RecordRegisterUiEvent.OnClearClick) - }, - onNext = { - focusManager.moveFocus(FocusDirection.Down) - }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) - Text( - text = stringResource(R.string.quote_step_sentence_label), - color = ReedTheme.colors.contentPrimary, - style = ReedTheme.typography.body1Medium, - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedRecordTextField( - recordState = state.recordSentenceState, - recordHintRes = R.string.quote_step_sentence_hint, - modifier = Modifier - .fillMaxWidth() - .height(140.dp), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) - ReedButton( - onClick = { - state.eventSink(RecordRegisterUiEvent.OnSentenceScanButtonClick) - }, - colorStyle = ReedButtonColorStyle.STROKE, - sizeStyle = smallRoundedButtonStyle, - modifier = Modifier.align(Alignment.End), - text = stringResource(R.string.quote_step_scan_sentence), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(com.ninecraft.booket.core.designsystem.R.drawable.ic_maximize), - contentDescription = "Scan Icon", + item { + Text( + text = stringResource(R.string.quote_step_title), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.heading1Bold, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing10)) + Text( + text = stringResource(R.string.quote_step_page_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordPageState, + recordHintRes = R.string.quote_step_page_hint, + inputTransformation = digitOnlyInputTransformation, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + lineLimits = TextFieldLineLimits.SingleLine, + isError = state.isPageError, + errorMessage = stringResource(R.string.quote_step_page_input_error), + onClear = { + state.eventSink(RecordRegisterUiEvent.OnClearClick) + }, + onNext = { + focusManager.moveFocus(FocusDirection.Down) + }, + modifier = Modifier + .fillMaxWidth() + .height(50.dp), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing8)) + Text( + text = stringResource(R.string.quote_step_sentence_label), + color = ReedTheme.colors.contentPrimary, + style = ReedTheme.typography.body1Medium, + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedRecordTextField( + recordState = state.recordSentenceState, + recordHintRes = R.string.quote_step_sentence_hint, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing3)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + ReedButton( + onClick = { + state.eventSink(RecordRegisterUiEvent.OnSentenceScanButtonClick) + }, + colorStyle = ReedButtonColorStyle.STROKE, + sizeStyle = smallRoundedButtonStyle, + text = stringResource(R.string.quote_step_scan_sentence), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(designR.drawable.ic_maximize), + contentDescription = "Scan Icon", + ) + }, ) - }, - ) + } + } } } diff --git a/feature/record/src/main/res/values/strings.xml b/feature/record/src/main/res/values/strings.xml index eec3c417..4fa7e4b1 100644 --- a/feature/record/src/main/res/values/strings.xml +++ b/feature/record/src/main/res/values/strings.xml @@ -29,12 +29,14 @@ 감상평 가이드로 쉽게 남길 수 있어요 내용을 입력해주세요. 감상평 가이드 - 감상평 가이드 - 아래 문장 중 하나를 선택해 이어서 감상을 적어보세요 - 선택 완료 + 아래 문장 중 하나를 선택해 이어서 감상을 적어보세요 + 새로운 가이드를 선택하면 기존 내용은 사라져요 + 선택 완료 + 변경 완료 ______ 기록이 저장되었어요! 방금 남긴 기록을 확인해볼까요? 기록 보러가기 닫기 + 해당 책의 마지막 페이지 수를 초과했습니다