-
Notifications
You must be signed in to change notification settings - Fork 8
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
[AN/USER] feat: 학생 인증 화면 구현 (#370) #429
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니당 👍 코멘트 확인해주세요!
binding.uiState = uiState | ||
when (uiState) { | ||
is StudentVerificationUiState.Success -> handleSuccess(uiState) | ||
is StudentVerificationUiState.Loading, StudentVerificationUiState.Error -> {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is StudentVerificationUiState.Loading, StudentVerificationUiState.Error -> {} | |
is StudentVerificationUiState.Loading, StudentVerificationUiState.Error -> Unit |
import kotlinx.coroutines.flow.asStateFlow | ||
import kotlinx.coroutines.launch | ||
|
||
class StudentsVerificationViewModel( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
StudentVerification으로 통일하는 게 어떤가요?
fun confirmVerificationCode() { | ||
viewModelScope.launch { | ||
studentsVerificationRepository.requestVerificationCodeConfirm( | ||
StudentVerificationCode(studentVerificationCode.value), | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
타이머 시간이 유효한지도 확인이 필요할 것 같아요!
<com.google.android.material.textfield.TextInputEditText | ||
android:id="@+id/tieUserName" | ||
android:layout_width="match_parent" | ||
android:layout_height="wrap_content" | ||
android:digits="0123456789_abcdefghijklmnopqrstuvwxyz" | ||
android:hint="@string/student_verification_tie_username" | ||
android:maxLength="20" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
digits와 maxLength를 이렇게 설정하신 이유가 있나요? google 계정 같은 경우 30자까지 가능하고 마침표도 입력할 수 있더라고요! 제한을 두지 않거나 제한이 필요하다면 정책을 같이 고민해볼 필요가 있을 것 같네요.
<TextView | ||
android:id="@+id/tvUserNameGuide" | ||
android:layout_width="0dp" | ||
android:layout_height="wrap_content" | ||
android:layout_marginStart="12dp" | ||
android:layout_marginTop="12dp" | ||
android:gravity="start" | ||
app:layout_constraintEnd_toStartOf="@+id/btnRequestVerificationCode" | ||
app:layout_constraintStart_toStartOf="@id/glStart" | ||
app:layout_constraintTop_toBottomOf="@id/tilEmail" | ||
tools:text="이메일 형식이 아닙니다." /> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 쓰이지 않는 것 같네요!
style="@style/btnFesta" | ||
android:layout_width="0dp" | ||
android:layout_height="wrap_content" | ||
android:layout_marginHorizontal="20dp" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
버튼만 가로 margin 있는 이유가 있나요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그냥 제 눈에 더 예뻐 보여서 했는데 다시 보니깐 통일성 없는 것 같기도 하네요.. ㅜ
_uiState.value = StudentVerificationUiState.Success( | ||
schoolEmail = email, | ||
remainTime = MIN_REMAIN_TIME, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
화면 회전 시 uiState가 다시 생성되면서 isValidateCode가 유지가 안됩니다! 화면 회전인 경우 리프레시하지 않도록 하면 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다! 코멘트 남겨났으니 확인 부탁드립니다!
// TODO: API 연동 작업 필요 | ||
return Result.success(Unit) | ||
|
||
// studentsVerificationRetrofitService.sendVerificationCode( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 주석은 나중에 추가할 내용 같은데 삭제하는것이 어떤가요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR 과 관련없는 내용이네요. 삭제하겠습니다!
super.onCreate(savedInstanceState) | ||
initBinding() | ||
initObserving() | ||
initView() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private fun 호출 순서에 맞추면 가독성이 더 좋을 것같습니다!
|
||
class StudentVerificationActivity : AppCompatActivity() { | ||
|
||
private lateinit var binding: ActivityStudentVerificationBinding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private lateinit var binding: ActivityStudentVerificationBinding | |
private val binding: ActivityStudentVerificationBinding by lazy { | |
ActivityStudentVerificationBinding.inflate(layoutInflater) | |
} |
바인딩 같은 경우는 변경될 일이 없기 때문에 다음과 같이 선언하는 건 어떤가요?
|
||
private fun initObserving() { | ||
lifecycleScope.launch { | ||
repeatOnLifecycle(Lifecycle.State.STARTED) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
관용적으로 사용되는 repeatOnStarted
를 하나 만들어서 depth를 줄이는건 어떤가요?
companion object { | ||
|
||
private const val KEY_SCHOOL_ID = "KEY_SCHOOL_ID" | ||
fun getIntent(context: Context, schoolId: Long): Intent { | ||
return Intent(context, StudentVerificationActivity::class.java).apply { | ||
putExtra(KEY_SCHOOL_ID, schoolId) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
companion object { | |
private const val KEY_SCHOOL_ID = "KEY_SCHOOL_ID" | |
fun getIntent(context: Context, schoolId: Long): Intent { | |
return Intent(context, StudentVerificationActivity::class.java).apply { | |
putExtra(KEY_SCHOOL_ID, schoolId) | |
} | |
} | |
} | |
companion object { | |
private const val KEY_SCHOOL_ID = "KEY_SCHOOL_ID" | |
fun getIntent(context: Context, schoolId: Long): Intent { | |
return Intent(context, StudentVerificationActivity::class.java).apply { | |
putExtra(KEY_SCHOOL_ID, schoolId) | |
} | |
} | |
} |
함수하고 변수는 떨어져 있는게 더 가독성있는거 같습니다!
@ValueSource(strings = ["1234567", "a123456", "asdfeq", "999999a"]) | ||
fun `학생 인증 코드가 6자리 숫자가 아니면 예외가 발생한다 `(code: String) { | ||
assertThrows<IllegalArgumentException> { StudentVerificationCode(code) } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
특수문자랑 5자리 이하도 테스트하면 좋을 것같습니다!
app:layout_constraintEnd_toStartOf="@+id/glEnd" | ||
app:layout_constraintTop_toBottomOf="@+id/tilEmail" /> | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
두칸 띄운건 설정을 따로 안해주면 안스가 못 잡아주더라구요! 통일성을 위해서 정리한번 해주는게 좋을 것같네요!
|
||
private fun validateLength(code: String) = code.length == length | ||
|
||
private fun validateDigits(code: String) = code.chunked(CHUNKED_SIZE).all { it in digits } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code.chucked(CHUNKED_SIZE)
에서 code.toList()
더 간략하게 만들 수 있습니다!
추가로 code
는 SentenceValidation
하고 네이밍이 적합하지 않다고 생각들고 있습니다!
따로 의논을 나눈대로 더 적합한 네이밍을 찾는것이 졸다고 생각합니다!
} | ||
} | ||
|
||
private fun checkValidateCodeWhenSuccess(code: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check와 Validate는 중복된다고 생각이 들고 있습니다!
추가로 WhenSuccess는 캡슐화를 헤치는 경우라고 생각합니다!
private fun checkValidateCodeWhenSuccess(code: String) { | |
private fun validateCode(code: String) { |
그리고 어느 곳에서는 StudentVerificationCode로 사용되고 여기서는 Code만 사용되고 있는데 전체적으로 통일되게하는건 어떤가요??
개인적인 생각으로는 StudentVerificationViewModel이기 때문에
VerificationCode
나 StudentCode
, 그리고 Code
로 모두 통일해도 괜찮다고 생각합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
함수의 역할을 모두 드러내야 한다고 생각했는데 캡슐화를 해치는 것일 수 있겠네요. 동의합니다.
validateStudentVerificationCode() | ||
} | ||
|
||
private fun validateStudentVerificationCode() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메소드 명은 검증인데 실제로는 studentVerificationCode.collect
으로 수집만 하고 있습니다!
observeStudentVerificationCode
와 같이 메소드의 행위로 변경하시는건 어떤가요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
고생하셨습니다! 코멘트 내용 확인 부탁드립니다!
|
||
@ParameterizedTest | ||
@ValueSource(strings = ["asdf", "fkwk", "test"]) | ||
fun `4자리 알파벳 소문자이면 검증된 문자이다`(sentence: String) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트 이름에 when도 넣어주는게 어떤가요? 예시로 다음과 같이 작성하면 모든 케이스를 담을 수 있을 것같습니다!
- 소문자 알파벳만 가능 할 때 알파벳만 들어오면 검증된 문자이다.
- 검증된 문자들이 들어올 때 제한된 글자 수 이내면 검증된 길이다.
- 검증된 문자들이고 검증된 길이면 참이다.
- 검증된 문자가 아니고 검증된 길이면 거짓이다.
- 검증된 문자고 검증된 길이가 아니면 거짓이다.
- 검증된 문자가 아니고 검증된 길이가 아니면 거짓이다.
|
||
val verificationCode = MutableStateFlow("") | ||
|
||
private val timer: Timer = Timer() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금보니 timer를 interface로 두고 생성자로 주입받게 만드는건 어떤가요?? 여기뿐만아니라 티켓인증에서 테스트를 어렵게 만드는 주범같다는 생각이 듭니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ViewModel 이 주입받는걸 말씀하시는 걸까요??
) | ||
} returns result | ||
|
||
vm.confirmVerificationCode() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coEvery들은 given절이라고 생각이들고 vm.confirmVerificationCode()는 when절이라고 생각이 듭니다! 따로 분리하는 것이 어떨까요?
@Test | ||
fun `이메일 불러오기에 실패하면 실패 상태가 된다`() { | ||
// given | ||
`이메일 불러오기`(Result.failure(Exception())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// given
`다음 이메일 결과가 반환될 때`(Result.failure(Exception()))
or
`실패 이메일 결과가 반환될 때`()
// when
`이메일을 불러오면`()
// then
assertThat(vm.uiState.value is StudentVerificationUiState.Error).isTrue
같이 분리하는건 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
반복되는 given 과 when 을 각각 함수화하고 사용하자!
테스트가 더 명시적이고 좋은 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이번에 새로 도입한게 많아서 예상치 못한 허들이 많았을텐데 고생하셨습니다 👍 코멘트 확인해주세요!
handleOnUiStateSuccess { | ||
validateCode(code, it) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
깔끔하네요
private val _event = MutableSharedFlow<StudentVerificationEvent>() | ||
val event: SharedFlow<StudentVerificationEvent> = _event.asSharedFlow() | ||
|
||
val verificationCode = MutableStateFlow("") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two way binding 하려면 MutableStateFlow를 public하게 두는 방법뿐일까요? 🤔 (그런거같기도..?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two way를 포기하면 될 것 같은데.. 둘 다 가져가는 방법은 모르겠네요ㅜ
@Test | ||
fun `이메일 불러오기 성공 상태가 아닐 때 화면 상태를 변경할 수 없다 `() { | ||
// when | ||
vm.verificationCode.value = "1234" | ||
|
||
// then | ||
assertThat(vm.uiState.value is StudentVerificationUiState.Loading).isTrue | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트명과 내용이 일치하지 않는 것 같아요. verificationCode가 "1234"이면 Loading 상태다? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엇 그렇네요 when 이 더 필요한데 빼먹었군요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트가 필요없다고 생각해서 제거했습니다!
vm.event.test { | ||
// when | ||
`인증 코드 확인하기`(result = Result.success(Unit), fakeCode = fakeCode) | ||
|
||
// then | ||
assertThat(awaitItem()).isEqualTo(StudentVerificationEvent.VerificationSuccess) | ||
cancelAndIgnoreRemainingEvents() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
turbine 사용 👍👍 async await 보다 훨 깔끔하네요!
} | ||
|
||
@Test | ||
fun `이메일 불러오기와 인증 코드 전송이 성공일 때 인증 번호 확인이 성공하면 인증 성공 이벤트가 발생한다`() = runTest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어제 runTest에 testDispatcher를 넣어야한다고 했던 것 같은데 turbine 도입하면서 해결된건가요❓
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기존에도 runTest 에서 UnconfinedTestDispatcher() 를 사용하면 되는데
그렇게 사용하지 않아도 turbine 을 도입하면 해결됩니다.
fun of(allowedCharSequence: String, expectedLength: Int): TextValidator { | ||
return TextValidator(allowedCharSequence.toList(), expectedLength) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트에서만 사용되고 있네요..! 팩토리 함수 사용할거라면 TextValidator 생성자를 private으로 두고, StudentVerificationCode에서도 팩토리 함수 사용하는 게 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm!
* feat: 학생 인증 레포지토리 정의 * feat: 학교 레포지토리 임시 정의 * feat: 학교 인증 화면 뷰모델 구현 * test: 학생 인증 화면 뷰모델 테스트 작성 * refactor: LiveData stateFlow 로 리팩터링 * feat: StudentVerificationActivity 생성 * feat: 학생 인증 화면 xml 그리기 * feat: 재사용 가능한 문장 검증 로직 생성 및 테스트 * feat: 학생 인증 검증 로직 생성 및 테스트 * feat: SchoolId Long 으로 변경 * feat: 인증 코드 확인 요청 기능 작성 * feat: 코드 형식 확인 인증 * feat: 임시 학교 인증 화면 이동 * refator: ktlint check * refator: 클래스 및 변수 명 변경 * fix: 화면 회전 시 학교 이메일을 재요청하지 않음 * refactor: 사용하지않는 코드 주석 제거 * refactor: repeatOnStartedState util 추가 * refactor: StudentVerificationActivity 전체 리팩터링 * refactor: Students 를 Student 로 통일 * refactor: State 성공 시 처리 함수 분리 및 네이밍 변경 * refactor: 글자 30개까지 입력으로 변경 * feat: sealed interface 학생 인증 이벤트 추가 * refactor: 학생 인증 ViewModel 리팩터링 * test: 학생 인증 이벤트 테스트 추가 * refactor: 사용하지 않는 뷰 제거 * test: 학생 인증 코드 예외 테스트 추가 * feat: Event observing 추가 * refactor: 테스트 리팩터링 * refactor: 테스트 네이밍 리팩터링 * refactor: turbine 추가 및 sharedFlow 테스트 리팩터링 * refactor: Text 검증 테스트 네이밍 및 구조 리팩터링 * refactor: TextValidator 생성 로직 통일 * refactor: given when 함수 분리 및 테스트 리팩터링 * refactor: seal interface 는 isExactlyInstanceOf 로 테스트
* feat: 학생 인증 레포지토리 정의 * feat: 학교 레포지토리 임시 정의 * feat: 학교 인증 화면 뷰모델 구현 * test: 학생 인증 화면 뷰모델 테스트 작성 * refactor: LiveData stateFlow 로 리팩터링 * feat: StudentVerificationActivity 생성 * feat: 학생 인증 화면 xml 그리기 * feat: 재사용 가능한 문장 검증 로직 생성 및 테스트 * feat: 학생 인증 검증 로직 생성 및 테스트 * feat: SchoolId Long 으로 변경 * feat: 인증 코드 확인 요청 기능 작성 * feat: 코드 형식 확인 인증 * feat: 임시 학교 인증 화면 이동 * refator: ktlint check * refator: 클래스 및 변수 명 변경 * fix: 화면 회전 시 학교 이메일을 재요청하지 않음 * refactor: 사용하지않는 코드 주석 제거 * refactor: repeatOnStartedState util 추가 * refactor: StudentVerificationActivity 전체 리팩터링 * refactor: Students 를 Student 로 통일 * refactor: State 성공 시 처리 함수 분리 및 네이밍 변경 * refactor: 글자 30개까지 입력으로 변경 * feat: sealed interface 학생 인증 이벤트 추가 * refactor: 학생 인증 ViewModel 리팩터링 * test: 학생 인증 이벤트 테스트 추가 * refactor: 사용하지 않는 뷰 제거 * test: 학생 인증 코드 예외 테스트 추가 * feat: Event observing 추가 * refactor: 테스트 리팩터링 * refactor: 테스트 네이밍 리팩터링 * refactor: turbine 추가 및 sharedFlow 테스트 리팩터링 * refactor: Text 검증 테스트 네이밍 및 구조 리팩터링 * refactor: TextValidator 생성 로직 통일 * refactor: given when 함수 분리 및 테스트 리팩터링 * refactor: seal interface 는 isExactlyInstanceOf 로 테스트
📌 관련 이슈
✨ PR 세부 내용
학생 인증 화면 구현
StateFlow 를 사용하여 uiState 를 구현
MutableStateFlow 로 인증 코드를 Two-Way Data Binding
임시로 마이페이지에서 학생 인증을 클릭하면 학교 선택 없이 이동하도록 했습니다.
인증 코드가 6자리 입력되면 버튼이 활성화 됩니다.
길이와 문자로 문장 검증을 하는 SentenceValidation class 작성
해당 클래스를 사용해 6자리 숫자를 입력해야하는 StudentVerificationCode value class 작성
단위 테스트 작성 완료
5분 타이머는 코루틴 타이머를 그대로 사용하였고 300초를 LocalTime 으로 Format 에 맞게 변환하여 보여줍니다.
TODO
API 연동 작업 및 오류 발생 메세지 띄워줘야 합니다.
이메일 입력도 검증이 필요하다면 추가해야합니다.
이벤트 처리가 되어있지 않습니다 SharedFlow 로 추가해야합니다.
추가로 Turbine flow 테스트 라이브러리 사용했습니다.
참고 문서 https://github.com/cashapp/turbine
KakaoTalk_Video_2023-09-07-19-13-44.mp4