본문 바로가기

Android/랜덤리즘

[랜덤리즘] 로그인 구현 대작전(구현) -2

 

일단 로그인을 구현하기 위해서는 아래의 2가지 상황이 있기 때문에 온보딩 페이지가 필요하다고 생각이 들었다.

1. 로그인을 하는 경우

2. 로그인을 하지 않는 경우 -> 둘러보기

로그인을 하지 않는 경우(2번)는 기존에 만들었던 화면으로 이동시켜주었다.

 

기능을 구현한 순서대로 차근차근 작성해보자고 .. 레스고 ..

 

아! 그리고 현재 repository에서 LiveData를 사용하고 있는데, LiveData를 사용하는 것이 안좋다는 포스팅을 본 것 같아서 왜 사용하면 안되는지에 대해서 알아 볼 필요가 있을 것 같다. (LiveData 대신 Flow 사용하는 것이 좋다고 함)

LiveData 를 사용한 이유는 ViewModel 에서 LiveData 를 사용했기때문에 자연스레 repository 의 변수에도 LiveData 를 사용했다.

 


🎰 Playstore

 

랜덤리즘 - 알고리즘 랜덤 디펜스 - Google Play 앱

코딩테스트를 위한 알고리즘 랜덤 디펜스

play.google.com

🚀 PR

 

[feat/RANDOM-39] 사용자 계정 연동 by w36495 · Pull Request #63 · w36495/randomrithm

ISSUE 사용자 계정 연동 (#39 ) Todo Jetpack Navigation 적용 onBoarding 화면 구현 Login 화면 구현 계정 확인 기능 구현 사용자가 푼 문제 목록 가져오기 구현 결과 1️⃣ 사용자 계정 확인 사용자의 계정이 so

github.com

 

 


1️⃣ 사용자 계정 확인

 

위의 순서대로 진행되도록 코드를 작성하였다.

 

1. 사용자가 계정을 입력한 후 '계정 확인' 버튼을 누른다. (LoginFragment -> LoginViewModel)

계정이 입력된 EditText 의 값에대해 먼저 유효성 검사를 해주어야 했는데, 이 부분을 어디서 진행해야되는지 고민이 많았다.

API를 요청보내기 전에 기본적인 판단을 해야하니까 ViewModel 아니면 UseCase 에서 해주어야 한다고 생각을 했다.

View 에 대한 처리를 하는 것이므로 ViewModel 에서 유효성 검사를 진행하는 것이 맞다고 생각되면서도, 이것은 '계정 확인'에 대한 기능이니 UseCase 에서 처리를 해도 된다고 생각이 들어서 일단은 UseCase 내에 작성하였다.

 

2. 계정의 존재여부를 위한 API 요청을 보낸다. (LoginViewModel -> CheckUserIdUseCase -> Server)

 

// 사용자가 존재하지 않는 경우
{
  "count":0,
  "items":[]
}

// 사용자가 존재하는 경우
{
  "count": 1,
  "items": [ ... ]
}

 

API의 결과가 위와 같이 전달되는데 계정이 존재하면 count = 1, 존재하지 않으면 count = 0 으로 반환이 된다.

items 에는 간단한 사용자의 계정 정보가 들어있다.

 

// CheckUserIdUseCase.kt

class CheckUserIdUseCase @Inject constructor(
    private val userRepository: UserRepository
) {
    suspend operator fun invoke(userId: String): Boolean {
        if (userId.trim().isEmpty()) {
            throw IllegalArgumentException(Constants.EXCEPTION_WRONG_INPUT.message)
        }

        val result = userRepository.getUser(userId)
        if (result.isSuccessful) {
            result.body()?.let {
                if (it.count == 1) return true
            }
        }

        return false
    }
}

 

EditText 의 값이 비어있는지/아닌지에 대한 확인만 진행하였다.

그 외의 값들은 API 요청을 보냈을 때 존재하지 않다는 결과가 반환될테니까 ..

 

3. API의 결과를 전달받아 처리한다. (Server -> CheckUserIdUseCase -> LoginViewModel -> LoginFragment)

 

// LoginViewModel.kt

fun checkUserAccount(userId: String) {
    viewModelScope.launch {
        try {
            _loginState.value = checkUserIdUseCase.invoke(userId)
        } catch (exception: Exception) {
            _error.value = exception.message
        }
    }
}

 

checkUserIdUseCase의 결과값이 Boolean 의 형태이기 때문에 LiveData 또한 Boolean 으로 선언해주었다.

LoginFragment에서 loginState 의 값을 관찰하고 있다가, EditText 의 하단에 결과에 대한 문구를 표시해주었다!

 

계정이 존재하지 않는 경우 계정이 존재하는 경우

 

이 글을 작성하다보니, 계정확인 버튼을 누르면 EditText 에 포커스가 잡히지 않도록 수정해야겠다.

확인 버튼을 눌렀는데도 커서가 깜빡거리니 신경쓰이는군 ...

 

4. 로그인 완료 후, 사용자 계정을 repo 에 저장한다.

계정확인까지 진행한 후에, 가장 하단의 로그인 버튼을 클릭했을 때 LoginViewModel 의 loginState 값을 사용하여 조건문을 작성해주었다.

// LoginFragment.kt

binding.btnLogin.setOnClickListener {
    if (loginViewModel.loginState.value == true) {
        requireContext().showShortToast(Constants.LOGIN_SUCCESS.message)
        loginViewModel.getUserInfo(binding.etId.text.toString())

        moveHomeActivity()
    } else {
        if (binding.etId.text.toString().trim().isNotEmpty()) {
            requireContext().showShortToast(Constants.LOGIN_SUGGESTION_CHECK_ACCOUNT.message)
        } else {
            requireContext().showShortToast(Constants.LOGIN_SUGGESTION_INPUT_ACCOUNT.message)
        }
    }
}

 

- 계정 확인에 성공한 경우

토스트를 표시하고, 사용자의 정보를 Repository 에 저장한 후 main 화면으로 이동시켜주었다.

- 계정 확인에 실패한 경우

계정 확인에 실패한 상태이거나, 계정 확인 버튼을 클릭하지 않았을 경우 -> 계정 확인 버튼 클릭을 유도하도록 토스트 메세지를 띄웠다.

EditText 가 비어있을 경우 -> 계정 입력에 대한 안내 문구를 띄웠다.

 

그런데 이 기능을 하면서 또다시 수정해야 할 부분이 생각이 났다.

계정 확인은 성공하였는데, 계정을 수정하고 로그인 버튼을 클릭했을 때 .. (loginState 는 true 인 상황)

그렇게되면 존재하지 않는 계정으로 로그인이 되는 것이니 올바르게 진행이 불가능하다. .. .....

 

아래에 작성할 문제 리스트를 가져올 때 위와 같은 문제(존재하지 않은 계정으로 로그인)가 발생한다면, IllegarStateException 이 발생하도록 작성하긴 했는데 .. 이후에 어떻게 처리해야 할지에 대해 조금 더 생각해봐야겠다.

Exception 핸들링을 작성해주어야 하는 것인가?!

2️⃣ 사용자가 풀었던 문제리스트 가져오기

1. Home 화면으로 넘어왔을 때, 사용자가 푼 문제 목록 가져오기

 

// HomeFragment.kt

private fun subscribeUi() {
    with(homeViewModel) {
        user.observe(viewLifecycleOwner) { user ->
            homeViewModel.fetchSolvedProblems(user.id, user.solvedCount)
            setupUserProfile(user)
        }
        
        ...
    }
}

 

위에서 user의 정보를 repository 의 변수에 저장하였는데, 이를 HomeFragment에서 관찰자를 등록해주었다.

user의 정보로 변경이되면, fetchSolvedProblems()를 통해 기존에 풀었던 문제 목록을 가져올 수 있도록 하였다.

User 클래스에는 푼 문제의 총 개수를 가지고 있는데, 이를 함께 매개변수로 넘겨주었다.

 

2. API 호출이 필요한 횟수 구하기

 

// HomeViewModel.kt

private fun calculatePageOfProblems(solvedProblemCount: Int): Int {
    return if (solvedProblemCount % 50 != 0) (solvedProblemCount / 50) + 1
    else solvedProblemCount / 50
}

 

API 요청을 보내게되면, 최대 50개의 문제들이 넘어오게된다. 즉, 1페이지 = 50개의 문제

그렇기때문에 총 몇페이지인지에 대한 계산이 필요하다고 생각이 되었다.

 

3. API 요청 보내고, 응답 받기

 

// HomeViewModel.kt

fun fetchSolvedProblems(userId: String, solvedProblemCount: Int) {
    val lastPage = calculatePageOfProblems(solvedProblemCount)

    (FIRST_PAGE..lastPage).forEach { page ->
        viewModelScope.launch {
            try {
                problemRepository.fetchSolvedProblems(userId, page)
            } catch (exception : Exception) {
                _error.value = exception.message
            }
        }
    }
}

 

HomeFragment 에서 fetchSolvedProblems() 를 호출하면, 가장 먼저 마지막 페이지를 구하도록 했다.

그러면서 for 문을 통해 1페이지부터 마지막 페이지까지 api를 호출하며 데이터를 저장하였다.

 

4. API 응답을 Repository 변수에 저장하기

 

// ProblemRepositoryImpl.kt

class ProblemRepositoryImpl @Inject constructor(
    private val problemRemoteDataSource: ProblemRemoteDataSource,
) : ProblemRepository {
    private val _solvedProblems = MutableLiveData(emptyList<Problem>())
    override val solvedProblems: LiveData<List<Problem>> get() = _solvedProblems

    override suspend fun fetchSolvedProblems(userId: String, page: Int) {
        val query = "%40$userId"
        val result = problemRemoteDataSource.fetchSolvedProblems(query, page)

        if (result.isSuccessful) {
            result.body()?.let { problemDto ->
                _solvedProblems.value = _solvedProblems.value?.plus(
                    problemDto.items.map { it.toDomainModel() }
                )
            }
        } else {
            throw IllegalStateException(Constants.EXCEPTION_DATA_ROAD_FAILED.message)
        }
    }
}

 

MutableLiveData 에 어떻게 데이터를 추가할 수 있을까? 생각을 했었다.

그런데 String 에서도 + (플러스)를 통해 쉽게 연결이 가능하다는 것을 알게되었고, 그렇다면 LiveData의 value 에서도 plus 를 사용할 수 있을까?

 

public operator fun <T> Collection<T>.plus(elements: Iterable<T>): List<T> {
    if (elements is Collection) {
        val result = ArrayList<T>(this.size + elements.size)
        result.addAll(this)
        result.addAll(elements)
        return result
    } else {
        val result = ArrayList<T>(this)
        result.addAll(elements)
        return result
    }
}

 

plus 의 내부 코드를 살펴보니 새로운 List 를 만들어서 기존의 값과 새로운 값들을 모두 합친 List 를 반환해준다.

딱 내가 생각했던 것이잖아! plus 를 통해서 1페이지의 값들부터 마지막 페이지의 문제들까지 1개의 변수안에 넣어주었다!

 

이제 문제를 받아오면 이 solvedProblems 안에 있는 값들과 비교한 후에, 사용자에게 보여주는 기능을 만들어야 한다!

물론 솔브드에서 내가 풀지 않은 문제들로만 검색이되도록 하는 기능을 제공하기는 하지만, 검색하는 알고리즘을 이때 써볼 수 있을 것 같아 직접 해보기로 다짐했다...! 

 

 

테스트도 직접 작성해볼 수 있을 것 같음!