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())
+ )
+ }
+}