From fc694081a7f94d1335e9828fab4a1b13f12eba50 Mon Sep 17 00:00:00 2001 From: jinukeu Date: Wed, 24 Jan 2024 20:48:43 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=88=AC=ED=91=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EB=8F=99=EC=9E=91=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/appbar/icon/RegisterText.kt | 10 +-- core/ui/src/main/res/values/strings.xml | 1 + .../navigation/CommunityNavigation.kt | 5 +- .../community/voteadd/VoteAddContract.kt | 29 +++++++ .../community/voteadd/VoteAddScreen.kt | 87 +++++++++++++++++-- .../community/voteadd/VoteAddViewModel.kt | 77 ++++++++++++++++ 6 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddContract.kt create mode 100644 feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddViewModel.kt diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/icon/RegisterText.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/icon/RegisterText.kt index 7dbf1179..8a7b4993 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/icon/RegisterText.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/appbar/icon/RegisterText.kt @@ -3,24 +3,24 @@ package com.susu.core.designsystem.component.appbar.icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.susu.core.designsystem.R +import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.SusuTheme import com.susu.core.ui.extension.susuClickable @Composable fun RegisterText( modifier: Modifier = Modifier, - onClick: () -> Unit = {}, + color: Color = Gray100, ) { Text( - modifier = modifier.susuClickable( - rippleEnabled = false, - onClick = onClick, - ), + modifier = modifier, text = stringResource(com.susu.core.ui.R.string.word_register), style = SusuTheme.typography.title_xxs, + color = color, ) } diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index e3aac96d..748fd4e8 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -42,4 +42,5 @@ 연락처 메모 등록 + 자유 diff --git a/feature/community/src/main/java/com/susu/feature/community/navigation/CommunityNavigation.kt b/feature/community/src/main/java/com/susu/feature/community/navigation/CommunityNavigation.kt index e3758977..9822cf6d 100644 --- a/feature/community/src/main/java/com/susu/feature/community/navigation/CommunityNavigation.kt +++ b/feature/community/src/main/java/com/susu/feature/community/navigation/CommunityNavigation.kt @@ -34,7 +34,10 @@ fun NavGraphBuilder.communityNavGraph( } composable(route = CommunityRoute.voteAddRoute) { - VoteAddRoute() + VoteAddRoute( + popBackStack = popBackStack, + handleException = handleException, + ) } } diff --git a/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddContract.kt b/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddContract.kt new file mode 100644 index 00000000..51450600 --- /dev/null +++ b/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddContract.kt @@ -0,0 +1,29 @@ +package com.susu.feature.community.voteadd + +import com.susu.core.model.Category +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import java.util.UUID + +data class VoteAddState( + val categoryConfigList: PersistentList = persistentListOf(), + val selectedCategory: Category = Category(), + val voteOptionStateList: PersistentList = persistentListOf(VoteOptionState(), VoteOptionState()), + val content: String = "", + val isLoading: Boolean = false, +) : UiState { + val buttonEnabled = content.length in 1 .. 50 && + voteOptionStateList.all { it.content.length in 1 ..10 } +} + +data class VoteOptionState( + val content: String = "", + val isSaved: Boolean = false, +) + +sealed interface VoteAddSideEffect : SideEffect { + data object PopBackStack : VoteAddSideEffect + data class HandleException(val throwable: Throwable, val retry: () -> Unit) : VoteAddSideEffect +} diff --git a/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddScreen.kt b/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddScreen.kt index 9a50f341..de95407c 100644 --- a/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddScreen.kt +++ b/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddScreen.kt @@ -8,6 +8,7 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -18,6 +19,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,6 +27,8 @@ 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 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.icon.BackIcon import com.susu.core.designsystem.component.appbar.icon.DeleteText @@ -39,18 +43,59 @@ import com.susu.core.designsystem.component.textfieldbutton.TextFieldButtonColor import com.susu.core.designsystem.component.textfieldbutton.style.MediumTextFieldButtonStyle import com.susu.core.designsystem.component.textfieldbutton.style.TextFieldButtonStyle import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.Orange60 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Category +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.core.ui.extension.susuClickable import com.susu.feature.community.R @Composable -fun VoteAddRoute() { - VoteAddScreen() +fun VoteAddRoute( + viewModel: VoteAddViewModel = hiltViewModel(), + popBackStack: () -> Unit, + handleException: (Throwable, () -> Unit) -> Unit, +) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is VoteAddSideEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) + VoteAddSideEffect.PopBackStack -> popBackStack() + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.getCategoryConfig() + } + + VoteAddScreen( + uiState = uiState, + onClickBack = viewModel::popBackStack, + onClickRegister = {}, + onClickCategoryButton = viewModel::selectCategory, + onTextChangeContent = viewModel::updateContent, + onTextChangeOptionContent = viewModel::updateOptionContent, + onClickOptionFilledButton = viewModel::toggleOptionSavedState, + onClickOptionClearIcon = { index -> viewModel.updateOptionContent(index, "") }, + onClickOptionCloseIcon = viewModel::removeOptionState, + onClickAddOptionButton = viewModel::addOptionState, + ) } @Composable fun VoteAddScreen( + uiState: VoteAddState = VoteAddState(), onClickBack: () -> Unit = {}, + onClickRegister: () -> Unit = {}, + onClickCategoryButton: (Category) -> Unit = {}, + onTextChangeContent: (String) -> Unit = {}, + onTextChangeOptionContent: (Int, String) -> Unit = { _, _ -> }, + onClickOptionFilledButton: (Int) -> Unit = {}, + onClickOptionClearIcon: (Int) -> Unit = {}, + onClickOptionCloseIcon: (Int) -> Unit = {}, + onClickAddOptionButton: () -> Unit = {}, ) { Column( modifier = Modifier @@ -66,7 +111,14 @@ fun VoteAddScreen( title = stringResource(R.string.vote_add_screen_title), actions = { RegisterText( - modifier = Modifier.padding(end = SusuTheme.spacing.spacing_m), + modifier = Modifier + .padding(end = SusuTheme.spacing.spacing_m) + .susuClickable( + rippleEnabled = false, + runIf = uiState.buttonEnabled, + onClick = onClickRegister + ), + color = if (uiState.buttonEnabled) Gray100 else Gray40 ) }, ) @@ -80,11 +132,23 @@ fun VoteAddScreen( Row( horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxxxs) ) { - listOf("결혼식", "장례식", "돌잔치", "생일 기념일", "자유").forEach { + uiState.categoryConfigList.dropLast(1).forEach { + SusuFilledButton( + color = FilledButtonColor.Black, + style = XSmallButtonStyle.height28, + text = it.name, + isActive = it == uiState.selectedCategory, + onClick = { onClickCategoryButton(it) }, + ) + } + + uiState.categoryConfigList.lastOrNull()?.let { SusuFilledButton( color = FilledButtonColor.Black, style = XSmallButtonStyle.height28, - text = it, + text = stringResource(com.susu.core.ui.R.string.word_free), + isActive = it == uiState.selectedCategory, + onClick = { onClickCategoryButton(it) }, ) } } @@ -92,7 +156,9 @@ fun VoteAddScreen( Spacer(modifier = Modifier.size(SusuTheme.spacing.spacing_m)) SusuBasicTextField( - text = "", + modifier = Modifier.fillMaxWidth(), + text = uiState.content, + onTextChange = onTextChangeContent, textStyle = SusuTheme.typography.text_xxs, maxLines = 10, placeholder = stringResource(R.string.vote_add_screen_textfield_placeholder), @@ -103,8 +169,14 @@ fun VoteAddScreen( Column( verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs) ) { - repeat(5) { + uiState.voteOptionStateList.forEachIndexed { index, option -> SusuTextFieldFillMaxButton( + text = option.content, + onTextChange = { text -> onTextChangeOptionContent(index, text) }, + onClickFilledButton = { onClickOptionFilledButton(index) }, + onClickClearIcon = { onClickOptionClearIcon(index) }, + onClickCloseIcon = { onClickOptionCloseIcon(index) }, + isSaved = option.isSaved, style = MediumTextFieldButtonStyle.height52, color = TextFieldButtonColor.Gray, placeholder = stringResource(R.string.vote_add_screen_textfield_button_placeholder), @@ -117,6 +189,7 @@ fun VoteAddScreen( modifier = Modifier .imePadding() .clip(CircleShape) + .susuClickable(onClick = onClickAddOptionButton) .background(Orange60) .padding(SusuTheme.spacing.spacing_xxxxs) .align(Alignment.CenterHorizontally), diff --git a/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddViewModel.kt b/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddViewModel.kt new file mode 100644 index 00000000..b93018e7 --- /dev/null +++ b/feature/community/src/main/java/com/susu/feature/community/voteadd/VoteAddViewModel.kt @@ -0,0 +1,77 @@ +package com.susu.feature.community.voteadd + +import androidx.lifecycle.viewModelScope +import com.susu.core.model.Category +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.categoryconfig.GetCategoryConfigUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class VoteAddViewModel @Inject constructor( + private val getCategoryConfigUseCase: GetCategoryConfigUseCase, +) : BaseViewModel( + VoteAddState(), +) { + companion object { + private const val MIN_OPTION_COUNT = 2 + private const val MAX_OPTION_COUNT = 5 + } + + fun getCategoryConfig() = viewModelScope.launch { + if (currentState.categoryConfigList.isNotEmpty()) return@launch + + getCategoryConfigUseCase() + .onSuccess { categoryConfig -> + intent { + copy( + categoryConfigList = categoryConfig.toPersistentList(), + selectedCategory = categoryConfig.first(), + ) + } + } + } + + fun popBackStack() = postSideEffect(VoteAddSideEffect.PopBackStack) + fun selectCategory(category: Category) = intent { + copy(selectedCategory = category) + } + + fun updateContent(content: String) = intent { + copy(content = content) + } + + fun updateOptionContent(index: Int, content: String) = intent { + copy( + voteOptionStateList = voteOptionStateList.mapIndexed { voteIndex, voteOptionState -> + if (index == voteIndex) voteOptionState.copy(content = content) + else voteOptionState + }.toPersistentList() + ) + } + + fun toggleOptionSavedState(index: Int) = intent { + copy( + voteOptionStateList = voteOptionStateList.mapIndexed { voteIndex, voteOptionState -> + if (index == voteIndex) voteOptionState.copy(isSaved = voteOptionState.isSaved.not()) + else voteOptionState + }.toPersistentList() + ) + } + + fun removeOptionState(index: Int) = intent { + if (voteOptionStateList.size <= MIN_OPTION_COUNT) return@intent this + copy( + voteOptionStateList = voteOptionStateList.removeAt(index) + ) + } + + fun addOptionState() = intent { + if (voteOptionStateList.size >= MAX_OPTION_COUNT) return@intent this + copy( + voteOptionStateList = voteOptionStateList.add(VoteOptionState()) + ) + } +}