diff --git a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt index e99e9d20..7cd51036 100644 --- a/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt +++ b/core/data/src/main/java/com/goalpanzi/mission_mate/core/data/repository/ProfileRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.goalpanzi.mission_mate.core.data.repository import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository import com.goalpanzi.mission_mate.core.network.service.ProfileService +import com.luckyoct.core.model.CharacterType import com.luckyoct.core.model.base.NetworkResult import com.luckyoct.core.model.request.SaveProfileRequest import javax.inject.Inject @@ -9,8 +10,8 @@ import javax.inject.Inject class ProfileRepositoryImpl @Inject constructor( private val profileService: ProfileService ): ProfileRepository { - override suspend fun saveProfile(nickname: String, index: Int): NetworkResult = handleResult { - val request = SaveProfileRequest.createRequest(nickname, index) + override suspend fun saveProfile(nickname: String, type: CharacterType): NetworkResult = handleResult { + val request = SaveProfileRequest.createRequest(nickname, type) profileService.saveProfile(request) } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt index 55400ac6..174add5e 100644 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/repository/ProfileRepository.kt @@ -1,8 +1,9 @@ package com.goalpanzi.mission_mate.core.domain.repository import com.goalpanzi.mission_mate.core.network.ResultHandler +import com.luckyoct.core.model.CharacterType import com.luckyoct.core.model.base.NetworkResult interface ProfileRepository: ResultHandler { - suspend fun saveProfile(nickname: String, index: Int): NetworkResult + suspend fun saveProfile(nickname: String, type: CharacterType): NetworkResult } \ No newline at end of file diff --git a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt index 89c23033..1e5791f2 100644 --- a/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt +++ b/core/domain/src/main/java/com/goalpanzi/mission_mate/core/domain/usecase/ProfileUseCase.kt @@ -1,10 +1,21 @@ package com.goalpanzi.mission_mate.core.domain.usecase +import com.goalpanzi.mission_mate.core.datastore.datasource.DefaultDataSource import com.goalpanzi.mission_mate.core.domain.repository.ProfileRepository +import com.luckyoct.core.model.CharacterType +import com.luckyoct.core.model.UserProfile +import com.luckyoct.core.model.base.NetworkResult +import kotlinx.coroutines.flow.first import javax.inject.Inject class ProfileUseCase @Inject constructor( - private val profileRepository: ProfileRepository + private val profileRepository: ProfileRepository, + private val defaultDataSource: DefaultDataSource ) { - suspend fun saveProfile(nickname: String, index: Int) = profileRepository.saveProfile(nickname, index) + suspend fun saveProfile(nickname: String, type: CharacterType) = profileRepository.saveProfile(nickname, type).also { + if (it is NetworkResult.Success) { + defaultDataSource.setUserProfile(UserProfile(nickname, type)).first() + } + } + suspend fun getProfile(): UserProfile? = defaultDataSource.getUserProfile().first() } \ No newline at end of file diff --git a/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt b/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt index 79592cc5..f4c69505 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/request/SaveProfileRequest.kt @@ -9,9 +9,9 @@ data class SaveProfileRequest( val characterType: String, ) { companion object { - fun createRequest(nickname: String, index: Int) = SaveProfileRequest( + fun createRequest(nickname: String, type: CharacterType) = SaveProfileRequest( nickname = nickname, - characterType = CharacterType.entries[index].name.uppercase() + characterType = type.name.uppercase() ) } } diff --git a/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt b/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt index 38c4ac36..4d7a2366 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/response/MissionBoardMembersResponse.kt @@ -1,6 +1,6 @@ package com.luckyoct.core.model.response -import com.luckyoct.core.model.request.CharacterType +import com.luckyoct.core.model.CharacterType import kotlinx.serialization.Serializable @Serializable diff --git a/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt b/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt index faf150ae..b1f849c3 100644 --- a/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt +++ b/core/model/src/main/java/com/luckyoct/core/model/response/MissionVerificationResponse.kt @@ -1,6 +1,6 @@ package com.luckyoct.core.model.response -import com.luckyoct.core.model.request.CharacterType +import com.luckyoct.core.model.CharacterType import kotlinx.serialization.Serializable @Serializable diff --git a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt index 2eb52c6f..0b159155 100644 --- a/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt +++ b/feature/board/src/main/java/com/goalpanzi/mission_mate/feature/board/model/Character.kt @@ -1,7 +1,7 @@ package com.goalpanzi.mission_mate.feature.board.model import androidx.annotation.DrawableRes -import com.luckyoct.core.model.request.CharacterType +import com.luckyoct.core.model.CharacterType enum class Character( @DrawableRes val imageId: Int, diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt index 561c1ed3..0741d032 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileNavigation.kt @@ -24,14 +24,14 @@ fun NavGraphBuilder.profileNavGraph( composable { ProfileRoute( profileSettingType = ProfileSettingType.CREATE, - onSaveSuccess = { onSaveSuccess() } + onSaveSuccess = onSaveSuccess ) } composable { ProfileRoute( profileSettingType = ProfileSettingType.SETTING, - onSaveSuccess = { onSaveSuccess() }, - onBackClick = { onBackClick() } + onSaveSuccess = onBackClick, + onBackClick = onBackClick ) } } \ No newline at end of file diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt index 3b33862e..2fb3c419 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileScreen.kt @@ -9,21 +9,24 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,6 +37,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.paint +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -42,17 +47,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextFieldGroup import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9 -import com.goalpanzi.mission_mate.core.designsystem.theme.ColorPink_FFFFE4E4 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography import com.goalpanzi.mission_mate.core.designsystem.theme.component.MissionMateTopAppBar import com.goalpanzi.mission_mate.core.designsystem.theme.component.NavigationType +import com.luckyoct.core.model.CharacterType import com.luckyoct.feature.profile.model.CharacterListItem -import com.luckyoct.feature.profile.model.ProfileEvent +import com.luckyoct.feature.profile.model.ProfileUiState import dagger.hilt.android.EntryPointAccessors import kotlinx.coroutines.flow.collectLatest @@ -74,46 +80,51 @@ fun ProfileRoute( onBackClick: (() -> Unit)? = null ) { val viewModel = profileViewModel(profileSettingType = profileSettingType) - val characters = viewModel.characters.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val isInvalidNickname by viewModel.isInvalidNickname.collectAsStateWithLifecycle() LaunchedEffect(true) { - viewModel.event.collectLatest { - when (it) { - ProfileEvent.Success -> onSaveSuccess() - else -> return@collectLatest - } + viewModel.isSaveSuccess.collectLatest { + if (it) onSaveSuccess() } } - ProfileScreen( - profileSettingType = profileSettingType, - characters = characters.value, - onclickCharacter = { viewModel.selectCharacter(it) }, - onClickSave = { viewModel.saveProfile(it) }, - onBackClick = { onBackClick?.invoke() } - ) + Box( + modifier = Modifier + .fillMaxSize() + .background(color = ColorWhite_FFFFFFFF) + .systemBarsPadding() + .navigationBarsPadding() + .imePadding() + ) { + ProfileContent( + modifier = modifier, + uiState = uiState, + profileSettingType = profileSettingType, + onClickCharacter = { viewModel.selectCharacter(it) }, + onClickSave = { viewModel.saveProfile(it) }, + onBackClick = onBackClick, + isInvalidNickname = isInvalidNickname, + resetNicknameErrorState = { viewModel.resetNicknameErrorState() } + ) + } } @Composable -fun ProfileScreen( +fun ProfileContent( modifier: Modifier = Modifier, + uiState: ProfileUiState, profileSettingType: ProfileSettingType, - characters: List, - onclickCharacter: (CharacterListItem) -> Unit, - onClickSave: (String) -> Unit, - onBackClick: (() -> Unit)? = null + onClickCharacter: (CharacterListItem) -> Unit = {}, + onClickSave: (String) -> Unit = {}, + onBackClick: (() -> Unit)? = null, + isInvalidNickname: Boolean, + resetNicknameErrorState: () -> Unit ) { - - var nicknameInput by remember { mutableStateOf("") } - val scrollState = rememberScrollState() - val regex = Regex("^[가-힣ㅏ-ㅣㄱ-ㅎa-zA-Z0-9]{1,6}$") - Column( modifier = modifier .fillMaxSize() .background(color = ColorWhite_FFFFFFFF) - .imePadding() - .statusBarsPadding() ) { if (profileSettingType == ProfileSettingType.SETTING) { MissionMateTopAppBar( @@ -122,74 +133,129 @@ fun ProfileScreen( onNavigationClick = { onBackClick?.invoke() } ) } - Column( - modifier = modifier - .padding(bottom = 18.dp) - .fillMaxWidth() - .weight(1f) - .verticalScroll(scrollState) - ) { - Text( - text = stringResource(id = when (profileSettingType) { - ProfileSettingType.CREATE -> R.string.profile_create - ProfileSettingType.SETTING -> R.string.profile_setting_title - }), - modifier = modifier - .align(Alignment.CenterHorizontally) - .padding(top = 48.dp), - style = MissionMateTypography.heading_sm_bold, - color = ColorGray1_FF404249 - ) + when (uiState) { + ProfileUiState.Loading -> { + ProfileLoading() + } - characters.find { it.isSelected }?.let { - Box( - modifier = modifier - .padding(top = 32.dp) - .size(220.dp) - .background(color = it.backgroundColor, shape = CircleShape) - .align(Alignment.CenterHorizontally) - ) { - CharacterLargeImage(imageResId = it.imageResId) - } + is ProfileUiState.Success -> { + ProfileScreen( + modifier = modifier, + profileSettingType = profileSettingType, + initialNickname = uiState.nickname, + characters = uiState.characterList, + onClickCharacter = onClickCharacter, + onClickSave = onClickSave, + isInvalidNickname = isInvalidNickname, + resetNicknameErrorState = resetNicknameErrorState + ) } - CharacterRow( - characters = characters, - onClick = onclickCharacter - ) + } + } +} - MissionMateTextFieldGroup( +@Composable +fun ColumnScope.ProfileScreen( + modifier: Modifier = Modifier, + initialNickname: String, + profileSettingType: ProfileSettingType, + characters: List, + onClickCharacter: (CharacterListItem) -> Unit, + onClickSave: (String) -> Unit, + isInvalidNickname: Boolean, + resetNicknameErrorState: () -> Unit = {} +) { + var nicknameInput by remember { mutableStateOf(initialNickname) } + val scrollState = rememberScrollState() + val regex = Regex("^[가-힣ㅏ-ㅣㄱ-ㅎa-zA-Z0-9]{1,6}$") + + Column( + modifier = modifier + .padding(bottom = 18.dp) + .weight(1f) + .fillMaxWidth() + .verticalScroll(scrollState) + .imePadding() + ) { + Text( + text = stringResource( + id = when (profileSettingType) { + ProfileSettingType.CREATE -> R.string.profile_create + ProfileSettingType.SETTING -> R.string.profile_setting_title + } + ), + modifier = modifier + .align(Alignment.CenterHorizontally) + .padding(top = 48.dp), + style = MissionMateTypography.heading_sm_bold, + color = ColorGray1_FF404249 + ) + + characters.find { it.isSelected }?.let { + Box( modifier = modifier - .padding(top = 38.dp, start = 24.dp, end = 24.dp) - .fillMaxWidth() - .wrapContentHeight(), - text = nicknameInput, - onValueChange = { if (regex.matches(it) || it.isEmpty()) nicknameInput = it }, - hintId = R.string.nickname_hint, - guidanceId = R.string.nickname_input_guide - ) + .padding(top = 32.dp) + .size(220.dp) + .align(Alignment.CenterHorizontally) + ) { + CharacterLargeImage( + imageResId = it.imageResId, + backgroundResId = it.backgroundResId + ) + } } + CharacterRow( + characters = characters, + onClick = onClickCharacter + ) - MissionMateTextButton( + MissionMateTextFieldGroup( modifier = modifier - .padding(bottom = 36.dp, start = 24.dp, end = 24.dp) - .fillMaxWidth(), - textId = R.string.save, - onClick = { onClickSave(nicknameInput) } + .padding(top = 38.dp, start = 24.dp, end = 24.dp) + .fillMaxWidth() + .wrapContentHeight(), + text = nicknameInput, + onValueChange = { + if (regex.matches(it) || it.isEmpty()) { + nicknameInput = it + } + resetNicknameErrorState() + }, + hintId = R.string.nickname_hint, + guidanceId = if (isInvalidNickname) R.string.err_duplicated_nickname else R.string.nickname_input_guide, + isError = isInvalidNickname ) } + + MissionMateTextButton( + modifier = modifier + .padding(bottom = 36.dp, start = 24.dp, end = 24.dp) + .fillMaxWidth(), + textId = R.string.save, + buttonType = if (nicknameInput.trim().isEmpty()) { + MissionMateButtonType.DISABLED + } else { + MissionMateButtonType.ACTIVE + }, + onClick = { onClickSave(nicknameInput) } + ) } @Composable fun CharacterLargeImage( modifier: Modifier = Modifier, @DrawableRes imageResId: Int, + @DrawableRes backgroundResId: Int, ) { Image( - modifier = modifier - .padding(10.dp) - .fillMaxSize(), painter = painterResource(id = imageResId), - contentDescription = "" + contentDescription = null, + modifier = modifier + .fillMaxSize() + .paint( + painter = painterResource(backgroundResId), + contentScale = ContentScale.FillWidth, + ) ) } @@ -199,11 +265,20 @@ fun CharacterRow( characters: List, onClick: (CharacterListItem) -> Unit ) { + val scrollState = rememberLazyListState() + + LaunchedEffect(key1 = characters) { + characters.indexOfFirst { it.isSelected }.takeIf { it > 0 }?.let { + scrollState.animateScrollToItem(it - 1) + } + } + LazyRow( modifier = modifier .padding(top = 18.dp), horizontalArrangement = Arrangement.spacedBy(space = 8.dp), - contentPadding = PaddingValues(horizontal = 24.dp) + contentPadding = PaddingValues(horizontal = 24.dp), + state = scrollState ) { items(items = characters, key = { it.imageResId }) { CharacterElement( @@ -248,39 +323,52 @@ fun CharacterElement( } } +@Composable +private fun ProfileLoading() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} + @Preview @Composable -fun ProfileScreenPreview() { +fun ColumnScope.ProfileScreenPreview() { ProfileScreen( profileSettingType = ProfileSettingType.CREATE, + initialNickname = "", characters = listOf( CharacterListItem( + type = CharacterType.RABBIT, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected, nameResId = R.string.rabbit_name, isSelected = true, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_rabbit ), CharacterListItem( + type = CharacterType.CAT, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, nameResId = R.string.cat_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat ), CharacterListItem( + type = CharacterType.DOG, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected, nameResId = R.string.dog_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_dog ), CharacterListItem( + type = CharacterType.PANDA, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected, nameResId = R.string.panda_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_panda ), ), - onclickCharacter = {}, - onClickSave = {} + onClickCharacter = {}, + onClickSave = {}, + isInvalidNickname = false ) } @@ -289,10 +377,11 @@ fun ProfileScreenPreview() { fun CharacterElementPreview() { CharacterElement( character = CharacterListItem( + type = CharacterType.CAT, imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, nameResId = R.string.cat_name, isSelected = false, - backgroundColor = ColorPink_FFFFE4E4 + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat ) ) } \ No newline at end of file diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt index 41ed8210..5298d4c1 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/ProfileViewModel.kt @@ -1,5 +1,6 @@ package com.luckyoct.feature.profile +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -10,9 +11,11 @@ import com.goalpanzi.mission_mate.core.designsystem.theme.ColorLightGreen_FFC2E7 import com.goalpanzi.mission_mate.core.designsystem.theme.ColorLightYellow_FFFFE59A import com.goalpanzi.mission_mate.core.designsystem.theme.ColorPink_FFFFE4E4 import com.goalpanzi.mission_mate.core.domain.usecase.ProfileUseCase +import com.luckyoct.core.model.CharacterType +import com.luckyoct.core.model.UserProfile import com.luckyoct.core.model.base.NetworkResult import com.luckyoct.feature.profile.model.CharacterListItem -import com.luckyoct.feature.profile.model.ProfileEvent +import com.luckyoct.feature.profile.model.ProfileUiState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -25,7 +28,7 @@ import kotlinx.coroutines.launch class ProfileViewModel @AssistedInject constructor( @Assisted private val profileSettingType: ProfileSettingType, private val profileUseCase: ProfileUseCase -): ViewModel() { +) : ViewModel() { @AssistedFactory interface Factory { @@ -44,77 +47,78 @@ class ProfileViewModel @AssistedInject constructor( } } - private val defaultImageIds = listOf( - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_selected, - com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_selected - ) - private val defaultNameIds = listOf( - R.string.rabbit_name, - R.string.cat_name, - R.string.dog_name, - R.string.panda_name, - R.string.bear_name, - R.string.bird_name - ) - private val defaultColors = listOf( - ColorPink_FFFFE4E4, - ColorBlue_FFBFD7FF, - ColorLightYellow_FFFFE59A, - ColorLightGreen_FFC2E792, - ColorLightBrown_FFF7D8B3, - ColorLightBlue_FFBCE7FF - ) + private val defaultCharacters = CharacterListItem.createDefaultList() - private val _event = MutableSharedFlow() - val event = _event.asSharedFlow() + private val _uiState = MutableStateFlow(ProfileUiState.Loading) + val uiState = _uiState.asStateFlow() - private val _characters = MutableStateFlow( - when (profileSettingType) { - ProfileSettingType.CREATE -> { - defaultImageIds.indices.map { - CharacterListItem( - imageResId = defaultImageIds[it], - nameResId = defaultNameIds[it], - isSelected = it == 0, - backgroundColor = defaultColors[it] + private val _isInvalidNickname = MutableStateFlow(false) + val isInvalidNickname = _isInvalidNickname.asStateFlow() + + private val _isSaveSuccess = MutableSharedFlow() + val isSaveSuccess = _isSaveSuccess.asSharedFlow() + + init { + viewModelScope.launch { + when (profileSettingType) { + ProfileSettingType.CREATE -> { + _uiState.value = ProfileUiState.Success( + nickname = "", + characterList = defaultCharacters.toMutableList().apply { + set(0, get(0).copy(isSelected = true)) + } ) } - } - // TODO : set my character selected - ProfileSettingType.SETTING -> { - defaultImageIds.indices.map { - CharacterListItem( - imageResId = defaultImageIds[it], - nameResId = defaultNameIds[it], - isSelected = false, - backgroundColor = defaultColors[it] + + ProfileSettingType.SETTING -> { + val userProfile = profileUseCase.getProfile() + ?: UserProfile( + nickname = "", + characterType = CharacterType.RABBIT + ) // TODO : API + _uiState.value = ProfileUiState.Success( + nickname = userProfile.nickname, + characterList = defaultCharacters.toMutableList().apply { + val index = indexOfFirst { it.type == userProfile.characterType } + set(index, get(index).copy(isSelected = true)) + } ) } } } - ) - val characters = _characters.asStateFlow() + } fun selectCharacter(character: CharacterListItem) { - _characters.value = _characters.value.map { - it.copy(isSelected = it == character) - } + val state = uiState.value as? ProfileUiState.Success ?: return + _uiState.value = state.copy( + characterList = state.characterList.map { + it.copy(isSelected = it == character) + } + ) + } + + fun resetNicknameErrorState() { + _isInvalidNickname.value = false } fun saveProfile(nickname: String) { if (nickname.isEmpty()) return - viewModelScope.launch { - val response = profileUseCase.saveProfile(nickname, characters.value.indexOfFirst { it.isSelected }) - when (response) { + val selectedItem = (uiState.value as? ProfileUiState.Success)?.characterList?.find { + it.isSelected + } ?: return@launch + + when(val response = profileUseCase.saveProfile(nickname, selectedItem.type)) { is NetworkResult.Success -> { - _event.emit(ProfileEvent.Success) + profileUseCase.saveProfile(nickname, selectedItem.type) + _isSaveSuccess.emit(true) + } + is NetworkResult.Exception -> {} + is NetworkResult.Error -> { + if (response.code == 409) { + _isInvalidNickname.emit(true) + } } - else -> return@launch } } } diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt index 824e9390..6b0cf1b9 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/CharacterListItem.kt @@ -2,11 +2,54 @@ package com.luckyoct.feature.profile.model import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.ui.graphics.Color +import com.luckyoct.core.model.CharacterType +import com.luckyoct.feature.profile.R data class CharacterListItem( + val type: CharacterType, @DrawableRes val imageResId: Int, @StringRes val nameResId: Int, - val isSelected: Boolean, - val backgroundColor: Color -) + val isSelected: Boolean = false, + @DrawableRes val backgroundResId: Int +) { + companion object { + fun createDefaultList() = listOf( + CharacterListItem( + type = CharacterType.RABBIT, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_rabbit_selected, + nameResId = R.string.rabbit_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_rabbit + ), + CharacterListItem( + type = CharacterType.CAT, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_cat_selected, + nameResId = R.string.cat_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_cat + ), + CharacterListItem( + type = CharacterType.DOG, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_dog_selected, + nameResId = R.string.dog_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_dog + ), + CharacterListItem( + type = CharacterType.PANDA, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_panda_selected, + nameResId = R.string.panda_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_panda + ), + CharacterListItem( + type = CharacterType.BEAR, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bear_selected, + nameResId = R.string.bear_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bear + ), + CharacterListItem( + type = CharacterType.BIRD, + imageResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.img_bird_selected, + nameResId = R.string.bird_name, + backgroundResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.background_bird + ) + ) + } +} diff --git a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt index 03f1b26a..3133f867 100644 --- a/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt +++ b/feature/profile/src/main/java/com/luckyoct/feature/profile/model/ProfileUiState.kt @@ -1,8 +1,11 @@ package com.luckyoct.feature.profile.model -sealed interface ProfileEvent { +sealed interface ProfileUiState { - data object Loading : ProfileEvent + data object Loading : ProfileUiState - data object Success : ProfileEvent + data class Success( + val nickname: String, + val characterList: List + ) : ProfileUiState } \ No newline at end of file diff --git a/feature/profile/src/main/res/values/strings.xml b/feature/profile/src/main/res/values/strings.xml index 4443427d..38f15500 100644 --- a/feature/profile/src/main/res/values/strings.xml +++ b/feature/profile/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ 닉네임 입력 1~6자, 한글, 영문 또는 숫자를 입력하세요. 저장하기 + 이미 존재하는 회원 닉네임이에요. 뚝심토끼 포기란없다냥