Skip to content

Commit

Permalink
Merge pull request #304 from team-winey/feature/refactor-nickname-input
Browse files Browse the repository at this point in the history
[refactor] λ‹‰λ„€μž„ μœ νš¨μ„± 검증 둜직 κ°œμ„ 
  • Loading branch information
leeeha authored Jul 27, 2024
2 parents e2afac4 + dffa02e commit c7d77f5
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 87 deletions.
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

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

0 comments on commit c7d77f5

Please sign in to comment.