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 f380e984..b00ce039 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 @@ -46,7 +46,8 @@ class UploadViewModel @Inject constructor( /** Content Fragment */ val _content = MutableStateFlow("") val content: String get() = _content.value - val inputUiState: StateFlow = _content.map { updateInputUiState(it) } + + val inputUiState: StateFlow = _content.map { checkInputUiState(it) } .stateIn( initialValue = InputUiState.Empty, scope = viewModelScope, @@ -62,7 +63,7 @@ class UploadViewModel @Inject constructor( private fun validateContent(state: InputUiState) = state == InputUiState.Success - private fun updateInputUiState(content: String): InputUiState { + private fun checkInputUiState(content: String): InputUiState { if (content.isBlank()) return InputUiState.Empty if (!checkContentLength((content))) { return InputUiState.Failure(ErrorCode.CODE_INVALID_LENGTH) 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 112a0707..233bba9e 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 @@ -27,6 +27,7 @@ import org.go.sopt.winey.util.view.InputUiState import org.go.sopt.winey.util.view.UiState import org.json.JSONException import org.json.JSONObject +import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -43,7 +44,6 @@ class NicknameActivity : BindingActivity(R.layout.activ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) amplitudeUtils.logEvent("view_set_nickname") - binding.vm = viewModel viewModel.updatePrevScreenName(prevScreenName) @@ -57,18 +57,6 @@ class NicknameActivity : BindingActivity(R.layout.activ initPatchNicknameStateObserver() } - private fun sendEventToAmplitude() { - val eventProperties = JSONObject() - try { - eventProperties.put("button_name", "nickname_next_button") - .put("paging_number", 1) - } catch (e: JSONException) { - System.err.println("Invalid JSON") - e.printStackTrace() - } - amplitudeUtils.logEvent("click_button", eventProperties) - } - private fun initEditTextWatcher() { var prevText = "" binding.etNickname.addTextChangedListener(object : TextWatcher { @@ -78,9 +66,12 @@ class NicknameActivity : BindingActivity(R.layout.activ // 텍스트가 바뀌면 중복체크 상태 false로 초기화 override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { val inputText = s.toString() + if (inputText.isNotBlank() && inputText != prevText) { viewModel.updateDuplicateCheckState(false) + Timber.d("DUPLICATE CHECK: ${viewModel.duplicateChecked.value}") } + prevText = inputText } }) @@ -88,21 +79,25 @@ class NicknameActivity : BindingActivity(R.layout.activ private fun initDuplicateCheckButtonClickListener() { binding.btnNicknameDuplicateCheck.setOnClickListener { - viewModel.getNicknameDuplicateCheck() + viewModel.apply { + if (checkValidInput()) { + getNicknameDuplicateCheck() + } + } } } private fun initCompleteButtonClickListener() { binding.btnNicknameComplete.setOnClickListener { - // 중복체크를 하지 않은 상태에서 완료 버튼을 클릭하면 에러 표시 - if (!viewModel.isDuplicateChecked.value) { + // 중복체크 하지 않고 시작하기 버튼 누르면 에러 표시 + if (!viewModel.duplicateChecked.value) { viewModel.updateInputUiState( InputUiState.Failure(ErrorCode.CODE_UNCHECKED_DUPLICATION) ) return@setOnClickListener } - // 서버통신 결과 중복되지 않은 닉네임인 경우에만 PATCH 서버통신 진행 + // 유효한 닉네임인 경우에만 PATCH 서버통신 진행 if (viewModel.isValidNickname.value) { sendEventToAmplitude() viewModel.patchNickname() @@ -133,13 +128,13 @@ class NicknameActivity : BindingActivity(R.layout.activ } private fun switchEditTextHint() { - lifecycleScope.launch { - when (prevScreenName) { - STORY_SCREEN -> { - binding.etNickname.hint = stringOf(R.string.nickname_default_hint) - } + when (prevScreenName) { + STORY_SCREEN -> { + binding.etNickname.hint = stringOf(R.string.nickname_default_hint) + } - MY_PAGE_SCREEN -> { + MY_PAGE_SCREEN -> { + lifecycleScope.launch { val user = dataStoreRepository.getUserInfo().first() ?: return@launch binding.etNickname.hint = user.nickname binding.originalNicknameLength = user.nickname.length @@ -168,6 +163,18 @@ class NicknameActivity : BindingActivity(R.layout.activ } } + private fun sendEventToAmplitude() { + val eventProperties = JSONObject() + try { + eventProperties.put("button_name", "nickname_next_button") + .put("paging_number", 1) + } catch (e: JSONException) { + System.err.println("Invalid JSON") + e.printStackTrace() + } + amplitudeUtils.logEvent("click_button", eventProperties) + } + companion object { private const val EXTRA_KEY = "PREV_SCREEN_NAME" const val MY_PAGE_SCREEN = "MyPageFragment" 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 d577d4ae..b2162b12 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 @@ -26,14 +26,15 @@ class NicknameViewModel @Inject constructor( private val authRepository: AuthRepository ) : ViewModel() { val _nickname = MutableStateFlow("") - val nickname: String get() = _nickname.value - - private val _inputUiState: MutableStateFlow = - _nickname.map { checkInputUiState(it) } - .mutableStateIn( - initialValue = InputUiState.Empty, - scope = viewModelScope - ) + private val nickname: String get() = _nickname.value + + // Why MutableStateFlow -> map 이외의 함수에서도 값을 바꿀 수 있도록 + private val _inputUiState: MutableStateFlow = _nickname.map { checkInputUiState(it) } + .mutableStateIn( + initialValue = InputUiState.Empty, + scope = viewModelScope + ) + val inputUiState: StateFlow = _inputUiState.asStateFlow() val isValidNickname: StateFlow = _inputUiState.map { validateNickname(it) } @@ -42,48 +43,58 @@ class NicknameViewModel @Inject constructor( scope = viewModelScope, started = SharingStarted.WhileSubscribed(PRODUCE_STOP_TIMEOUT) ) + private fun validateNickname(state: InputUiState) = state == InputUiState.Success - private val _isDuplicateChecked = MutableStateFlow(false) - val isDuplicateChecked: StateFlow = _isDuplicateChecked.asStateFlow() + private val _duplicateChecked = MutableStateFlow(false) + val duplicateChecked: StateFlow = _duplicateChecked.asStateFlow() private val _patchNicknameState = MutableStateFlow>(UiState.Empty) val patchNicknameState: StateFlow> = _patchNicknameState.asStateFlow() - private var prevCheckResult: Pair? = null - var prevScreenName: String? = null - fun updatePrevScreenName(name: String?) { - prevScreenName = name + 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) + } + 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 + } + + // 중복체크 하지 않고 시작하기 버튼 눌렀을 때 -> Failure 상태로 갱신 fun updateInputUiState(inputUiState: InputUiState) { _inputUiState.value = inputUiState } + // 액티비티, 뷰모델에서 갱신 fun updateDuplicateCheckState(checked: Boolean) { - _isDuplicateChecked.value = checked + _duplicateChecked.value = checked } - fun patchNickname() { - viewModelScope.launch { - _patchNicknameState.value = UiState.Loading + fun checkValidInput(): Boolean { + if (nickname.isBlank()) { + _inputUiState.value = InputUiState.Failure(ErrorCode.CODE_BLANK_INPUT) + return false + } - 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()) - } + if (containsSpaceOrSpecialChar(nickname)) { + _inputUiState.value = InputUiState.Failure(ErrorCode.CODE_SPACE_SPECIAL_CHAR) + return false } + + return true } fun getNicknameDuplicateCheck() { @@ -91,12 +102,10 @@ class NicknameViewModel @Inject constructor( authRepository.getNicknameDuplicateCheck(nickname) .onSuccess { response -> if (response == null) return@onSuccess + showDuplicateCheckResult(response.isDuplicated) - response.isDuplicated.let { - showDuplicateCheckResult(it) - saveDuplicateCheckResult(it) - } updateDuplicateCheckState(true) + Timber.d("DUPLICATE CHECK: ${duplicateChecked.value}") } .onFailure { t -> Timber.e("${t.message}") @@ -106,30 +115,32 @@ class NicknameViewModel @Inject constructor( private fun showDuplicateCheckResult(isDuplicated: Boolean) { _inputUiState.value = if (isDuplicated) { - InputUiState.Failure(ErrorCode.CODE_DUPLICATE) + InputUiState.Failure(ErrorCode.CODE_DUPLICATED) } else { InputUiState.Success } } - private fun saveDuplicateCheckResult(isDuplicated: Boolean) { - prevCheckResult = Pair(nickname, isDuplicated) - } + fun patchNickname() { + viewModelScope.launch { + _patchNicknameState.value = UiState.Loading - private fun checkInputUiState(nickname: String): InputUiState { - if (nickname.isEmpty()) return InputUiState.Empty - if (!checkLength(nickname)) return InputUiState.Failure(ErrorCode.CODE_INVALID_LENGTH) - if (containsSpaceOrSpecialChar(nickname)) { - return InputUiState.Failure(ErrorCode.CODE_SPACE_SPECIAL_CHAR) + 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()) + } } - 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) - // _nickname.map{} Flow -> MutableStateFlow 변환을 위한 확장 함수 private fun Flow.mutableStateIn( initialValue: T, 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 a85fd353..da697de3 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 @@ -13,7 +13,6 @@ import de.hdodenhof.circleimageview.CircleImageView import org.go.sopt.winey.R 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.code.ErrorCode import org.go.sopt.winey.util.code.ErrorCode.* import org.go.sopt.winey.util.context.colorOf import org.go.sopt.winey.util.context.drawableOf @@ -84,7 +83,7 @@ fun TextView.setUploadContentHelperText(inputUiState: InputUiState) { if (inputUiState is Failure) { visibility = View.VISIBLE - if (inputUiState.code == ErrorCode.CODE_INVALID_LENGTH) { + if (inputUiState.code == CODE_INVALID_LENGTH) { text = context.stringOf(R.string.upload_content_error_text) } } @@ -114,10 +113,11 @@ 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_DUPLICATE -> context.stringOf(R.string.nickname_duplicate_error) + CODE_DUPLICATED -> context.stringOf(R.string.nickname_duplicated_error) } } } diff --git a/app/src/main/java/org/go/sopt/winey/util/code/ErrorCode.kt b/app/src/main/java/org/go/sopt/winey/util/code/ErrorCode.kt index 86e3f795..adf97bd6 100644 --- a/app/src/main/java/org/go/sopt/winey/util/code/ErrorCode.kt +++ b/app/src/main/java/org/go/sopt/winey/util/code/ErrorCode.kt @@ -1,8 +1,9 @@ package org.go.sopt.winey.util.code enum class ErrorCode { - CODE_INVALID_LENGTH, + CODE_BLANK_INPUT, CODE_SPACE_SPECIAL_CHAR, CODE_UNCHECKED_DUPLICATION, - CODE_DUPLICATE + CODE_DUPLICATED, + CODE_INVALID_LENGTH } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb1598ef..cd60e6c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -148,10 +148,12 @@ 중복확인 확인 시작하기 + + 입력값이 비어있습니다 :( 1~8자로 입력해주세요 :( 공백과 특수문자는 사용할 수 없습니다 :( 닉네임 중복확인을 해주세요 :( - 중복된 닉네임입니다 :( + 중복된 닉네임입니다 :( 사용 가능한 닉네임입니다 :)