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

로그인 화면 생성, 이메일/비밀번호 유효성 검사, 회원가입 화면으로 전환 #74

Merged
merged 33 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
266e2b1
feat: 로그인 UI viewmodel 추가
EunhoKang Nov 15, 2023
882e8e1
feat: 로그인 xml databinding 적용
EunhoKang Nov 15, 2023
907cfee
feat: 로그인 Activity 수정 및 Intent 추가
EunhoKang Nov 15, 2023
866b71e
feat: 로그인 실패 시 AlertDialog 추가
EunhoKang Nov 15, 2023
1b08974
feat: 로그인 페이지 아이디 & 비밀번호 유효성 검사
EunhoKang Nov 15, 2023
4606fc5
feat: material alert dialog 색 변경
EunhoKang Nov 15, 2023
402bedd
fix: 로그인/회원가입에 대해 setOnClickListener를 사용하도록 변경
EunhoKang Nov 15, 2023
0884511
refactor: context 수정 및 binding 방식 변경
EunhoKang Nov 15, 2023
ee72913
Merge branch 'feat/and/intro' of https://github.com/boostcampwm2023/a…
EunhoKang Nov 15, 2023
690504b
Merge branch 'dev/and' of https://github.com/boostcampwm2023/and09-Pr…
EunhoKang Nov 15, 2023
2cc769c
fix: 로그인 체크 방식을 콜백으로 변경
EunhoKang Nov 15, 2023
cf28410
fix: import 제거
EunhoKang Nov 15, 2023
5c4092f
fix: 초기화면 데이터 바인딩 적용
EunhoKang Nov 15, 2023
8074848
chore: 자바 버전 변경 및 activity 종속성 추가
EunhoKang Nov 15, 2023
096ee3e
refactor: LoginViewModel의 id를 email로 변경
EunhoKang Nov 15, 2023
777d81c
fix: editText의 text 제거
EunhoKang Nov 15, 2023
9208192
fix: apply를 with로 변경하고 listener 함수로 묶기
EunhoKang Nov 15, 2023
e6d92cd
fix: 정규식 변경
EunhoKang Nov 15, 2023
939dac9
fix: login 이름 수정
EunhoKang Nov 15, 2023
9e96128
fix: match to matchEntire
EunhoKang Nov 15, 2023
6e3b19b
fix: string 리소스화
EunhoKang Nov 15, 2023
ae8a4b8
fix: 로그인 뷰모델 콜백 삭제
EunhoKang Nov 16, 2023
27bdeac
feat: 로그인 화면 dialog 다크모드 적용
EunhoKang Nov 16, 2023
800910d
feat: 버튼 로딩 추가
EunhoKang Nov 16, 2023
625f533
fix: lifecycle 코드 간소화
EunhoKang Nov 16, 2023
bd56d86
fix: ProgressIndicator 생성부 함수화
EunhoKang Nov 16, 2023
c4e9cb1
fix: it 제거
EunhoKang Nov 16, 2023
a28a5d8
feat: 유효성 검증 방식 변경
EunhoKang Nov 16, 2023
10a4cd2
feat: 버튼을 머테리얼 버튼으로 변경
EunhoKang Nov 16, 2023
890f443
fix: 로딩 중 뷰모델 데이터 변경 x
EunhoKang Nov 16, 2023
c4dcb57
Revert "feat: 버튼을 머테리얼 버튼으로 변경"
EunhoKang Nov 16, 2023
3372c60
fix: if 사이 공백 추가
EunhoKang Nov 16, 2023
8a7981a
fix: 로그인 버튼을 데이터 바인딩 방식으로 변경
EunhoKang Nov 16, 2023
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
7 changes: 4 additions & 3 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_17.toString()
}
}

Expand All @@ -58,6 +58,7 @@ dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,95 @@
package app.priceguard.ui.login

import androidx.appcompat.app.AppCompatActivity
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import app.priceguard.R
import app.priceguard.databinding.ActivityLoginBinding
import app.priceguard.ui.signup.SignupActivity
import app.priceguard.ui.util.lifecycle.repeatOnStarted
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
import com.google.android.material.progressindicator.IndeterminateDrawable

class LoginActivity : AppCompatActivity() {
private val loginViewModel: LoginViewModel by viewModels()
private lateinit var binding: ActivityLoginBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
with(binding) {
viewModel = loginViewModel
}
initListener()
collectEvent()
setContentView(binding.root)
}

private fun initListener() {
with(binding) {
btnLoginLogin.setOnClickListener {
setLoginButtonActive(false, getProgressIndicatorDrawable())
}
btnLoginSignup.setOnClickListener {
Taewan-P marked this conversation as resolved.
Show resolved Hide resolved
gotoSignUp()
}
}
}

private fun getProgressIndicatorDrawable(): IndeterminateDrawable<CircularProgressIndicatorSpec> {
val spec = CircularProgressIndicatorSpec(
this,
null,
0,
com.google.android.material.R.style.Widget_Material3_CircularProgressIndicator_ExtraSmall
)
return IndeterminateDrawable.createCircularDrawable(this, spec)
}

private fun collectEvent() {
repeatOnStarted {
loginViewModel.event.collect { eventType ->
when (eventType) {
LoginViewModel.LoginEvent.StartLoading -> setLoginButtonActive(false, getProgressIndicatorDrawable())
LoginViewModel.LoginEvent.Invalid -> showDialog(
getString(R.string.login_invalid),
getString(R.string.login_invalid_message),
getString(R.string.login_fail_accept)
)

LoginViewModel.LoginEvent.LoginFailed -> showDialog(
getString(R.string.login_fail),
getString(R.string.login_fail_message),
getString(R.string.login_fail_accept)
)

LoginViewModel.LoginEvent.LoginSuccess -> TODO("로그인 성공 시 처리")
}
}
}
}

private fun showDialog(title: String, message: String, buttonText: String) {
MaterialAlertDialogBuilder(this, R.style.ThemeOverlay_App_MaterialAlertDialog)
.setTitle(title)
.setMessage(message)
.setPositiveButton(buttonText) { _, _ -> setLoginButtonActive(true, null) }
.create()
.show()
}

private fun setLoginButtonActive(
active: Boolean,
icon: IndeterminateDrawable<CircularProgressIndicatorSpec>?
) {
(binding.btnLoginLogin as MaterialButton).icon = icon
binding.btnLoginLogin.isEnabled = active
}

private fun gotoSignUp() {
startActivity(Intent(this, SignupActivity::class.java))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package app.priceguard.ui.login

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class LoginViewModel : ViewModel() {

data class State(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false
)

sealed interface LoginEvent {
object StartLoading : LoginEvent
object Invalid : LoginEvent
object LoginFailed : LoginEvent
Taewan-P marked this conversation as resolved.
Show resolved Hide resolved
object LoginSuccess : LoginEvent
}

private val emailPattern = """^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,6}$""".toRegex()
private val passwordPattern = """^(?=[A-Za-z\d!@#$%^&*]*\d)(?=[A-Za-z\d!@#$%^&*]*[a-z])(?=[A-Za-z\d!@#$%^&*]*[A-Z])(?=[A-Za-z\d!@#$%^&*]*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,16}$""".toRegex()

private var _event = MutableSharedFlow<LoginEvent>()
val event: SharedFlow<LoginEvent> = _event.asSharedFlow()
private val _state = MutableStateFlow(State())
var state: StateFlow<State> = _state.asStateFlow()

fun setID(s: CharSequence, start: Int, before: Int, count: Int) {
if (_state.value.isLoading) return
_state.value = _state.value.copy(email = s.toString())
}

fun setPassword(s: CharSequence, start: Int, before: Int, count: Int) {
if (_state.value.isLoading) return
_state.value = _state.value.copy(password = s.toString())
}

fun login() {
_state.value = _state.value.copy(isLoading = true)
if (checkEmailAndPassword()) {
// TODO: 서버에 정보 전송
} else {
viewModelScope.launch {
_event.emit(LoginEvent.StartLoading)
delay(2000)
_event.emit(LoginEvent.Invalid)
}
}
_state.value = _state.value.copy(isLoading = false)
}

private fun checkEmailAndPassword(): Boolean {
return emailPattern.matchEntire(_state.value.email) != null && passwordPattern.matchEntire(_state.value.password) != null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import kotlinx.coroutines.launch

fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block)
repeatOnLifecycle(Lifecycle.State.STARTED, block)
}
}
127 changes: 66 additions & 61 deletions android/app/src/main/res/layout/activity_intro.xml
Original file line number Diff line number Diff line change
@@ -1,63 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_vertical_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_vertical_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />

<ImageView
android:id="@+id/iv_intro_app_logo"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginTop="114dp"
android:src="@drawable/ic_priceguard_foreground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_intro_app_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="?attr/textAppearanceDisplayMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_intro_app_logo" />

<Button
android:id="@+id/btn_intro_login"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/login"
app:layout_constraintBottom_toTopOf="@id/btn_intro_sign_up"
app:layout_constraintEnd_toStartOf="@+id/gl_vertical_end"
app:layout_constraintStart_toEndOf="@+id/gl_vertical_start" />

<Button
android:id="@+id/btn_intro_sign_up"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/sign_up"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/gl_vertical_end"
app:layout_constraintStart_toEndOf="@+id/gl_vertical_start" />

</androidx.constraintlayout.widget.ConstraintLayout>
xmlns:tools="http://schemas.android.com/tools">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.intro.IntroActivity">

<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_vertical_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/gl_vertical_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />

<ImageView
android:id="@+id/iv_intro_app_logo"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_marginTop="114dp"
android:src="@drawable/ic_priceguard_foreground"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/tv_intro_app_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="?attr/textAppearanceDisplayMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_intro_app_logo" />

<Button
android:id="@+id/btn_intro_login"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="@string/login"
app:layout_constraintBottom_toTopOf="@id/btn_intro_sign_up"
app:layout_constraintEnd_toStartOf="@+id/gl_vertical_end"
app:layout_constraintStart_toEndOf="@+id/gl_vertical_start" />

<Button
android:id="@+id/btn_intro_sign_up"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/sign_up"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/gl_vertical_end"
app:layout_constraintStart_toEndOf="@+id/gl_vertical_start" />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Loading