diff --git a/app/src/main/java/org/go/sopt/winey/presentation/main/feed/upload/UploadViewModel.kt b/app/src/main/java/org/go/sopt/winey/presentation/main/feed/upload/UploadViewModel.kt index a568e837..44a43f59 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/main/feed/upload/UploadViewModel.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/main/feed/upload/UploadViewModel.kt @@ -18,8 +18,8 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.go.sopt.winey.data.model.remote.response.ResponsePostWineyFeedDto import org.go.sopt.winey.domain.repository.FeedRepository import org.go.sopt.winey.presentation.model.WineyFeedType -import org.go.sopt.winey.util.state.ErrorCode import org.go.sopt.winey.util.multipart.UriToRequestBody +import org.go.sopt.winey.util.state.InputError import org.go.sopt.winey.util.state.InputUiState import org.go.sopt.winey.util.state.UiState import retrofit2.HttpException @@ -94,9 +94,11 @@ class UploadViewModel @Inject constructor( private fun checkInputUiState(content: String): InputUiState { if (content.isBlank()) return InputUiState.Empty + if (!checkContentLength((content))) { - return InputUiState.Failure(ErrorCode.CODE_INVALID_LENGTH) + return InputUiState.Failure(InputError.Upload) } + return InputUiState.Success } diff --git a/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameActivity.kt b/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameActivity.kt index 58a98594..a362d11f 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameActivity.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameActivity.kt @@ -19,15 +19,12 @@ import org.go.sopt.winey.domain.repository.DataStoreRepository import org.go.sopt.winey.presentation.main.MainActivity import org.go.sopt.winey.util.amplitude.AmplitudeUtils import org.go.sopt.winey.util.binding.BindingActivity -import org.go.sopt.winey.util.state.ErrorCode import org.go.sopt.winey.util.context.hideKeyboard import org.go.sopt.winey.util.context.snackBar import org.go.sopt.winey.util.context.stringOf -import org.go.sopt.winey.util.state.InputUiState import org.go.sopt.winey.util.state.UiState import org.json.JSONException import org.json.JSONObject -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -63,15 +60,12 @@ class NicknameActivity : BindingActivity(R.layout.activ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun afterTextChanged(s: Editable?) {} - // 텍스트가 바뀌면 중복체크 상태 false로 초기화 + // 텍스트가 변할 때마다 중복 체크 상태 갱신 override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { val inputText = s.toString() - - if (inputText.isNotBlank() && inputText != prevText) { + if (inputText != prevText) { viewModel.updateDuplicateCheckState(false) - Timber.d("DUPLICATE CHECK: ${viewModel.duplicateChecked.value}") } - prevText = inputText } }) @@ -80,7 +74,7 @@ class NicknameActivity : BindingActivity(R.layout.activ private fun initDuplicateCheckButtonClickListener() { binding.btnNicknameDuplicateCheck.setOnClickListener { viewModel.apply { - if (checkValidInput()) { + if (validateNickname()) { getNicknameDuplicateCheck() } } @@ -89,15 +83,13 @@ class NicknameActivity : BindingActivity(R.layout.activ private fun initCompleteButtonClickListener() { binding.btnNicknameComplete.setOnClickListener { - // 중복체크 하지 않고 시작하기 버튼 누르면 에러 표시 + if (!viewModel.validateNickname()) return@setOnClickListener + if (!viewModel.duplicateChecked.value) { - viewModel.updateInputUiState( - InputUiState.Failure(ErrorCode.CODE_UNCHECKED_DUPLICATION) - ) + viewModel.showUncheckedDuplicationError() return@setOnClickListener } - // 유효한 닉네임인 경우에만 PATCH 서버통신 진행 if (viewModel.isValidNickname.value) { sendEventToAmplitude() viewModel.patchNickname() diff --git a/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameViewModel.kt b/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameViewModel.kt index 85fa1e76..03a29e07 100644 --- a/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameViewModel.kt +++ b/app/src/main/java/org/go/sopt/winey/presentation/nickname/NicknameViewModel.kt @@ -14,10 +14,9 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.go.sopt.winey.data.model.remote.request.RequestPatchNicknameDto import org.go.sopt.winey.domain.repository.AuthRepository -import org.go.sopt.winey.util.state.ErrorCode +import org.go.sopt.winey.util.state.InputError import org.go.sopt.winey.util.state.InputUiState import org.go.sopt.winey.util.state.UiState -import retrofit2.HttpException import timber.log.Timber import javax.inject.Inject @@ -28,84 +27,79 @@ class NicknameViewModel @Inject constructor( val _nickname = MutableStateFlow("") private val nickname: String get() = _nickname.value - // Why MutableStateFlow -> map 이외의 함수에서도 값을 바꿀 수 있도록 - private val _inputUiState: MutableStateFlow = _nickname.map { checkInputUiState(it) } - .mutableStateIn( - initialValue = InputUiState.Empty, - scope = viewModelScope - ) + private val _inputUiState: MutableStateFlow = + _nickname.map { updateInputUiState(it) } + .mutableStateIn( + initialValue = InputUiState.Empty, + scope = viewModelScope + ) val inputUiState: StateFlow = _inputUiState.asStateFlow() - val isValidNickname: StateFlow = _inputUiState.map { validateNickname(it) } + val isValidNickname: StateFlow = _inputUiState.map { checkNicknameFinally(it) } .stateIn( initialValue = false, scope = viewModelScope, started = SharingStarted.WhileSubscribed(PRODUCE_STOP_TIMEOUT) ) - private fun validateNickname(state: InputUiState) = state == InputUiState.Success - private val _duplicateChecked = MutableStateFlow(false) - val duplicateChecked: StateFlow = _duplicateChecked.asStateFlow() + val duplicateChecked = _duplicateChecked.asStateFlow() private val _patchNicknameState = MutableStateFlow>(UiState.Empty) - val patchNicknameState: StateFlow> = _patchNicknameState.asStateFlow() + val patchNicknameState = _patchNicknameState.asStateFlow() var prevScreenName: String? = null - private fun checkInputUiState(nickname: String): InputUiState { - if (nickname.isBlank()) return InputUiState.Empty - if (!checkLength(nickname)) return InputUiState.Failure(ErrorCode.CODE_INVALID_LENGTH) - if (containsSpaceOrSpecialChar(nickname)) { - return InputUiState.Failure(ErrorCode.CODE_SPACE_SPECIAL_CHAR) + private fun updateInputUiState(nickname: String): InputUiState { + if (containsInvalidChar(nickname)) { + return InputUiState.Failure(InputError.Nickname.INVALID_CHAR) } - return InputUiState.Empty - } - private fun checkLength(nickname: String) = nickname.length in MIN_LENGTH..MAX_LENGTH - - private fun containsSpaceOrSpecialChar(nickname: String) = - !Regex(REGEX_PATTERN).matches(nickname) - - // 액티비티에서 전달 -> XML 바인딩 어댑터에 사용 - fun updatePrevScreenName(intentExtraValue: String?) { - prevScreenName = intentExtraValue + return InputUiState.Empty } - // 중복체크 하지 않고 시작하기 버튼 눌렀을 때 -> Failure 상태로 갱신 - fun updateInputUiState(inputUiState: InputUiState) { - _inputUiState.value = inputUiState - } + private fun containsInvalidChar(nickname: String) = !Regex(REGEX_PATTERN).matches(nickname) - // 액티비티, 뷰모델에서 갱신 - fun updateDuplicateCheckState(checked: Boolean) { - _duplicateChecked.value = checked - } + private fun checkNicknameFinally(state: InputUiState) = state == InputUiState.Success - fun checkValidInput(): Boolean { + fun validateNickname(): Boolean { if (nickname.isBlank()) { - _inputUiState.value = InputUiState.Failure(ErrorCode.CODE_BLANK_INPUT) + _inputUiState.value = InputUiState.Failure(InputError.Nickname.BLANK_INPUT) return false } - if (containsSpaceOrSpecialChar(nickname)) { - _inputUiState.value = InputUiState.Failure(ErrorCode.CODE_SPACE_SPECIAL_CHAR) + if (containsInvalidChar(nickname)) { + _inputUiState.value = InputUiState.Failure(InputError.Nickname.INVALID_CHAR) return false } return true } + fun updatePrevScreenName(intentExtraValue: String?) { + prevScreenName = intentExtraValue + } + + fun updateDuplicateCheckState(checked: Boolean) { + _duplicateChecked.value = checked + } + + fun showUncheckedDuplicationError() { + _inputUiState.value = InputUiState.Failure(InputError.Nickname.UNCHECKED_DUPLICATION) + } + fun getNicknameDuplicateCheck() { viewModelScope.launch { authRepository.getNicknameDuplicateCheck(nickname) .onSuccess { response -> - if (response == null) return@onSuccess - showDuplicateCheckResult(response.isDuplicated) + if (response == null) { + Timber.e("닉네임 중복체크 응답값 null") + return@launch + } updateDuplicateCheckState(true) - Timber.d("DUPLICATE CHECK: ${duplicateChecked.value}") + showDuplicateCheckResult(response.isDuplicated) } .onFailure { t -> Timber.e("${t.message}") @@ -115,7 +109,7 @@ class NicknameViewModel @Inject constructor( private fun showDuplicateCheckResult(isDuplicated: Boolean) { _inputUiState.value = if (isDuplicated) { - InputUiState.Failure(ErrorCode.CODE_DUPLICATED) + InputUiState.Failure(InputError.Nickname.DUPLICATED) } else { InputUiState.Success } @@ -127,21 +121,14 @@ class NicknameViewModel @Inject constructor( authRepository.patchNickname(RequestPatchNicknameDto(nickname)) .onSuccess { response -> - Timber.d("SUCCESS PATCH NICKNAME") _patchNicknameState.value = UiState.Success(response) } .onFailure { t -> - if (t is HttpException) { - Timber.e("HTTP FAIL PATCH NICKNAME: ${t.code()} ${t.message}") - return@onFailure - } - Timber.e("FAIL PATCH NICKNAME: ${t.message}") _patchNicknameState.value = UiState.Failure(t.message.toString()) } } } - // _nickname.map{} Flow -> MutableStateFlow 변환을 위한 확장 함수 private fun Flow.mutableStateIn( initialValue: T, scope: CoroutineScope @@ -154,7 +141,6 @@ class NicknameViewModel @Inject constructor( } companion object { - private const val MIN_LENGTH = 1 const val MAX_LENGTH = 8 private const val REGEX_PATTERN = "^[0-9|a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]*$" private const val PRODUCE_STOP_TIMEOUT = 5000L diff --git a/app/src/main/java/org/go/sopt/winey/util/binding/BindingAdapter.kt b/app/src/main/java/org/go/sopt/winey/util/binding/BindingAdapter.kt index 2c493a60..a9e229f7 100644 --- a/app/src/main/java/org/go/sopt/winey/util/binding/BindingAdapter.kt +++ b/app/src/main/java/org/go/sopt/winey/util/binding/BindingAdapter.kt @@ -18,11 +18,11 @@ import org.go.sopt.winey.presentation.model.UserLevel import org.go.sopt.winey.presentation.model.WineyFeedType import org.go.sopt.winey.presentation.nickname.NicknameActivity.Companion.MY_PAGE_SCREEN import org.go.sopt.winey.presentation.nickname.NicknameActivity.Companion.STORY_SCREEN -import org.go.sopt.winey.util.state.ErrorCode.* import org.go.sopt.winey.util.context.colorOf import org.go.sopt.winey.util.context.drawableOf import org.go.sopt.winey.util.context.stringOf import org.go.sopt.winey.util.number.formatAmountNumber +import org.go.sopt.winey.util.state.InputError import org.go.sopt.winey.util.state.InputUiState import org.go.sopt.winey.util.state.InputUiState.* import java.text.DecimalFormat @@ -122,7 +122,7 @@ fun TextView.setUploadContentHelperText(inputUiState: InputUiState) { if (inputUiState is Failure) { visibility = View.VISIBLE - if (inputUiState.code == CODE_INVALID_LENGTH) { + if (inputUiState.error == InputError.Upload) { text = context.stringOf(R.string.upload_content_error_text) } } @@ -167,12 +167,12 @@ fun TextView.setNicknameHelperText(inputUiState: InputUiState) { is Failure -> { visibility = View.VISIBLE - text = when (inputUiState.code) { - CODE_BLANK_INPUT -> context.stringOf(R.string.nickname_blank_input_error) - CODE_INVALID_LENGTH -> context.stringOf(R.string.nickname_invalid_length_error) - CODE_SPACE_SPECIAL_CHAR -> context.stringOf(R.string.nickname_space_special_char_error) - CODE_UNCHECKED_DUPLICATION -> context.stringOf(R.string.nickname_unchecked_duplication_error) - CODE_DUPLICATED -> context.stringOf(R.string.nickname_duplicated_error) + text = when (inputUiState.error) { + InputError.Nickname.BLANK_INPUT -> context.stringOf(R.string.nickname_blank_input_error) + InputError.Nickname.INVALID_CHAR -> context.stringOf(R.string.nickname_space_special_char_error) + InputError.Nickname.UNCHECKED_DUPLICATION -> context.stringOf(R.string.nickname_unchecked_duplication_error) + InputError.Nickname.DUPLICATED -> context.stringOf(R.string.nickname_duplicated_error) + else -> "" } } } diff --git a/app/src/main/java/org/go/sopt/winey/util/state/ErrorCode.kt b/app/src/main/java/org/go/sopt/winey/util/state/ErrorCode.kt deleted file mode 100644 index e5f3d2e5..00000000 --- a/app/src/main/java/org/go/sopt/winey/util/state/ErrorCode.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.go.sopt.winey.util.state - -enum class ErrorCode { - CODE_BLANK_INPUT, - CODE_SPACE_SPECIAL_CHAR, - CODE_UNCHECKED_DUPLICATION, - CODE_DUPLICATED, - CODE_INVALID_LENGTH -} diff --git a/app/src/main/java/org/go/sopt/winey/util/state/InputError.kt b/app/src/main/java/org/go/sopt/winey/util/state/InputError.kt new file mode 100644 index 00000000..e5ca30a8 --- /dev/null +++ b/app/src/main/java/org/go/sopt/winey/util/state/InputError.kt @@ -0,0 +1,12 @@ +package org.go.sopt.winey.util.state + +sealed interface InputError { + enum class Nickname : InputError { + BLANK_INPUT, + INVALID_CHAR, + UNCHECKED_DUPLICATION, + DUPLICATED + } + + object Upload : InputError +} diff --git a/app/src/main/java/org/go/sopt/winey/util/state/InputUiState.kt b/app/src/main/java/org/go/sopt/winey/util/state/InputUiState.kt index 2352a460..8c10107a 100644 --- a/app/src/main/java/org/go/sopt/winey/util/state/InputUiState.kt +++ b/app/src/main/java/org/go/sopt/winey/util/state/InputUiState.kt @@ -3,5 +3,5 @@ package org.go.sopt.winey.util.state sealed class InputUiState { object Empty : InputUiState() object Success : InputUiState() - data class Failure(val code: ErrorCode) : InputUiState() + data class Failure(val error: InputError) : InputUiState() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d152dce0..aa7c3644 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,7 +229,6 @@ 시작하기 입력값이 비어있습니다 :( - 1~8자로 입력해주세요 :( 공백과 특수문자는 사용할 수 없습니다 :( 닉네임 중복확인을 해주세요 :( 중복된 닉네임입니다 :(