Skip to content

Commit

Permalink
feat: 투표 추가 동작 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
jinukeu committed Jan 24, 2024
1 parent 7641e4e commit fc69408
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}

Expand Down
1 change: 1 addition & 0 deletions core/ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@
<string name="word_phone_number">연락처</string>
<string name="word_memo">메모</string>
<string name="word_register">등록</string>
<string name="word_free">자유</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ fun NavGraphBuilder.communityNavGraph(
}

composable(route = CommunityRoute.voteAddRoute) {
VoteAddRoute()
VoteAddRoute(
popBackStack = popBackStack,
handleException = handleException,
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Category> = persistentListOf(),
val selectedCategory: Category = Category(),
val voteOptionStateList: PersistentList<VoteOptionState> = 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,13 +19,16 @@ 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
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
Expand All @@ -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
Expand All @@ -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
)
},
)
Expand All @@ -80,19 +132,33 @@ 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) },
)
}
}

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),
Expand All @@ -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),
Expand All @@ -117,6 +189,7 @@ fun VoteAddScreen(
modifier = Modifier
.imePadding()
.clip(CircleShape)
.susuClickable(onClick = onClickAddOptionButton)
.background(Orange60)
.padding(SusuTheme.spacing.spacing_xxxxs)
.align(Alignment.CenterHorizontally),
Expand Down
Original file line number Diff line number Diff line change
@@ -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, VoteAddSideEffect>(
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())
)
}
}

0 comments on commit fc69408

Please sign in to comment.