From 2ed775506f7b9e86802eba642b9424b0da10b172 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:49:29 +0900 Subject: [PATCH 01/26] =?UTF-8?q?[add]=20=EA=B7=B8=EB=9E=98=EB=93=A4=20?= =?UTF-8?q?=EC=A2=85=EC=86=8D=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 18 ++++++++++++++---- build.gradle | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ca90483..b7570ab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,8 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'kotlin-parcelize' + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' } android { @@ -25,11 +27,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '11' + jvmTarget = '17' } buildFeatures { @@ -39,12 +41,20 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.5.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.google.code.gson:gson:2.10.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + implementation "com.google.dagger:hilt-android:2.44" + kapt "com.google.dagger:hilt-compiler:2.44" + implementation 'androidx.activity:activity-ktx:1.2.2' + implementation 'androidx.fragment:fragment-ktx:1.3.3' +} + +kapt { + correctErrorTypes true } \ No newline at end of file diff --git a/build.gradle b/build.gradle index fd3d478..82ace0b 100644 --- a/build.gradle +++ b/build.gradle @@ -3,4 +3,5 @@ plugins { id 'com.android.application' version '8.0.2' apply false id 'com.android.library' version '8.0.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.20' apply false + id 'com.google.dagger.hilt.android' version '2.44' apply false } \ No newline at end of file From 678ccbb771a091bc9a7062b0b37c6dc659482fcd Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:50:00 +0900 Subject: [PATCH 02/26] =?UTF-8?q?[add]=20DI=20=EC=95=B1=EC=A7=84=EC=9E=85?= =?UTF-8?q?=EC=A0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 11 ++++++++--- .../java/org/sopt/dosopttemplate/DoSoptApplication.kt | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/DoSoptApplication.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1813f54..0d9b3d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + + android:exported="true" + android:windowSoftInputMode="adjustResize"> @@ -27,8 +30,10 @@ - + diff --git a/app/src/main/java/org/sopt/dosopttemplate/DoSoptApplication.kt b/app/src/main/java/org/sopt/dosopttemplate/DoSoptApplication.kt new file mode 100644 index 0000000..78b6c59 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/DoSoptApplication.kt @@ -0,0 +1,8 @@ +package org.sopt.dosopttemplate + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class DoSoptApplication : Application() { +} \ No newline at end of file From 2a6afb981c0153d1250145f931d1f77684cd8e41 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:50:25 +0900 Subject: [PATCH 03/26] =?UTF-8?q?[feat]=20SharedPref=20=EB=AA=A8=EB=93=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dosopttemplate/di/SharedPrefModule.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/di/SharedPrefModule.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/SharedPrefModule.kt b/app/src/main/java/org/sopt/dosopttemplate/di/SharedPrefModule.kt new file mode 100644 index 0000000..42add60 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/SharedPrefModule.kt @@ -0,0 +1,20 @@ +package org.sopt.dosopttemplate.di + +import android.content.Context +import android.content.SharedPreferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SharedPrefModule { + @Provides + @Singleton + fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + return context.getSharedPreferences("prefs", Context.MODE_PRIVATE) + } +} \ No newline at end of file From 2228e674a572a3cd97c62e5a0b8d9c9c0237bfc2 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:50:55 +0900 Subject: [PATCH 04/26] =?UTF-8?q?[feat]=20sharedpref=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/SharedPrefRepository.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/domain/repository/SharedPrefRepository.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/domain/repository/SharedPrefRepository.kt b/app/src/main/java/org/sopt/dosopttemplate/domain/repository/SharedPrefRepository.kt new file mode 100644 index 0000000..65de974 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/domain/repository/SharedPrefRepository.kt @@ -0,0 +1,12 @@ +package org.sopt.dosopttemplate.domain.repository + +import org.sopt.dosopttemplate.data.entity.User + +interface SharedPrefRepository { + fun saveUserInfo(user: User?) + fun getUserInfo(): User? + fun setAutoLogin() + fun isAutoLogin(): Boolean + fun clearAutoLogin() + fun clearSharedPref() +} \ No newline at end of file From f154dd9d09636743e1a2a61cf851541dfa6eaa8c Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:51:14 +0900 Subject: [PATCH 05/26] =?UTF-8?q?[feat]=20sharedpref=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EB=B6=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/SharedPrefRepositoryImpl.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/data/repository/SharedPrefRepositoryImpl.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/data/repository/SharedPrefRepositoryImpl.kt b/app/src/main/java/org/sopt/dosopttemplate/data/repository/SharedPrefRepositoryImpl.kt new file mode 100644 index 0000000..834ad68 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/data/repository/SharedPrefRepositoryImpl.kt @@ -0,0 +1,40 @@ +package org.sopt.dosopttemplate.data.repository + +import android.content.SharedPreferences +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.sopt.dosopttemplate.data.entity.User +import org.sopt.dosopttemplate.domain.repository.SharedPrefRepository +import javax.inject.Inject + +class SharedPrefRepositoryImpl @Inject constructor( + private val sharedPref: SharedPreferences +) : SharedPrefRepository { + val gson: Gson = GsonBuilder().create() + override fun saveUserInfo(user: User?) { + val value = gson.toJson(user) + sharedPref.edit().putString("user", value).apply() + } + + override fun getUserInfo(): User? { + val value = sharedPref.getString("user", "") + if (value.isNullOrBlank()) return null + return gson.fromJson(value, User::class.java) + } + + override fun setAutoLogin() { + sharedPref.edit().putBoolean("autoLogin", true).apply() + } + + override fun isAutoLogin(): Boolean { + return sharedPref.getBoolean("autoLogin", false) + } + + override fun clearAutoLogin() { + sharedPref.edit().putBoolean("autoLogin", false).apply() + } + + override fun clearSharedPref() { + sharedPref.edit().clear().apply() + } +} \ No newline at end of file From eed5c4e2cd689a9e3ff428c9b890d75457633199 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:51:29 +0900 Subject: [PATCH 06/26] =?UTF-8?q?[feat]=20=EB=A6=AC=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=AA=A8=EB=93=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dosopttemplate/di/RepositoryModule.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/di/RepositoryModule.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/di/RepositoryModule.kt b/app/src/main/java/org/sopt/dosopttemplate/di/RepositoryModule.kt new file mode 100644 index 0000000..fe726f4 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/di/RepositoryModule.kt @@ -0,0 +1,19 @@ +package org.sopt.dosopttemplate.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.sopt.dosopttemplate.data.repository.SharedPrefRepositoryImpl +import org.sopt.dosopttemplate.domain.repository.SharedPrefRepository +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + @Singleton + @Binds + abstract fun bindsSharedPrefRepository( + sharedPrefRepository: SharedPrefRepositoryImpl + ): SharedPrefRepository +} \ No newline at end of file From 3ff2daf79fcccba41ceebe241424236225056194 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:52:01 +0900 Subject: [PATCH 07/26] =?UTF-8?q?[add]=20hidekeyboard=20=ED=99=95=EC=9E=A5?= =?UTF-8?q?=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/sopt/dosopttemplate/util/activity/activityExt.kt | 9 +++++++++ .../org/sopt/dosopttemplate/util/context/contextExt.kt | 8 ++++++++ 2 files changed, 17 insertions(+) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/util/activity/activityExt.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/util/activity/activityExt.kt b/app/src/main/java/org/sopt/dosopttemplate/util/activity/activityExt.kt new file mode 100644 index 0000000..6740553 --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/util/activity/activityExt.kt @@ -0,0 +1,9 @@ +package org.sopt.dosopttemplate.util.activity + +import android.app.Activity +import android.view.View +import org.sopt.dosopttemplate.util.context.hideKeyboard + +fun Activity.hideKeyboard() { + hideKeyboard(currentFocus ?: View(this)) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/util/context/contextExt.kt b/app/src/main/java/org/sopt/dosopttemplate/util/context/contextExt.kt index 10f1720..3cfa9cf 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/util/context/contextExt.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/util/context/contextExt.kt @@ -1,8 +1,16 @@ package org.sopt.dosopttemplate.util.context +import android.app.Activity import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager import android.widget.Toast fun Context.toast(message: String) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} + +fun Context.hideKeyboard(view: View) { + val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) } \ No newline at end of file From 2e16d3b17d5085754f8c713f772662cf78d370df Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:52:25 +0900 Subject: [PATCH 08/26] [add] uistate sealed interface --- .../org/sopt/dosopttemplate/util/view/UiState.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/util/view/UiState.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/util/view/UiState.kt b/app/src/main/java/org/sopt/dosopttemplate/util/view/UiState.kt new file mode 100644 index 0000000..66320cd --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/util/view/UiState.kt @@ -0,0 +1,15 @@ +package org.sopt.dosopttemplate.util.view + +sealed interface UiState { + object Empty : UiState + + object Loading : UiState + + data class Success( + val data: T + ) : UiState + + data class Failure( + val msg: String + ) : UiState +} \ No newline at end of file From 7425030928744df2ca33f9690a291bf1a2e10ce1 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:53:10 +0900 Subject: [PATCH 09/26] =?UTF-8?q?[add]=20string=20=EB=A6=AC=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/values/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43ee5b0..972d1dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - DoSoptTemplate + DO SOPT Android Welcome to SOPT 아이디 @@ -12,6 +12,9 @@ 아이디 취미 + 로그아웃에 성공했습니다. + 회원탈퇴에 성공했습니다. + 뒤로가기 버튼을 한번 더 누르면 종료됩니다. 아이디 아이디를 입력하세요. (6~10 글자) @@ -27,4 +30,5 @@ 비밀번호는 8~12자 입니다. 공백으로만 이루어진 닉네임은 불가합니다. 취미를 적어주세요. + 로그아웃 \ No newline at end of file From 97ab0e7f8ba4b1a843d9aedc837700625dec3740 Mon Sep 17 00:00:00 2001 From: Sangwook123 Date: Wed, 11 Oct 2023 02:54:02 +0900 Subject: [PATCH 10/26] [refactor] viewmodel, databinding --- .../presentation/signup/SignupActivity.kt | 76 ++++--- .../presentation/signup/SignupViewModel.kt | 58 ++++++ app/src/main/res/layout/activity_signup.xml | 189 ++++++++++-------- 3 files changed, 211 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupActivity.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupActivity.kt index 9cd3d06..755826a 100644 --- a/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupActivity.kt +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupActivity.kt @@ -1,17 +1,23 @@ package org.sopt.dosopttemplate.presentation.signup import android.os.Bundle +import androidx.activity.viewModels import org.sopt.dosopttemplate.R import org.sopt.dosopttemplate.data.entity.User import org.sopt.dosopttemplate.databinding.ActivitySignupBinding +import org.sopt.dosopttemplate.util.activity.hideKeyboard import org.sopt.dosopttemplate.util.binding.BindingActivity +import org.sopt.dosopttemplate.util.view.UiState import org.sopt.dosopttemplate.util.view.snackBar class SignupActivity : BindingActivity(R.layout.activity_signup) { + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + binding.vm = viewModel initSignupButtonClickListener() + initSignupStateObserver() + initHideKeyboard() } private fun initSignupButtonClickListener() { @@ -22,44 +28,54 @@ class SignupActivity : BindingActivity(R.layout.activity_ binding.etSignupNickname.text.toString(), binding.etSignupHobby.text.toString() ) - - checkValidSignup(user) + viewModel.signUp(user) } } - private fun checkValidSignup(user: User) { - if (!isValidId(user.id)) { - binding.root.snackBar { getString(R.string.signup_fail_id) } - } else if (!isValidPw(user.pw)) { - binding.root.snackBar { getString(R.string.signup_fail_pw) } - } else if (!isValidNickName(user.nickname)) { - binding.root.snackBar { getString(R.string.signup_fail_nickname) } - } else if (!isValidHobby(user.hobby)) { - binding.root.snackBar { getString(R.string.signup_fail_hobby) } - } else { - intent.putExtra(USER_KEY, user) - setResult(RESULT_OK, intent) - finish() - } - } + private fun initSignupStateObserver() { + viewModel.signupState.observe(this) { state -> + when (state) { + is UiState.Success -> { + intent.putExtra(USER_KEY, state.data) + setResult(RESULT_OK, intent) + finish() + } + + is UiState.Failure -> { + when (state.msg) { + CODE_INVALID_ID -> { + binding.root.snackBar { getString(R.string.signup_fail_id) } + } - private fun isValidId(id: String): Boolean = - id.length in MIN_ID_LENGTH..MAX_ID_LENGTH + CODE_INVALID_PW -> { + binding.root.snackBar { getString(R.string.signup_fail_pw) } + } - private fun isValidPw(pw: String): Boolean = - pw.length in MIN_PW_LENGTH..MAX_PW_LENGTH + CODE_INVALID_NICKNAME -> { + binding.root.snackBar { getString(R.string.signup_fail_nickname) } + } - private fun isValidNickName(nickname: String): Boolean = - nickname.isNotBlank() + CODE_INVALID_HOBBY -> { + binding.root.snackBar { getString(R.string.signup_fail_hobby) } + } + } + } - private fun isValidHobby(hobby: String): Boolean = - hobby.isNotBlank() + else -> {} + } + } + } + + private fun initHideKeyboard() { + binding.root.setOnClickListener { hideKeyboard() } + binding.clSignup.setOnClickListener { hideKeyboard() } + } companion object { - private const val MIN_ID_LENGTH = 6 - private const val MAX_ID_LENGTH = 10 - private const val MIN_PW_LENGTH = 8 - private const val MAX_PW_LENGTH = 12 private const val USER_KEY = "user" + private const val CODE_INVALID_ID = "idFail" + private const val CODE_INVALID_PW = "pwFail" + private const val CODE_INVALID_NICKNAME = "nicknameFail" + private const val CODE_INVALID_HOBBY = "hobbyFail" } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt new file mode 100644 index 0000000..e8934fa --- /dev/null +++ b/app/src/main/java/org/sopt/dosopttemplate/presentation/signup/SignupViewModel.kt @@ -0,0 +1,58 @@ +package org.sopt.dosopttemplate.presentation.signup + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.sopt.dosopttemplate.data.entity.User +import org.sopt.dosopttemplate.util.view.UiState + +class SignupViewModel : ViewModel() { + val id = MutableLiveData() + val pw = MutableLiveData() + val nickname = MutableLiveData() + val hobby = MutableLiveData() + + private val _signupState = MutableLiveData>(UiState.Empty) + val signupState: LiveData> get() = _signupState + + private fun isValidId(id: String?): Boolean = + id?.length in MIN_ID_LENGTH..MAX_ID_LENGTH + + private fun isValidPw(pw: String?): Boolean = + pw?.length in MIN_PW_LENGTH..MAX_PW_LENGTH + + private fun isValidNickName(nickname: String?): Boolean = + nickname?.isNotBlank() ?: false + + private fun isValidHobby(hobby: String?): Boolean = + hobby?.isNotBlank() ?: false + + fun signUp(user: User) { + if (isValidId(id.value) && + isValidPw(pw.value) && + isValidNickName(nickname.value) && + isValidHobby(hobby.value) + ) { + _signupState.value = UiState.Success(user) + } else if (!isValidId(id.value)) { + _signupState.value = UiState.Failure(CODE_INVALID_ID) + } else if (!isValidPw(pw.value)) { + _signupState.value = UiState.Failure(CODE_INVALID_PW) + } else if (!isValidNickName(nickname.value)) { + _signupState.value = UiState.Failure(CODE_INVALID_NICKNAME) + } else { + _signupState.value = UiState.Failure(CODE_INVALID_HOBBY) + } + } + + companion object { + private const val MIN_ID_LENGTH = 6 + private const val MAX_ID_LENGTH = 10 + private const val MIN_PW_LENGTH = 8 + private const val MAX_PW_LENGTH = 12 + private const val CODE_INVALID_ID = "idFail" + private const val CODE_INVALID_PW = "pwFail" + private const val CODE_INVALID_NICKNAME = "nicknameFail" + private const val CODE_INVALID_HOBBY = "hobbyFail" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_signup.xml b/app/src/main/res/layout/activity_signup.xml index 61ddf09..1295d95 100644 --- a/app/src/main/res/layout/activity_signup.xml +++ b/app/src/main/res/layout/activity_signup.xml @@ -5,6 +5,9 @@ + - - - + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/btn_signup_signup" + app:layout_constraintTop_toBottomOf="@id/tv_signup_title"> + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - -