From 40fbcf308db8d5b1864176ea614664e1a3cad762 Mon Sep 17 00:00:00 2001 From: GunHyung Ham <54674781+ham2174@users.noreply.github.com> Date: Wed, 14 Feb 2024 02:15:33 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EC=9E=91=20?= =?UTF-8?q?(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] : 프로필 UI 모델 생성 * [feat] : 프로필 생성 기능 제작 * [feat] : 프로필 생성 버튼 상태 프로필 UiModel에서 관리 * [refactor] : 기존 뷰모델 profile 상태 제거 및 uiState로 통합 --- .../com/moya/funch/CreatePofileViewModel.kt | 141 ++++++++++-------- .../com/moya/funch/CreateProflieScreen.kt | 98 ++++++++---- .../com/moya/funch/uimodel/ProfileUiModel.kt | 23 +++ 3 files changed, 174 insertions(+), 88 deletions(-) create mode 100644 feature/profile/src/main/java/com/moya/funch/uimodel/ProfileUiModel.kt diff --git a/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt b/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt index c84be10d..3e9d355b 100644 --- a/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt +++ b/feature/profile/src/main/java/com/moya/funch/CreatePofileViewModel.kt @@ -9,105 +9,126 @@ import com.moya.funch.entity.Mbti import com.moya.funch.entity.SubwayStation import com.moya.funch.entity.profile.Profile import com.moya.funch.uimodel.MbtiItem +import com.moya.funch.uimodel.ProfileUiModel +import com.moya.funch.usecase.CreateUserProfileUseCase import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -data class MbtiState( - val eOrI: MbtiItem = MbtiItem.E, - val nOrS: MbtiItem = MbtiItem.N, - val tOrF: MbtiItem = MbtiItem.T, - val jOrP: MbtiItem = MbtiItem.J +data class CreateProfileUiState( + val profile: ProfileUiModel = ProfileUiModel(), + val isLoading: Boolean = false ) +internal sealed class CreateProfileEvent { + data object NavigateToHome : CreateProfileEvent() + data class ShowError(val message: String) : CreateProfileEvent() +} + @HiltViewModel internal class CreateProfileViewModel @Inject constructor( - // private val createUserProfileUseCase: CreateUserProfileUseCase + private val createUserProfileUseCase: CreateUserProfileUseCase ) : ViewModel() { + private val _uiState = MutableStateFlow(CreateProfileUiState()) + val uiState: StateFlow = _uiState.asStateFlow() - private val _profile = MutableStateFlow(Profile()) - val profile = _profile.asStateFlow() - - private val mbtiState = MutableStateFlow(MbtiState()) + private val _event = MutableSharedFlow() + val event = _event.asSharedFlow() fun setNickname(nickname: String) { - _profile.value = _profile.value.copy(name = nickname) + _uiState.value = _uiState.value.copy( + profile = _uiState.value.profile.copy( + name = nickname + ) + ) } fun setJob(job: Job) { - _profile.value = _profile.value.copy(job = job) + _uiState.value = _uiState.value.copy( + profile = _uiState.value.profile.copy( + job = job + ) + ) } fun setClub(club: Club) { - _profile.value = _profile.value.copy( - clubs = _profile.value.clubs.toggleElement(club) + _uiState.value = _uiState.value.copy( + profile = _uiState.value.profile.copy( + clubs = _uiState.value.profile.clubs.toggleElement(club) + ) ) } fun setBloodType(blood: Blood) { - _profile.value = _profile.value.copy(blood = blood) + _uiState.value = _uiState.value.copy( + profile = _uiState.value.profile.copy( + bloodType = blood + ) + ) } fun setMbti(item: MbtiItem) { viewModelScope.launch { when (item) { - MbtiItem.E, MbtiItem.I -> mbtiState.update { uiModel -> uiModel.copy(eOrI = item) } - MbtiItem.N, MbtiItem.S -> mbtiState.update { uiModel -> uiModel.copy(nOrS = item) } - MbtiItem.T, MbtiItem.F -> mbtiState.update { uiModel -> uiModel.copy(tOrF = item) } - MbtiItem.J, MbtiItem.P -> mbtiState.update { uiModel -> uiModel.copy(jOrP = item) } - } - _profile.value = _profile.value.copy( - mbti = Mbti.valueOf( - mbtiState.value.eOrI.name + - mbtiState.value.nOrS.name + - mbtiState.value.tOrF.name + - mbtiState.value.jOrP.name - ) - ) - } - } + MbtiItem.E, MbtiItem.I -> _uiState.update { uiModel -> + uiModel.copy(profile = uiModel.profile.copy(eOrI = item)) + } + + MbtiItem.N, MbtiItem.S -> _uiState.update { uiModel -> + uiModel.copy(profile = uiModel.profile.copy(nOrS = item)) + } + + MbtiItem.T, MbtiItem.F -> _uiState.update { uiModel -> + uiModel.copy(profile = uiModel.profile.copy(tOrF = item)) + } - fun isSelectMbti(mbtiItem: MbtiItem): Boolean { - return when (mbtiItem) { - MbtiItem.E -> mbtiState.value.eOrI == MbtiItem.E - MbtiItem.I -> mbtiState.value.eOrI == MbtiItem.I - MbtiItem.N -> mbtiState.value.nOrS == MbtiItem.N - MbtiItem.S -> mbtiState.value.nOrS == MbtiItem.S - MbtiItem.T -> mbtiState.value.tOrF == MbtiItem.T - MbtiItem.F -> mbtiState.value.tOrF == MbtiItem.F - MbtiItem.J -> mbtiState.value.jOrP == MbtiItem.J - MbtiItem.P -> mbtiState.value.jOrP == MbtiItem.P + MbtiItem.J, MbtiItem.P -> _uiState.update { uiModel -> + uiModel.copy(profile = uiModel.profile.copy(jOrP = item)) + } + } } } fun setSubwayName(subway: String) { - _profile.value = _profile.value.copy( - subways = - listOf( - SubwayStation(name = subway) + _uiState.value = _uiState.value.copy( + profile = _uiState.value.profile.copy( + subway = subway ) ) } - fun isCreateProfile(profile: Profile): Boolean { - return profile.job != Job.IDLE && - profile.clubs.isNotEmpty() && - profile.mbti != Mbti.IDLE && - profile.blood != Blood.IDLE && - profile.name.isNotBlank() && - profile.subways[0].name.isNotBlank() - } - fun createProfile() { viewModelScope.launch { - /*createUserProfileUseCase(_profile.value).onSuccess { - // TODO : navigate to main + _uiState.update { currentState -> currentState.copy(isLoading = true) } + val profile = Profile( + name = _uiState.value.profile.name, + job = _uiState.value.profile.job, + clubs = _uiState.value.profile.clubs, + mbti = Mbti.valueOf( + _uiState.value.profile.eOrI.name + + _uiState.value.profile.nOrS.name + + _uiState.value.profile.tOrF.name + + _uiState.value.profile.jOrP.name + ), + blood = _uiState.value.profile.bloodType, + subways = listOf( + SubwayStation( + name = _uiState.value.profile.subway + ) + ) + ) + createUserProfileUseCase(profile).onSuccess { + _event.emit(CreateProfileEvent.NavigateToHome) }.onFailure { - // TODO : show error - }*/ + _uiState.update { currentState -> currentState.copy(isLoading = false) } + _event.emit(CreateProfileEvent.ShowError(it.message ?: "Error")) + } } } } @@ -119,9 +140,3 @@ private fun List.toggleElement(element: T): List { this + element } } - -internal sealed class CreateProfileState { - data object Loading : CreateProfileState() - data object Success : CreateProfileState() - data object Error : CreateProfileState() -} diff --git a/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt b/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt index 4e92734c..8a5234a1 100644 --- a/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt +++ b/feature/profile/src/main/java/com/moya/funch/CreateProflieScreen.kt @@ -3,6 +3,7 @@ package com.moya.funch import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement @@ -35,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalFocusManager @@ -57,7 +59,6 @@ import com.moya.funch.component.FunchSmallLabel import com.moya.funch.entity.Blood import com.moya.funch.entity.Club import com.moya.funch.entity.Job -import com.moya.funch.entity.profile.Profile import com.moya.funch.icon.FunchIconAsset import com.moya.funch.profile.R import com.moya.funch.theme.FunchTheme @@ -73,43 +74,71 @@ import com.moya.funch.ui.FunchDropDownMenu import com.moya.funch.ui.FunchTopBar import com.moya.funch.uimodel.MbtiItem import com.moya.funch.uimodel.ProfileLabel +import com.moya.funch.uimodel.ProfileUiModel @Composable internal fun CreateProfileRoute(onNavigateToHome: () -> Unit, viewModel: CreateProfileViewModel = hiltViewModel()) { - val profile by viewModel.profile.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.event.collect { event -> + when (event) { + is CreateProfileEvent.NavigateToHome -> { + onNavigateToHome() + } + is CreateProfileEvent.ShowError -> { + // @Gun Hyung TODO : 에러 메시지 호출 + } + } + } + } CreateProfileScreen( - profile = profile, - isSelectMbti = viewModel::isSelectMbti, - isCreateProfile = viewModel::isCreateProfile, + profile = uiState.profile, + isCreateProfile = uiState.profile.isButtonEnabled, onSelectJob = viewModel::setJob, onSelectClub = viewModel::setClub, onSelectMbti = viewModel::setMbti, onSelectBloodType = viewModel::setBloodType, onNicknameChange = viewModel::setNickname, onSubwayStationChange = viewModel::setSubwayName, - onNavigateToHome = onNavigateToHome, + onCreateProfile = viewModel::createProfile, onSendFeedback = {} ) + + if (uiState.isLoading) { + Box( + modifier = Modifier + .fillMaxSize() + .clickable( + onClick = { }, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ), + contentAlignment = Alignment.Center + ) { + // @Gun Hyung TODO : 로딩 UI 디자인시스템에 정의하고 그리기 + } + } } @Composable fun CreateProfileScreen( - profile: Profile, - isSelectMbti: (MbtiItem) -> Boolean, - isCreateProfile: (Profile) -> Boolean, + profile: ProfileUiModel, + isCreateProfile: Boolean, onSelectJob: (Job) -> Unit, onSelectClub: (Club) -> Unit, onSelectMbti: (MbtiItem) -> Unit, onSelectBloodType: (Blood) -> Unit, onNicknameChange: (String) -> Unit, onSubwayStationChange: (String) -> Unit, - onNavigateToHome: () -> Unit, + onCreateProfile: () -> Unit, onSendFeedback: () -> Unit ) { val scrollState = rememberScrollState() val backgroundColor = LocalBackgroundTheme.current.color var isKeyboardVisible by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current Scaffold( topBar = { @@ -123,8 +152,8 @@ fun CreateProfileScreen( if (!isKeyboardVisible) { BottomBar( backgroundColor = backgroundColor, - isCreateProfile = isCreateProfile(profile), - onNavigateToHome = onNavigateToHome + isCreateProfile = isCreateProfile, + onCreateProfile = onCreateProfile ) } }, @@ -132,6 +161,11 @@ fun CreateProfileScreen( ) { padding -> Column( modifier = Modifier + .pointerInput(Unit) { + detectTapGestures(onTap = { + focusManager.clearFocus() + }) + } .fillMaxSize() .verticalScroll(state = scrollState) .padding(padding) @@ -161,10 +195,10 @@ fun CreateProfileScreen( ) { JobRow(profile = profile, onSelected = onSelectJob) ClubRow(onSelectClub = onSelectClub) - MbtiRow(onSelectMbti = onSelectMbti, isSelectMbti = isSelectMbti) + MbtiRow(profile = profile, onSelectMbti = onSelectMbti) BooldTypeRow(onSelectBloodType = onSelectBloodType) SubwayRow( - subwayStation = profile.subways[0].name, + subwayStation = profile.subway, onSubwayStationChange = onSubwayStationChange, isKeyboardVisible = { isKeyboardVisible = it } ) @@ -174,8 +208,8 @@ fun CreateProfileScreen( if (isKeyboardVisible) { BottomBar( backgroundColor = backgroundColor, - isCreateProfile = isCreateProfile(profile), - onNavigateToHome = onNavigateToHome + isCreateProfile = isCreateProfile, + onCreateProfile = onCreateProfile ) } } @@ -232,7 +266,7 @@ private fun NicknameRow(nickname: String, onNicknameChange: (String) -> Unit, is @OptIn(ExperimentalLayoutApi::class) @Composable -private fun JobRow(profile: Profile, onSelected: (Job) -> Unit) { +private fun JobRow(profile: ProfileUiModel, onSelected: (Job) -> Unit) { Row { FunchSmallLabel(text = ProfileLabel.JOB.labelName) FlowRow( @@ -327,14 +361,20 @@ private fun ClubRow(onSelectClub: (Club) -> Unit) { } @Composable -private fun MbtiRow(onSelectMbti: (MbtiItem) -> Unit, isSelectMbti: (MbtiItem) -> Boolean) { +private fun MbtiRow(profile: ProfileUiModel, onSelectMbti: (MbtiItem) -> Unit) { + val eOrI = profile.eOrI + val nOrS = profile.nOrS + val tOrF = profile.tOrF + val jOrP = profile.jOrP + val currentMbti = listOf(eOrI, nOrS, tOrF, jOrP) + Row { FunchSmallLabel(text = ProfileLabel.MBTI.labelName) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - MbtiItem.entries.chunked(2).forEach { pair -> + MbtiItem.entries.chunked(2).forEachIndexed { i, pair -> Column( modifier = Modifier .background(color = Gray800, shape = FunchTheme.shapes.medium) @@ -343,7 +383,7 @@ private fun MbtiRow(onSelectMbti: (MbtiItem) -> Unit, isSelectMbti: (MbtiItem) - pair.forEach { mbti -> MbtiButton( mbtiItem = mbti, - isSelected = isSelectMbti(mbti), + isSelected = currentMbti[i] == mbti, onSelected = { onSelectMbti(it) } @@ -457,7 +497,7 @@ private fun SubwayRow( } @Composable -private fun BottomBar(backgroundColor: Color, isCreateProfile: Boolean, onNavigateToHome: () -> Unit) { +private fun BottomBar(backgroundColor: Color, isCreateProfile: Boolean, onCreateProfile: () -> Unit) { Box( modifier = Modifier .background(color = backgroundColor) @@ -472,7 +512,7 @@ private fun BottomBar(backgroundColor: Color, isCreateProfile: Boolean, onNaviga enabled = isCreateProfile, modifier = Modifier.fillMaxWidth(), buttonType = FunchButtonType.Full, - onClick = onNavigateToHome, + onClick = onCreateProfile, text = stringResource(id = R.string.bottom_button_title) ) } @@ -493,9 +533,17 @@ private fun Preview1() { modifier = Modifier.fillMaxSize(), color = backgroundColor ) { - CreateProfileRoute( - onNavigateToHome = {}, - viewModel = CreateProfileViewModel() + CreateProfileScreen( + profile = ProfileUiModel(), + isCreateProfile = false, + onSelectJob = {}, + onSelectClub = {}, + onSelectMbti = {}, + onSelectBloodType = {}, + onNicknameChange = {}, + onSubwayStationChange = {}, + onCreateProfile = {}, + onSendFeedback = {} ) } } diff --git a/feature/profile/src/main/java/com/moya/funch/uimodel/ProfileUiModel.kt b/feature/profile/src/main/java/com/moya/funch/uimodel/ProfileUiModel.kt new file mode 100644 index 00000000..abfa8408 --- /dev/null +++ b/feature/profile/src/main/java/com/moya/funch/uimodel/ProfileUiModel.kt @@ -0,0 +1,23 @@ +package com.moya.funch.uimodel + +import com.moya.funch.entity.Blood +import com.moya.funch.entity.Club +import com.moya.funch.entity.Job + +data class ProfileUiModel( + val name: String = "", + val job: Job = Job.IDLE, + val clubs: List = emptyList(), + val eOrI: MbtiItem = MbtiItem.E, + val nOrS: MbtiItem = MbtiItem.N, + val tOrF: MbtiItem = MbtiItem.T, + val jOrP: MbtiItem = MbtiItem.J, + val bloodType: Blood = Blood.A, + val subway: String = "" +) { + val isButtonEnabled: Boolean + get() = name.isNotBlank() && + job != Job.IDLE && + clubs.isNotEmpty() && + subway.isNotBlank() +}