From ee5fa793ff218aa6bf2c0f1e315977b7d9c4c284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:06:14 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=EA=B4=80=EA=B3=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20&=20=EB=93=B1=EB=A1=9D=20=EA=B5=AC=EC=B2=B4?= =?UTF-8?q?=ED=99=94=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Refactor]: 관계 등록 기능 구조 변경 -> 등록 + 수정 * [Feat]: relation 모델 구현 * [Feat]: destination 연걸, edit,add 구분 * [Feat]: Relation usecase 기능 추가 * [Fix]: Textfield focus 밖으로 빼기 * [Fix]: Textfield focus 수정 * [Fix]: Snackbar ui 수정 * [Feat]: 관게 등록 & 수정 구체화 * [Chore]: 코드 포맷 변경 --- .../relation/GetKakaoFriendInfoUseCase.kt | 3 +- .../common/view/SnackBarScreen.kt | 25 +- .../common/view/textfield/TypingTextField.kt | 15 +- .../RelationDetailWithUserInfoModel.kt | 42 + .../model/relation/RelationType.kt | 6 + ...elationConstant.kt => RelationConstant.kt} | 6 +- .../common/relation/RelationDestination.kt | 93 +++ .../home/common/relation/RelationEvent.kt | 22 + .../home/common/relation/RelationIntent.kt | 24 + .../home/common/relation/RelationModel.kt | 12 + .../home/common/relation/RelationScreen.kt | 784 ++++++++++++++++++ .../home/common/relation/RelationState.kt | 6 + .../home/common/relation/RelationViewModel.kt | 209 +++++ .../relation/add/AddRelationDestination.kt | 46 - .../common/relation/add/AddRelationEvent.kt | 7 - .../common/relation/add/AddRelationIntent.kt | 10 - .../common/relation/add/AddRelationModel.kt | 10 - .../common/relation/add/AddRelationScreen.kt | 352 -------- .../common/relation/add/AddRelationState.kt | 6 - .../relation/add/AddRelationViewModel.kt | 100 --- .../relation/get/GetRelationViewModel.kt | 3 +- 21 files changed, 1227 insertions(+), 554 deletions(-) create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationDetailWithUserInfoModel.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationType.kt rename presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/{add/AddRelationConstant.kt => RelationConstant.kt} (57%) create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationDestination.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationEvent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationIntent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationModel.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationScreen.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationState.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationViewModel.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationDestination.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationEvent.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationIntent.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationModel.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationScreen.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationState.kt delete mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationViewModel.kt diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/relation/GetKakaoFriendInfoUseCase.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/relation/GetKakaoFriendInfoUseCase.kt index 1f6b4925..3a1051f4 100644 --- a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/relation/GetKakaoFriendInfoUseCase.kt +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/usecase/feature/relation/GetKakaoFriendInfoUseCase.kt @@ -2,8 +2,9 @@ package ac.dnd.bookkeeping.android.domain.usecase.feature.relation import ac.dnd.bookkeeping.android.domain.model.feature.relation.KakaoFriendInfo import ac.dnd.bookkeeping.android.domain.repository.KakaoFriendRepository +import javax.inject.Inject -class GetKakaoFriendInfoUseCase( +class GetKakaoFriendInfoUseCase @Inject constructor( private val kakaoFriendRepository: KakaoFriendRepository ) { suspend operator fun invoke(): Result { diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt index b3576fd5..767abd00 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/SnackBarScreen.kt @@ -1,16 +1,18 @@ package ac.dnd.bookkeeping.android.presentation.common.view +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray100 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray800 +import ac.dnd.bookkeeping.android.presentation.common.theme.Shapes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp @Composable fun SnackBarScreen(message: String) { @@ -19,15 +21,22 @@ fun SnackBarScreen(message: String) { .padding(20.dp) .fillMaxWidth() .background( - shape = RoundedCornerShape(10.dp), - color = Color.LightGray + shape = Shapes.medium, + color = Gray800 ), ) { Text( text = message, - fontSize = 20.sp, - color = Color.White, - modifier = Modifier.padding(20.dp), + style = Body1.merge( + color = Gray100, + fontWeight = FontWeight.Normal + ), + modifier = Modifier + .padding( + start = 16.dp, + end = 8.dp + ) + .padding(vertical = 14.dp) ) } } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt index b0c3a234..a7e004cd 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/textfield/TypingTextField.kt @@ -59,6 +59,7 @@ fun TypingTextField( hintText: String = "", isError: Boolean = false, isEnabled: Boolean = true, + isSingleLine: Boolean = true, maxTextLength: Int = 100, keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), visualTransformation: VisualTransformation = VisualTransformation.None, @@ -71,15 +72,11 @@ fun TypingTextField( leadingIconContent: (@Composable () -> Unit)? = null, trailingIconContent: (@Composable () -> Unit)? = null, errorMessageContent: (@Composable () -> Unit) = { }, + onTextFieldFocusChange : (Boolean) -> Unit = {} ) { val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } var isTextFieldFocused by remember { mutableStateOf(false) } - val isSingleLine = when (textType) { - TypingTextFieldType.Basic -> true - TypingTextFieldType.LongSentence -> false - } - val currentColor = if (isError) Negative else if (isTextFieldFocused) Primary3 else basicBorderColor val currentColorState = animateColorAsState( @@ -102,6 +99,7 @@ fun TypingTextField( .wrapContentHeight() .onFocusChanged { isTextFieldFocused = it.isFocused + onTextFieldFocusChange(it.isFocused) } ) { BasicTextField( @@ -116,12 +114,9 @@ fun TypingTextField( textStyle = Body1.merge( color = Gray800 ), - singleLine = isSingleLine, + singleLine = if (textType == TypingTextFieldType.LongSentence) false else isSingleLine, minLines = if (isSingleLine) 1 else 3, keyboardOptions = keyboardOptions, - keyboardActions = KeyboardActions( - - ), cursorBrush = SolidColor(value = currentColorState.value), interactionSource = interactionSource, ) { textField -> @@ -129,7 +124,7 @@ fun TypingTextField( value = text, innerTextField = textField, enabled = isEnabled, - singleLine = isSingleLine, + singleLine = if (textType == TypingTextFieldType.LongSentence) false else isSingleLine, visualTransformation = visualTransformation, interactionSource = interactionSource, placeholder = { diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationDetailWithUserInfoModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationDetailWithUserInfoModel.kt new file mode 100644 index 00000000..89ba18f8 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationDetailWithUserInfoModel.kt @@ -0,0 +1,42 @@ +package ac.dnd.bookkeeping.android.presentation.model.relation + +import ac.dnd.bookkeeping.android.domain.model.feature.relation.RelationDetailGroup +import ac.dnd.bookkeeping.android.domain.model.feature.relation.RelationDetailWithUserInfo +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RelationDetailWithUserInfoModel( + val id: Long, + val name: String, + val imageUrl: String, + val memo: String, + val group: RelationDetailGroupModel, + val giveMoney: Long, + val takeMoney: Long +) : Parcelable + +fun RelationDetailWithUserInfo.toUiModel(): RelationDetailWithUserInfoModel { + return RelationDetailWithUserInfoModel( + id = id, + name = name, + imageUrl = imageUrl, + memo = memo, + group = group.toUiModel(), + giveMoney = giveMoney, + takeMoney = takeMoney + ) +} + +@Parcelize +data class RelationDetailGroupModel( + val id: Long, + val name: String +) : Parcelable + +fun RelationDetailGroup.toUiModel(): RelationDetailGroupModel { + return RelationDetailGroupModel( + id = id, + name = name + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationType.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationType.kt new file mode 100644 index 00000000..1e8fee74 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/model/relation/RelationType.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.presentation.model.relation + +enum class RelationType { + EDIT, + ADD +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationConstant.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationConstant.kt similarity index 57% rename from presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationConstant.kt rename to presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationConstant.kt index 519a95e6..d403fa01 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationConstant.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationConstant.kt @@ -1,8 +1,8 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation -object AddRelationConstant { +object RelationConstant { const val ROUTE: String = "/addName" const val ROUTE_ARGUMENT_MODEL = "relation" - const val CONTAIN_RELATION = "${ROUTE}/{${ROUTE_ARGUMENT_MODEL}}" + const val CONTAIN_RELATION = "$ROUTE/{$ROUTE_ARGUMENT_MODEL}" } diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationDestination.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationDestination.kt new file mode 100644 index 00000000..c2137c67 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationDestination.kt @@ -0,0 +1,93 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +import ac.dnd.bookkeeping.android.presentation.common.util.ErrorObserver +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationDetailGroupModel +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationDetailWithUserInfoModel +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationType +import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import androidx.compose.runtime.getValue +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable + +fun NavGraphBuilder.RelationDestination( + appState: ApplicationState +) { + val defaultModel = RelationDetailWithUserInfoModel( + id = 0L, + name = "", + imageUrl = "", + memo = "", + group = RelationDetailGroupModel( + id = -1L, + name = "" + ), + giveMoney = 0L, + takeMoney = 0L + ) + + composable( + route = RelationConstant.ROUTE + ) { + val viewModel: RelationViewModel = hiltViewModel() + + val model: RelationModel = let { + val state by viewModel.state.collectAsStateWithLifecycle() + val groups by viewModel.groups.collectAsStateWithLifecycle() + + RelationModel( + state = state, + groups = groups, + relationDetail = defaultModel + ) + } + + ErrorObserver(viewModel) + + RelationScreen( + relationType = RelationType.ADD, + appState = appState, + model = model, + event = viewModel.event, + intent = viewModel::onIntent, + handler = viewModel.handler + ) + } + + composable( + route = RelationConstant.CONTAIN_RELATION + ) { + val relationModel = appState.navController.previousBackStackEntry + ?.savedStateHandle + ?.get(RelationConstant.ROUTE_ARGUMENT_MODEL) + ?: defaultModel + + if (relationModel.id == -1L) { + appState.navController.popBackStack() + } + + val viewModel: RelationViewModel = hiltViewModel() + val model: RelationModel = let { + val state by viewModel.state.collectAsStateWithLifecycle() + val groups by viewModel.groups.collectAsStateWithLifecycle() + + RelationModel( + state = state, + groups = groups, + relationDetail = relationModel + ) + } + + ErrorObserver(viewModel) + + RelationScreen( + relationType = RelationType.EDIT, + appState = appState, + model = model, + event = viewModel.event, + intent = viewModel::onIntent, + handler = viewModel.handler + ) + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationEvent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationEvent.kt new file mode 100644 index 00000000..77ebb7f4 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationEvent.kt @@ -0,0 +1,22 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +sealed interface RelationEvent { + sealed interface AddRelation : RelationEvent { + data object Success : AddRelation + } + + sealed interface EditRelation : RelationEvent { + data object Success : EditRelation + } + + sealed interface DeleteRelation : RelationEvent { + data object Success : DeleteRelation + } + + sealed interface LoadKakaoFriend : RelationEvent { + data class Success( + val name: String, + val imageUrl: String + ) : LoadKakaoFriend + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationIntent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationIntent.kt new file mode 100644 index 00000000..bbbdffa5 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationIntent.kt @@ -0,0 +1,24 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +sealed interface RelationIntent { + data class OnClickAdd( + val groupId: Long, + val name: String, + val imageUrl: String, + val memo: String + ) : RelationIntent + + data class OnClickEdit( + val id: Long, + val groupId: Long, + val name: String, + val imageUrl: String, + val memo: String + ) : RelationIntent + + data class OnClickDelete( + val id: Long + ) : RelationIntent + + data object OnClickLoadFriend : RelationIntent +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationModel.kt new file mode 100644 index 00000000..0d2ab288 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationModel.kt @@ -0,0 +1,12 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +import ac.dnd.bookkeeping.android.domain.model.feature.group.Group +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationDetailWithUserInfoModel +import androidx.compose.runtime.Immutable + +@Immutable +data class RelationModel( + val state: RelationState, + val relationDetail: RelationDetailWithUserInfoModel, + val groups: List +) diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationScreen.kt new file mode 100644 index 00000000..47c3b833 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationScreen.kt @@ -0,0 +1,784 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +import ac.dnd.bookkeeping.android.domain.model.feature.group.Group +import ac.dnd.bookkeeping.android.presentation.R +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Caption2 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray200 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray400 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray500 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray800 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Headline3 +import ac.dnd.bookkeeping.android.presentation.common.theme.Negative +import ac.dnd.bookkeeping.android.presentation.common.theme.Shapes +import ac.dnd.bookkeeping.android.presentation.common.theme.Space12 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space20 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space24 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space4 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space56 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space8 +import ac.dnd.bookkeeping.android.presentation.common.theme.Space80 +import ac.dnd.bookkeeping.android.presentation.common.util.LaunchedEffectWithLifecycle +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.eventObserve +import ac.dnd.bookkeeping.android.presentation.common.util.expansion.addFocusCleaner +import ac.dnd.bookkeeping.android.presentation.common.view.DialogScreen +import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipItem +import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipType +import ac.dnd.bookkeeping.android.presentation.common.view.component.FieldSelectComponent +import ac.dnd.bookkeeping.android.presentation.common.view.component.FieldSubject +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButton +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonProperties +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonSize +import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonType +import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextField +import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextFieldType +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationDetailGroupModel +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationDetailWithUserInfoModel +import ac.dnd.bookkeeping.android.presentation.model.relation.RelationType +import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState +import ac.dnd.bookkeeping.android.presentation.ui.main.rememberApplicationState +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.aspectRatio +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.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import kotlinx.coroutines.CoroutineExceptionHandler + +@Composable +fun RelationScreen( + relationType: RelationType, + appState: ApplicationState, + model: RelationModel, + event: EventFlow, + intent: (RelationIntent) -> Unit, + handler: CoroutineExceptionHandler +) { + val focusManager = LocalFocusManager.current + var isRecordState by remember { mutableStateOf(false) } + var isNameTypeTyping by remember { mutableStateOf(true) } + var currentImageUrl by remember { mutableStateOf(model.relationDetail.imageUrl) } + var isUserNameInValid by remember { mutableStateOf(false) } + var currentNameText by remember { mutableStateOf(model.relationDetail.name) } + var isKakaoNameInValid by remember { mutableStateOf(false) } + var kakaoNameTextFocus by remember { mutableStateOf(false) } + var kakaoNameText by remember { mutableStateOf("카카오에서 친구 선택") } + var isShowingKakaoPicker by remember { mutableStateOf(false) } + var currentGroupId by remember { mutableLongStateOf(model.relationDetail.group.id) } + var isMemoTextFocus by remember { mutableStateOf(false) } + var memoText by remember { mutableStateOf(model.relationDetail.memo) } + var isShowingDeleteDialog by remember { mutableStateOf(false) } + var isCancelEditState by remember { mutableStateOf(false) } + + BackHandler( + enabled = true, + onBack = { + isCancelEditState = true + } + ) + + fun applyKakaoFriend(event: RelationEvent.LoadKakaoFriend) { + when (event) { + is RelationEvent.LoadKakaoFriend.Success -> { + currentImageUrl = event.imageUrl + kakaoNameText = event.name + } + } + } + + fun addRelation(event: RelationEvent.AddRelation) { + when (event) { + is RelationEvent.AddRelation.Success -> { + if (isRecordState) { + //TODO go heart record this relation + } else { + //TODO go main + } + } + } + } + + fun deleteRelation(event: RelationEvent.DeleteRelation) { + when (event) { + is RelationEvent.DeleteRelation.Success -> { + //TODO go main + } + } + } + + fun editRelation(event: RelationEvent.EditRelation) { + when (event) { + is RelationEvent.EditRelation.Success -> { + //TODO go main + } + } + } + + fun checkSubmitState(): Boolean { + return if (currentGroupId < 0) { + // TODO 그룹이 선택되지 않았습니다. 스낵바 + false + } else { + // TODO 이름이 입력되지 않았습니다. 스낵바 + !(isNameTypeTyping && currentNameText.isEmpty() || !isNameTypeTyping && kakaoNameText.isEmpty()) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(Gray000) + .addFocusCleaner(focusManager) + ) { + Row( + modifier = Modifier + .align(Alignment.TopCenter) + .height(Space56) + .fillMaxWidth() + .padding( + horizontal = 20.dp, + vertical = 13.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_chevron_left), + contentDescription = null, + modifier = Modifier.clickable { + isCancelEditState = true + } + ) + Spacer(modifier = Modifier.width(Space4)) + Text( + text = when (relationType) { + RelationType.EDIT -> "관계 수정하기" + RelationType.ADD -> "관계 등록하기" + }, + style = Headline1.merge( + color = Gray800, + fontWeight = FontWeight.SemiBold + ) + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = Space20) + .padding( + top = when (relationType) { + RelationType.ADD -> 120.dp + RelationType.EDIT -> 96.dp + } + ) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(Space80), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier.clickable { + // TODO to gallery + } + ) { + Box( + modifier = Modifier + .aspectRatio(1f) + .clip(CircleShape) + .background(Gray400) + ) { + AsyncImage( + model = currentImageUrl, + contentDescription = null, + contentScale = ContentScale.Crop + ) + } + Image( + painter = painterResource(R.drawable.ic_camera), + contentDescription = null, + modifier = Modifier.align(Alignment.BottomEnd) + ) + } + } + Spacer( + modifier = Modifier.height( + when (relationType) { + RelationType.ADD -> 60.dp + RelationType.EDIT -> 40.dp + } + ) + ) + FieldSubject(subject = "이름") + Spacer(modifier = Modifier.height(Space12)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + ChipItem( + chipType = if (isNameTypeTyping) ChipType.MAIN else ChipType.BORDER, + chipText = "직접 입력", + currentSelectedId = setOf(if (isNameTypeTyping) 0 else 1), + chipId = 0, + onSelectChip = { + isNameTypeTyping = true + } + ) + // TODO 툴팁 필요 -> 라이브러리 or 커스텀 + ChipItem( + chipType = if (isNameTypeTyping) ChipType.BORDER else ChipType.MAIN, + chipText = "카카오톡으로 등록", + chipId = 1, + currentSelectedId = setOf(if (isNameTypeTyping) 0 else 1), + onSelectChip = { + isNameTypeTyping = false + } + ) + } + Spacer(modifier = Modifier.height(Space12)) + if (isNameTypeTyping) { + TypingTextField( + textType = TypingTextFieldType.Basic, + text = currentNameText, + onValueChange = { + currentNameText = it + isUserNameInValid = currentNameText.length > 5 + }, + isError = isUserNameInValid, + errorMessageContent = { + if (isUserNameInValid) { + errorMessage() + } + }, + trailingIconContent = { + if (currentNameText.isNotEmpty()) { + if (isUserNameInValid) { + Image( + painter = painterResource(R.drawable.ic_warning), + contentDescription = null, + modifier = Modifier.size(Space20) + ) + } else { + Image( + painter = painterResource(R.drawable.ic_close_circle), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable { + currentNameText = "" + isUserNameInValid = false + } + ) + } + } + }, + ) + } else { + if (kakaoNameText == "카카오톡에서 친구 선택") { + FieldSelectComponent( + isSelected = isShowingKakaoPicker, + text = kakaoNameText, + onClick = { + isShowingKakaoPicker = true + }, + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + TypingTextField( + textType = TypingTextFieldType.Basic, + text = kakaoNameText, + onValueChange = { + currentNameText = it + isKakaoNameInValid = it.length > 5 + }, + isError = isKakaoNameInValid, + isSingleLine = true, + onTextFieldFocusChange = { + kakaoNameTextFocus = it + }, + trailingIconContent = { + if (kakaoNameTextFocus) { + if (kakaoNameText.isNotEmpty()) { + if (isKakaoNameInValid) { + Image( + painter = painterResource(R.drawable.ic_warning), + contentDescription = null, + modifier = Modifier.size(Space20) + ) + } else { + Image( + painter = painterResource(R.drawable.ic_close_circle), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable { + kakaoNameText = "" + isKakaoNameInValid = false + } + ) + } + } + } else { + Image( + painter = painterResource(R.drawable.ic_edit), + contentDescription = null + ) + } + }, + errorMessageContent = { + if (isKakaoNameInValid) { + errorMessage() + } + } + ) + Spacer(modifier = Modifier.width(12.dp)) + Box( + modifier = Modifier + .background( + color = Gray200, + shape = Shapes.medium + ) + .clickable { + isShowingKakaoPicker = true + } + .padding( + vertical = 13.5.dp, + horizontal = 16.dp + ) + ) { + Text( + text = "재등록", + style = Body1.merge( + color = Gray600, + fontWeight = FontWeight.SemiBold + ) + ) + } + } + } + } + Spacer(modifier = Modifier.height(Space24)) + FieldSubject(subject = "그룹") + Spacer(modifier = Modifier.height(6.dp)) + LazyRow( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + + items(model.groups) { group -> + ChipItem( + chipType = ChipType.BORDER, + currentSelectedId = setOf(currentGroupId), + chipText = group.name, + onSelectChip = { + currentGroupId = it + }, + chipId = group.id + ) + } + } + Spacer(modifier = Modifier.height(Space12)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(Shapes.medium) + .background(color = Gray200) + .clickable { + // TODO open -> GetGroupScreen + } + .padding( + horizontal = Space8, + vertical = 5.dp + ) + ) { + Image( + painter = painterResource(R.drawable.ic_edit), + contentDescription = null + ) + Spacer(modifier = Modifier.width(2.dp)) + Text( + text = "편집", + style = Caption2.merge( + color = Gray600, + fontWeight = FontWeight.Medium + ) + ) + } + Spacer(modifier = Modifier.height(Space24)) + FieldSubject( + subject = "메모", + isViewIcon = false + ) + Spacer( + modifier = Modifier.height(6.dp) + ) + when (relationType) { + RelationType.EDIT -> { + TypingTextField( + text = memoText, + onValueChange = { + memoText = it + }, + onTextFieldFocusChange = { + isMemoTextFocus = it + }, + textType = TypingTextFieldType.Basic, + trailingIconContent = { + if (isMemoTextFocus) { + if (memoText.isNotEmpty()) { + Image( + painter = painterResource(R.drawable.ic_close_circle), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable { + memoText = "" + } + ) + } + } else { + Image( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + colorFilter = ColorFilter.tint(Gray500) + ) + } + }, + isSingleLine = false + ) + } + + RelationType.ADD -> { + TypingTextField( + text = memoText, + onValueChange = { + memoText = it + }, + textType = TypingTextFieldType.LongSentence + ) + } + } + } + + Row( + modifier = Modifier + .background(color = Gray000) + .align(Alignment.BottomCenter) + .padding( + vertical = Space12, + horizontal = Space20 + ), + horizontalArrangement = Arrangement.spacedBy(Space12) + ) { + when (relationType) { + RelationType.EDIT -> { + Box( + modifier = Modifier + .clip(Shapes.large) + .border( + width = 1.dp, + color = Gray400, + shape = Shapes.large + ) + .clickable { + isShowingDeleteDialog = true + } + .padding( + vertical = 14.dp, + horizontal = 24.dp + ) + ) { + Image( + painter = painterResource(R.drawable.ic_trash), + colorFilter = ColorFilter.tint(Color.Black), + contentDescription = null, + modifier = Modifier.size(Space24) + ) + } + ConfirmButton( + modifier = Modifier.weight(1f), + properties = ConfirmButtonProperties( + size = ConfirmButtonSize.Xlarge, + type = ConfirmButtonType.Primary + ), + onClick = { + if (checkSubmitState()) { + intent( + RelationIntent.OnClickEdit( + id = model.relationDetail.id, + groupId = currentGroupId, + name = if (isNameTypeTyping) currentNameText else kakaoNameText, + imageUrl = currentImageUrl, + memo = memoText + ) + ) + } + } + ) { + Text( + text = "저장하기", + style = Headline3.merge( + color = Gray000, + fontWeight = FontWeight.SemiBold + ) + ) + } + } + + RelationType.ADD -> { + ConfirmButton( + modifier = Modifier.weight(1f), + properties = ConfirmButtonProperties( + size = ConfirmButtonSize.Xlarge, + type = ConfirmButtonType.Secondary + ), + content = { + Text( + text = "바로 마음 기록", + style = Headline3.merge( + color = Gray700, + fontWeight = FontWeight.SemiBold + ) + ) + }, + onClick = { + if (checkSubmitState()) { + isRecordState = true + intent( + RelationIntent.OnClickAdd( + groupId = currentGroupId, + name = if (isNameTypeTyping) currentNameText else kakaoNameText, + imageUrl = currentImageUrl, + memo = memoText + ) + ) + } + } + ) + ConfirmButton( + modifier = Modifier.weight(1f), + properties = ConfirmButtonProperties( + size = ConfirmButtonSize.Xlarge, + type = ConfirmButtonType.Primary + ), + content = { + Text( + text = "저장하기", + style = Headline3.merge( + color = Gray000, + fontWeight = FontWeight.SemiBold + ) + ) + }, + onClick = { + if (checkSubmitState()) { + intent( + RelationIntent.OnClickAdd( + groupId = currentGroupId, + name = if (isNameTypeTyping) currentNameText else kakaoNameText, + imageUrl = currentImageUrl, + memo = memoText + ) + ) + } + } + ) + } + } + } + + } + + if (isShowingKakaoPicker) { + intent(RelationIntent.OnClickLoadFriend) + kakaoNameTextFocus = false + } + + if (isShowingDeleteDialog) { + DialogScreen( + isCancelable = true, + message = "관계 내역을 삭제하시겠어요?", + cancelMessage = "취소", + confirmMessage = "삭제", + onCancel = { + isShowingDeleteDialog = false + }, + onConfirm = { + intent(RelationIntent.OnClickDelete(model.relationDetail.id)) + isShowingDeleteDialog = false + }, + onDismissRequest = { + isShowingDeleteDialog = false + } + ) + } + + LaunchedEffectWithLifecycle(event, handler) { + event.eventObserve { event -> + when (event) { + is RelationEvent.LoadKakaoFriend -> applyKakaoFriend(event) + is RelationEvent.AddRelation -> addRelation(event) + is RelationEvent.DeleteRelation -> deleteRelation(event) + is RelationEvent.EditRelation -> editRelation(event) + } + } + } +} + +@Composable +private fun errorMessage() { + Spacer(modifier = Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_alert_triangle), + contentDescription = null + ) + Spacer(Modifier.width(Space4)) + Text( + text = "5자 이내로 입력해주세요", + style = Body1.merge(color = Negative) + ) + } +} + +@Preview +@Composable +private fun AddRelationScreen1Preview() { + RelationScreen( + relationType = RelationType.ADD, + appState = rememberApplicationState(), + model = RelationModel( + groups = listOf( + Group( + id = 0, + name = "친구" + ), + Group( + id = 0, + name = "가족" + ), + Group( + id = 0, + name = "지인" + ), + Group( + id = 0, + name = "직장" + ), + Group( + id = -1, + name = "친척" + ) + ), + state = RelationState.Init, + relationDetail = RelationDetailWithUserInfoModel( + id = 0L, + name = "김진우", + imageUrl = "", + memo = "무르는 경사비 관리앱으로 사용자가 다양한 개인적인 축하 상황에 대해 금전적 기여를 쉽게 할 수 있게 돕는 모바일 애플리케이션입니다", + group = RelationDetailGroupModel( + id = -1, + name = "친척" + ), + giveMoney = 1000L, + takeMoney = 1000L + ) + ), + intent = {}, + event = MutableEventFlow(), + handler = CoroutineExceptionHandler { _, _ -> } + ) +} + +@Preview +@Composable +private fun AddRelationScreen2Preview() { + RelationScreen( + relationType = RelationType.EDIT, + appState = rememberApplicationState(), + model = RelationModel( + groups = listOf( + Group( + id = 0, + name = "친구" + ), + Group( + id = 0, + name = "가족" + ), + Group( + id = 0, + name = "지인" + ), + Group( + id = 0, + name = "직장" + ), + Group( + id = -1, + name = "친척" + ) + ), + state = RelationState.Init, + relationDetail = RelationDetailWithUserInfoModel( + id = 0L, + name = "김진우", + imageUrl = "", + memo = "무르는 경사비 관리앱으로 사용자가 다양한 개인적인 축하 상황에 대해 금전적 기여를 쉽게 할 수 있게 돕는 모바일 애플리케이션입니다", + group = RelationDetailGroupModel( + id = -1, + name = "친척" + ), + giveMoney = 1000L, + takeMoney = 1000L + ) + ), + intent = {}, + event = MutableEventFlow(), + handler = CoroutineExceptionHandler { _, _ -> } + ) +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationState.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationState.kt new file mode 100644 index 00000000..d7a8da75 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationState.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +sealed interface RelationState { + data object Init : RelationState + data object Loading : RelationState +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationViewModel.kt new file mode 100644 index 00000000..d79101e1 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/RelationViewModel.kt @@ -0,0 +1,209 @@ +package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation + +import ac.dnd.bookkeeping.android.domain.model.error.ServerException +import ac.dnd.bookkeeping.android.domain.model.feature.group.Group +import ac.dnd.bookkeeping.android.domain.usecase.feature.group.GetGroupListUseCase +import ac.dnd.bookkeeping.android.domain.usecase.feature.relation.AddRelationUseCase +import ac.dnd.bookkeeping.android.domain.usecase.feature.relation.DeleteRelationUseCase +import ac.dnd.bookkeeping.android.domain.usecase.feature.relation.EditRelationUseCase +import ac.dnd.bookkeeping.android.domain.usecase.feature.relation.GetKakaoFriendInfoUseCase +import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel +import ac.dnd.bookkeeping.android.presentation.common.base.ErrorEvent +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow +import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.asEventFlow +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class RelationViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val getGroupListUseCase: GetGroupListUseCase, + private val getKakaoFriendInfoUseCase: GetKakaoFriendInfoUseCase, + private val addRelationUseCase: AddRelationUseCase, + private val editRelationUseCase: EditRelationUseCase, + private val deleteRelationUseCase: DeleteRelationUseCase +) : BaseViewModel() { + + private val _state: MutableStateFlow = + MutableStateFlow(RelationState.Init) + val state: StateFlow = _state.asStateFlow() + + private val _event: MutableEventFlow = MutableEventFlow() + val event: EventFlow = _event.asEventFlow() + + private val _groups: MutableStateFlow> = MutableStateFlow(emptyList()) + val groups: StateFlow> = _groups.asStateFlow() + + init { + launch { + _state.value = RelationState.Loading + getGroupListUseCase() + .onSuccess { + _groups.value = it + } + .onFailure { exception -> + when (exception) { + is ServerException -> { + _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) + } + + else -> { + _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) + } + } + } + } + } + + fun onIntent(intent: RelationIntent) { + when (intent) { + is RelationIntent.OnClickAdd -> addRelation( + intent.groupId, + intent.name, + intent.imageUrl, + intent.memo + ) + + is RelationIntent.OnClickEdit -> editRelation( + intent.id, + intent.groupId, + intent.name, + intent.imageUrl, + intent.memo + ) + + is RelationIntent.OnClickDelete -> deleteRelation( + intent.id + ) + + is RelationIntent.OnClickLoadFriend -> loadKakaoFriend() + } + } + + private fun addRelation( + groupId: Long, + name: String, + imageUrl: String, + memo: String, + ) { + launch { + _state.value = RelationState.Loading + addRelationUseCase( + groupId = groupId, + name = name, + imageUrl = imageUrl, + memo = memo + ) + .onSuccess { + _state.value = RelationState.Init + _event.emit(RelationEvent.AddRelation.Success) + } + .onFailure { exception -> + _state.value = RelationState.Init + when (exception) { + is ServerException -> { + _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) + } + + else -> { + _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) + } + } + } + } + } + + private fun editRelation( + id: Long, + groupId: Long, + name: String, + imageUrl: String, + memo: String + ) { + launch { + _state.value = RelationState.Loading + editRelationUseCase( + id = id, + groupId = groupId, + name = name, + imageUrl = imageUrl, + memo = memo + ) + .onSuccess { + _state.value = RelationState.Init + _event.emit(RelationEvent.EditRelation.Success) + } + .onFailure { exception -> + _state.value = RelationState.Init + when (exception) { + is ServerException -> { + _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) + } + + else -> { + _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) + } + } + } + } + } + + private fun deleteRelation( + id: Long + ) { + launch { + _state.value = RelationState.Loading + deleteRelationUseCase(id = id) + .onSuccess { + _state.value = RelationState.Init + _event.emit(RelationEvent.DeleteRelation.Success) + } + .onFailure { exception -> + _state.value = RelationState.Init + when (exception) { + is ServerException -> { + _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) + } + + else -> { + _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) + } + } + + } + } + } + + private fun loadKakaoFriend() { + launch { + _state.value = RelationState.Loading + getKakaoFriendInfoUseCase() + .onSuccess { + _state.value = RelationState.Init + _event.emit( + RelationEvent.LoadKakaoFriend.Success( + name = it.name, + imageUrl = it.profileImageUrl + ) + ) + } + .onFailure { exception -> + _state.value = RelationState.Init + when (exception) { + is ServerException -> { + _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) + } + + else -> { + _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) + } + } + } + } + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationDestination.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationDestination.kt deleted file mode 100644 index 9bfb5941..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationDestination.kt +++ /dev/null @@ -1,46 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -import ac.dnd.bookkeeping.android.presentation.common.util.ErrorObserver -import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable - -fun NavGraphBuilder.AddRelationDestination( - appState: ApplicationState -) { - - composable( - route = AddRelationConstant.ROUTE - ) { - val viewModel: AddRelationViewModel = hiltViewModel() - - val model: AddRelationModel = let { - val state by viewModel.state.collectAsStateWithLifecycle() - val groups by viewModel.groups.collectAsStateWithLifecycle() - - AddRelationModel( - state = state, - groups = groups - ) - } - - ErrorObserver(viewModel) - - AddRelationScreen( - appState = appState, - model = model, - event = viewModel.event, - intent = viewModel::onIntent, - handler = viewModel.handler - ) - } - - composable( - route = AddRelationConstant.CONTAIN_RELATION - ) { - // TODO Edit relation - } -} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationEvent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationEvent.kt deleted file mode 100644 index 37414416..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -sealed interface AddRelationEvent { - sealed interface Submit : AddRelationEvent { - data object Success : Submit - } -} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationIntent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationIntent.kt deleted file mode 100644 index 531f6632..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationIntent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -sealed interface AddRelationIntent { - data class OnClickSubmit( - val groupId: Long, - val name: String, - val imageUrl: String, - val memo: String - ) : AddRelationIntent -} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationModel.kt deleted file mode 100644 index edd93fbc..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -import ac.dnd.bookkeeping.android.domain.model.feature.group.Group -import androidx.compose.runtime.Immutable - -@Immutable -data class AddRelationModel( - val state: AddRelationState, - val groups: List -) diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationScreen.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationScreen.kt deleted file mode 100644 index dabb0a9f..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationScreen.kt +++ /dev/null @@ -1,352 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -import ac.dnd.bookkeeping.android.domain.model.feature.group.Group -import ac.dnd.bookkeeping.android.presentation.R -import ac.dnd.bookkeeping.android.presentation.common.theme.Caption2 -import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 -import ac.dnd.bookkeeping.android.presentation.common.theme.Gray200 -import ac.dnd.bookkeeping.android.presentation.common.theme.Gray400 -import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 -import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 -import ac.dnd.bookkeeping.android.presentation.common.theme.Gray800 -import ac.dnd.bookkeeping.android.presentation.common.theme.Headline1 -import ac.dnd.bookkeeping.android.presentation.common.theme.Headline3 -import ac.dnd.bookkeeping.android.presentation.common.theme.Shapes -import ac.dnd.bookkeeping.android.presentation.common.theme.Space12 -import ac.dnd.bookkeeping.android.presentation.common.theme.Space20 -import ac.dnd.bookkeeping.android.presentation.common.theme.Space24 -import ac.dnd.bookkeeping.android.presentation.common.theme.Space4 -import ac.dnd.bookkeeping.android.presentation.common.theme.Space56 -import ac.dnd.bookkeeping.android.presentation.common.theme.Space8 -import ac.dnd.bookkeeping.android.presentation.common.theme.Space80 -import ac.dnd.bookkeeping.android.presentation.common.util.LaunchedEffectWithLifecycle -import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow -import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow -import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.eventObserve -import ac.dnd.bookkeeping.android.presentation.common.util.expansion.addFocusCleaner -import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipItem -import ac.dnd.bookkeeping.android.presentation.common.view.chip.ChipType -import ac.dnd.bookkeeping.android.presentation.common.view.component.FieldSelectComponent -import ac.dnd.bookkeeping.android.presentation.common.view.component.FieldSubject -import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButton -import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonProperties -import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonSize -import ac.dnd.bookkeeping.android.presentation.common.view.confirm.ConfirmButtonType -import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextField -import ac.dnd.bookkeeping.android.presentation.common.view.textfield.TypingTextFieldType -import ac.dnd.bookkeeping.android.presentation.ui.main.ApplicationState -import ac.dnd.bookkeeping.android.presentation.ui.main.rememberApplicationState -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -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.aspectRatio -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.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -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.draw.clip -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.CoroutineExceptionHandler - -@Composable -fun AddRelationScreen( - appState: ApplicationState, - model: AddRelationModel, - event: EventFlow, - intent: (AddRelationIntent) -> Unit, - handler: CoroutineExceptionHandler -) { - - val focusManager = LocalFocusManager.current - val isRecordState by remember { mutableStateOf(false) } - val isSaveState by remember { mutableStateOf(false) } - var isNameTypeChipType by remember { mutableStateOf(true) } - var currentNameText by remember { mutableStateOf("닉네임 입력 (15자 이내)") } - var kakaoNameText by remember { mutableStateOf("카카오에서 친구 선택") } - var isShowingKakaoPicker by remember { mutableStateOf(false) } - var currentGroupId by remember { mutableLongStateOf(-1) } - var memoText by remember { mutableStateOf("") } - - val recordButtonTextColorState = animateColorAsState( - targetValue = if (isRecordState) Gray700 else Gray000, - label = "" - ) - - Box( - modifier = Modifier - .fillMaxSize() - .background(Gray000) - .addFocusCleaner(focusManager) - ) { - Row( - modifier = Modifier - .align(Alignment.TopCenter) - .height(Space56) - .fillMaxWidth() - .padding( - horizontal = 20.dp, - vertical = 13.dp - ), - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painter = painterResource(R.drawable.ic_chevron_left), - contentDescription = null - ) - Spacer(modifier = Modifier.width(Space4)) - Text( - text = "관계 등록하기", - style = Headline1.merge( - color = Gray800, - fontWeight = FontWeight.SemiBold - ) - ) - } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = Space20) - .padding(top = 96.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(Space80), - contentAlignment = Alignment.Center - ) { - Box { - // TODO image - Box( - modifier = Modifier - .aspectRatio(1f) - .clip(CircleShape) - .background(Gray400) - ) - Image( - painter = painterResource(R.drawable.ic_camera), - contentDescription = null, - modifier = Modifier.align(Alignment.BottomEnd) - ) - } - } - Spacer(modifier = Modifier.height(60.dp)) - FieldSubject(subject = "이름") - Spacer(modifier = Modifier.height(Space12)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - ChipItem( - chipType = if (isNameTypeChipType) ChipType.MAIN else ChipType.BORDER, - chipText = "직접 입력", - currentSelectedId = setOf(if (isNameTypeChipType) 0 else 1), - chipId = 0, - onSelectChip = { - isNameTypeChipType = true - } - ) - ChipItem( - chipType = if (isNameTypeChipType) ChipType.BORDER else ChipType.MAIN, - chipText = "카카오톡으로 등록", - chipId = 1, - currentSelectedId = setOf(if (isNameTypeChipType) 0 else 1), - onSelectChip = { - isNameTypeChipType = false - } - ) - } - Spacer(modifier = Modifier.height(Space12)) - if (isNameTypeChipType) { - TypingTextField( - textType = TypingTextFieldType.Basic, - text = currentNameText, - onValueChange = { - currentNameText = it - } - ) - } else { - FieldSelectComponent( - text = kakaoNameText, - isSelected = isShowingKakaoPicker, - onClick = { - isShowingKakaoPicker = true - } - ) - } - Spacer(modifier = Modifier.height(Space24)) - FieldSubject(subject = "그룹") - Spacer(modifier = Modifier.height(6.dp)) - LazyRow( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - items(model.groups) { group -> - ChipItem( - chipType = ChipType.BORDER, - currentSelectedId = setOf(currentGroupId), - chipText = group.name, - onSelectChip = { - currentGroupId = it - }, - chipId = group.id - ) - } - } - Spacer(modifier = Modifier.height(Space12)) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(Shapes.medium) - .background(color = Gray200) - .clickable { - // TODO open edit - } - .padding( - horizontal = Space8, - vertical = 5.dp - ) - ) { - Image( - painter = painterResource(R.drawable.ic_edit), - contentDescription = null - ) - Spacer(modifier = Modifier.width(2.dp)) - Text( - text = "편집", - style = Caption2.merge( - color = Gray600, - fontWeight = FontWeight.Medium - ) - ) - } - Spacer(modifier = Modifier.height(Space24)) - FieldSubject( - subject = "메모", - isViewIcon = false - ) - Spacer( - modifier = Modifier.height(6.dp) - ) - TypingTextField( - text = memoText, - onValueChange = { - memoText = it - }, - textType = TypingTextFieldType.LongSentence - ) - } - - Row( - modifier = Modifier - .background(color = Gray000) - .align(Alignment.BottomCenter) - .padding( - vertical = Space12, - horizontal = Space20 - ), - horizontalArrangement = Arrangement.spacedBy(Space12) - ) { - ConfirmButton( - modifier = Modifier.weight(1f), - properties = ConfirmButtonProperties( - size = ConfirmButtonSize.Xlarge, - type = ConfirmButtonType.Secondary - ), - content = { - Text( - text = "바로 마음 기록", - style = Headline3.merge( - color = recordButtonTextColorState.value, - fontWeight = FontWeight.SemiBold - ) - ) - }, - isEnabled = isRecordState - ) - ConfirmButton( - modifier = Modifier.weight(1f), - properties = ConfirmButtonProperties( - size = ConfirmButtonSize.Xlarge, - type = ConfirmButtonType.Primary - ), - content = { - Text( - text = "저장하기", - style = Headline3.merge( - color = Gray000, - fontWeight = FontWeight.SemiBold - ) - ) - }, - isEnabled = isSaveState - ) - } - - } - - LaunchedEffectWithLifecycle(event, handler) { - event.eventObserve { event -> - // TODO event - } - } -} - -@Preview -@Composable -private fun AddRelationScreenPreview() { - AddRelationScreen( - appState = rememberApplicationState(), - model = AddRelationModel( - groups = listOf( - Group( - id = 0, - name = "친구" - ), - Group( - id = 0, - name = "가족" - ), - Group( - id = 0, - name = "지인" - ), - Group( - id = 0, - name = "직장" - ), - Group( - id = 0, - name = "label" - ) - ), - state = AddRelationState.Init - ), - intent = {}, - event = MutableEventFlow(), - handler = CoroutineExceptionHandler { _, _ -> } - ) -} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationState.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationState.kt deleted file mode 100644 index f8dca102..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -sealed interface AddRelationState { - data object Init : AddRelationState - data object Loading : AddRelationState -} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationViewModel.kt deleted file mode 100644 index 5b031f17..00000000 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/add/AddRelationViewModel.kt +++ /dev/null @@ -1,100 +0,0 @@ -package ac.dnd.bookkeeping.android.presentation.ui.main.home.common.relation.add - -import ac.dnd.bookkeeping.android.domain.model.error.ServerException -import ac.dnd.bookkeeping.android.domain.model.feature.group.Group -import ac.dnd.bookkeeping.android.domain.usecase.feature.group.GetGroupListUseCase -import ac.dnd.bookkeeping.android.domain.usecase.feature.relation.AddRelationUseCase -import ac.dnd.bookkeeping.android.presentation.common.base.BaseViewModel -import ac.dnd.bookkeeping.android.presentation.common.base.ErrorEvent -import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.EventFlow -import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.MutableEventFlow -import ac.dnd.bookkeeping.android.presentation.common.util.coroutine.event.asEventFlow -import androidx.lifecycle.SavedStateHandle -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject - -@HiltViewModel -class AddRelationViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, - private val getGroupListUseCase: GetGroupListUseCase, - private val addRelationUseCase: AddRelationUseCase -) : BaseViewModel() { - - private val _state: MutableStateFlow = - MutableStateFlow(AddRelationState.Init) - val state: StateFlow = _state.asStateFlow() - - private val _event: MutableEventFlow = MutableEventFlow() - val event: EventFlow = _event.asEventFlow() - - private val _groups: MutableStateFlow> = MutableStateFlow(emptyList()) - val groups: StateFlow> = _groups.asStateFlow() - - init { - launch { - _state.value = AddRelationState.Loading - getGroupListUseCase() - .onSuccess { - _groups.value = it - } - .onFailure { exception -> - when (exception) { - is ServerException -> { - _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) - } - - else -> { - _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) - } - } - } - } - } - - fun onIntent(intent: AddRelationIntent) { - when (intent) { - is AddRelationIntent.OnClickSubmit -> addRelation( - intent.groupId, - intent.name, - intent.imageUrl, - intent.memo - ) - } - } - - private fun addRelation( - groupId: Long, - name: String, - imageUrl: String, - memo: String, - ) { - launch { - _state.value = AddRelationState.Loading - addRelationUseCase( - groupId = groupId, - name = name, - imageUrl = imageUrl, - memo = memo - ) - .onSuccess { - _state.value = AddRelationState.Init - _event.emit(AddRelationEvent.Submit.Success) - } - .onFailure { exception -> - _state.value = AddRelationState.Init - when (exception) { - is ServerException -> { - _errorEvent.emit(ErrorEvent.InvalidRequest(exception)) - } - - else -> { - _errorEvent.emit(ErrorEvent.UnavailableServer(exception)) - } - } - } - } - } -} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationViewModel.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationViewModel.kt index 998ee115..c51aae54 100644 --- a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationViewModel.kt +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/ui/main/home/common/relation/get/GetRelationViewModel.kt @@ -28,7 +28,8 @@ class GetRelationViewModel @Inject constructor( private val _event: MutableEventFlow = MutableEventFlow() val event: EventFlow = _event.asEventFlow() - private val _groups: MutableStateFlow> = MutableStateFlow(emptyList()) + private val _groups: MutableStateFlow> = + MutableStateFlow(emptyList()) val groups: StateFlow> = _groups.asStateFlow() init {