Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] 닉네임 유효성 검증 로직 개선 #304

Merged
merged 4 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,15 +60,12 @@ class NicknameActivity : BindingActivity<ActivityNicknameBinding>(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
}
})
Expand All @@ -80,7 +74,7 @@ class NicknameActivity : BindingActivity<ActivityNicknameBinding>(R.layout.activ
private fun initDuplicateCheckButtonClickListener() {
binding.btnNicknameDuplicateCheck.setOnClickListener {
viewModel.apply {
if (checkValidInput()) {
if (validateNickname()) {
getNicknameDuplicateCheck()
}
}
Expand All @@ -89,15 +83,13 @@ class NicknameActivity : BindingActivity<ActivityNicknameBinding>(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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<InputUiState> = _nickname.map { checkInputUiState(it) }
.mutableStateIn(
initialValue = InputUiState.Empty,
scope = viewModelScope
)
private val _inputUiState: MutableStateFlow<InputUiState> =
_nickname.map { updateInputUiState(it) }
.mutableStateIn(
initialValue = InputUiState.Empty,
scope = viewModelScope
)

val inputUiState: StateFlow<InputUiState> = _inputUiState.asStateFlow()

val isValidNickname: StateFlow<Boolean> = _inputUiState.map { validateNickname(it) }
val isValidNickname: StateFlow<Boolean> = _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<Boolean> = _duplicateChecked.asStateFlow()
val duplicateChecked = _duplicateChecked.asStateFlow()

private val _patchNicknameState = MutableStateFlow<UiState<Unit>>(UiState.Empty)
val patchNicknameState: StateFlow<UiState<Unit>> = _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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

최대 8자까지만 입력 가능하도록 EditText에 maxLength 제한을 걸어뒀기 때문에
checkLength 함수는 불필요하다 판단해서 제거했습니다!


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}")
Expand All @@ -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
}
Expand All @@ -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 <T> Flow<T>.mutableStateIn(
initialValue: T,
scope: CoroutineScope
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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 -> ""
}
}
}
Expand Down
9 changes: 0 additions & 9 deletions app/src/main/java/org/go/sopt/winey/util/state/ErrorCode.kt

This file was deleted.

12 changes: 12 additions & 0 deletions app/src/main/java/org/go/sopt/winey/util/state/InputError.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
1 change: 0 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,6 @@
<string name="nickname_start_btn_text">시작하기</string>

<string name="nickname_blank_input_error">입력값이 비어있습니다 :(</string>
<string name="nickname_invalid_length_error">1~8자로 입력해주세요 :(</string>
<string name="nickname_space_special_char_error">공백과 특수문자는 사용할 수 없습니다 :(</string>
<string name="nickname_unchecked_duplication_error">닉네임 중복확인을 해주세요 :(</string>
<string name="nickname_duplicated_error">중복된 닉네임입니다 :(</string>
Expand Down
Loading