diff --git a/app/build.gradle b/app/build.gradle index feb3996..1082a6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -112,5 +112,7 @@ dependencies { // permission implementation "io.github.ParkSangGwon:tedpermission-normal:3.3.0" + implementation 'com.github.dhaval2404:imagepicker:2.1' + implementation 'com.google.android.gms:play-services-oss-licenses:17.0.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d5e7a2d..427ef57 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + + + build(retrofit: Retrofit, service: Class): T { + return retrofit.create(service) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/ApiResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/ApiResponse.kt new file mode 100644 index 0000000..5b8fa5b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/ApiResponse.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.common + +import com.google.gson.annotations.SerializedName + +data class ApiResponse( + @SerializedName("success") val success: Boolean, + @SerializedName("data") val data: T? = null, + @SerializedName("message") val message: String? = null, + @SerializedName("errorProperty") val errorProperty: String? = null +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt index 7c5f920..709f06e 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt @@ -1,4 +1,12 @@ package kr.co.vividnext.sodalive.common object Constants { + const val PREF_TOKEN = "pref_token" + const val PREF_EMAIL = "pref_email" + const val PREF_USER_ID = "pref_user_id" + const val PREF_NICKNAME = "pref_nickname" + const val PREF_PROFILE_IMAGE = "pref_profile_image" + + const val EXTRA_DATA = "extra_data" + const val EXTRA_TERMS = "extra_terms" } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/LoadingDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/LoadingDialog.kt new file mode 100644 index 0000000..2be3abf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/LoadingDialog.kt @@ -0,0 +1,49 @@ +package kr.co.vividnext.sodalive.common + +import android.app.Activity +import android.graphics.Color +import android.graphics.drawable.AnimationDrawable +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import kr.co.vividnext.sodalive.databinding.DialogLoadingBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class LoadingDialog( + activity: Activity, + layoutInflater: LayoutInflater +) { + private val alertDialog: AlertDialog + private val dialogView = DialogLoadingBinding.inflate(layoutInflater) + private val animationDrawable: AnimationDrawable + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + animationDrawable = dialogView.tvLoading.compoundDrawables[1] as AnimationDrawable + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + + fun show(width: Int, message: String = "") { + alertDialog.show() + animationDrawable.start() + dialogView.tvLoading.text = message + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = width - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + } + + fun dismiss() { + animationDrawable.stop() + alertDialog.dismiss() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt b/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt index 9d32b41..0a22dd0 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/common/SharedPreferenceManager.kt @@ -43,4 +43,34 @@ object SharedPreferenceManager { else -> throw UnsupportedOperationException("Error") } } + + var token: String + get() = sharedPreferences[Constants.PREF_TOKEN, ""] + set(value) { + sharedPreferences[Constants.PREF_TOKEN] = value + } + + var userId: Long + get() = sharedPreferences[Constants.PREF_USER_ID, 0] + set(value) { + sharedPreferences[Constants.PREF_USER_ID] = value + } + + var nickname: String + get() = sharedPreferences[Constants.PREF_NICKNAME, ""] + set(value) { + sharedPreferences[Constants.PREF_NICKNAME] = value + } + + var email: String + get() = sharedPreferences[Constants.PREF_EMAIL, ""] + set(value) { + sharedPreferences[Constants.PREF_EMAIL] = value + } + + var profileImage: String + get() = sharedPreferences[Constants.PREF_PROFILE_IMAGE, ""] + set(value) { + sharedPreferences[Constants.PREF_PROFILE_IMAGE] = value + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index 9d838bc..f15a8b3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -3,10 +3,20 @@ package kr.co.vividnext.sodalive.di import android.content.Context import com.google.gson.GsonBuilder import kr.co.vividnext.sodalive.BuildConfig +import kr.co.vividnext.sodalive.common.ApiBuilder import kr.co.vividnext.sodalive.network.TokenAuthenticator +import kr.co.vividnext.sodalive.settings.TermsApi +import kr.co.vividnext.sodalive.settings.TermsRepository +import kr.co.vividnext.sodalive.settings.TermsViewModel +import kr.co.vividnext.sodalive.user.UserApi +import kr.co.vividnext.sodalive.user.UserRepository +import kr.co.vividnext.sodalive.user.find_password.FindPasswordViewModel +import kr.co.vividnext.sodalive.user.login.LoginViewModel +import kr.co.vividnext.sodalive.user.signup.SignUpViewModel import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.context.startKoin import org.koin.dsl.module import retrofit2.Retrofit @@ -44,11 +54,22 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { .client(get()) .build() } + + single { ApiBuilder().build(get(), UserApi::class.java) } + single { ApiBuilder().build(get(), TermsApi::class.java) } } - private val viewModelModule = module {} + private val viewModelModule = module { + viewModel { LoginViewModel(get()) } + viewModel { SignUpViewModel(get()) } + viewModel { TermsViewModel(get()) } + viewModel { FindPasswordViewModel(get()) } + } - private val repositoryModule = module {} + private val repositoryModule = module { + factory { UserRepository(get()) } + factory { TermsRepository(get()) } + } private val moduleList = listOf( networkModule, diff --git a/app/src/main/java/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt b/app/src/main/java/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt new file mode 100644 index 0000000..3925be6 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.extensions + +import android.content.res.Resources +import android.util.DisplayMetrics +import java.text.DecimalFormat + +fun Float.dpToPx(): Float { + val metrics = Resources.getSystem().displayMetrics + return this * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) +} + +fun Int.dpToPx(): Float { + val metrics = Resources.getSystem().displayMetrics + return this.toFloat() * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) +} + +fun Int.moneyFormat(): String = DecimalFormat("###,###").format(this) +fun Long.moneyFormat(): String = DecimalFormat("###,###").format(this) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/GetTermsResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/GetTermsResponse.kt new file mode 100644 index 0000000..2a78780 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/GetTermsResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.settings + +import com.google.gson.annotations.SerializedName + +data class GetTermsResponse( + @SerializedName("title") val title: String, + @SerializedName("description") val description: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsActivity.kt new file mode 100644 index 0000000..af0a33d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsActivity.kt @@ -0,0 +1,84 @@ +package kr.co.vividnext.sodalive.settings + +import android.annotation.SuppressLint +import android.os.Bundle +import android.widget.Toast +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityTermsBinding +import org.koin.android.ext.android.inject + +class TermsActivity : BaseActivity(ActivityTermsBinding::inflate) { + + private val viewModel: TermsViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindData() + + val terms = intent.getStringExtra(Constants.EXTRA_TERMS) ?: "terms" + if (terms == "privacy") { + viewModel.getPrivacyPolicy() + } else { + viewModel.getTermsOfService() + } + } + + @SuppressLint("SetJavaScriptEnabled") + override fun setupView() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + WebSettingsCompat.setForceDark( + binding.webView.settings, + WebSettingsCompat.FORCE_DARK_ON + ) + } + + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.setOnClickListener { finish() } + + binding.webView.settings.apply { + javaScriptEnabled = false // 자바스크립트 실행 허용 + javaScriptCanOpenWindowsAutomatically = false // 자바스크립트에서 새창 실 행 허용 + setSupportMultipleWindows(false) // 새 창 실행 허용 + loadWithOverviewMode = true // 메타 태그 허용 + + useWideViewPort = true // 화면 사이즈 맞추기 허용 + setSupportZoom(false) // 화면 줌 허용 + builtInZoomControls = false // 화면 확대 축소 허용 여부 + } + } + + private fun bindData() { + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "공지사항을 불러오고 있습니다.") + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.titleLiveData.observe(this) { + binding.toolbar.tvBack.text = it + } + + viewModel.termsLiveData.observe(this) { + val viewPort = + "" + val data = viewPort + it + binding.webView.loadData( + data, + "text/html; charset=utf-8", + "utf-8" + ) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsApi.kt new file mode 100644 index 0000000..2a75b0e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsApi.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.settings + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse +import retrofit2.http.GET + +interface TermsApi { + @GET("/stplat/terms_of_service") + fun getTermsOfService(): Single> + + @GET("/stplat/privacy_policy") + fun getPrivacyPolicy(): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsRepository.kt new file mode 100644 index 0000000..6ef562c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsRepository.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.settings + +class TermsRepository(private val api: TermsApi) { + fun getTermsOfService() = api.getTermsOfService() + fun getPrivacyPolicy() = api.getPrivacyPolicy() +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsViewModel.kt new file mode 100644 index 0000000..62af221 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/settings/TermsViewModel.kt @@ -0,0 +1,92 @@ +package kr.co.vividnext.sodalive.settings + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel + +class TermsViewModel(private val repository: TermsRepository) : BaseViewModel() { + private val _titleLiveData = MutableLiveData() + val titleLiveData: LiveData + get() = _titleLiveData + + private val _termsLiveData = MutableLiveData() + val termsLiveData: LiveData + get() = _termsLiveData + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + fun getTermsOfService() { + _isLoading.value = true + + compositeDisposable.add( + repository.getTermsOfService() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success && it.data != null) { + _titleLiveData.postValue(it.data.title) + _termsLiveData.postValue(it.data.description) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun getPrivacyPolicy() { + _isLoading.value = true + + compositeDisposable.add( + repository.getPrivacyPolicy() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + + if (it.success && it.data != null) { + _titleLiveData.postValue(it.data.title) + _termsLiveData.postValue(it.data.description) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt index 519adc7..7199506 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/splash/SplashActivity.kt @@ -6,8 +6,10 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.ActivitySplashBinding import kr.co.vividnext.sodalive.main.MainActivity +import kr.co.vividnext.sodalive.user.login.LoginActivity @SuppressLint("CustomSplashScreen") class SplashActivity : BaseActivity(ActivitySplashBinding::inflate) { @@ -17,6 +19,14 @@ class SplashActivity : BaseActivity(ActivitySplashBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (SharedPreferenceManager.token.isBlank()) { + showLoginActivity() + } else { + showMainActivity() + } + } + + private fun showMainActivity() { handler.postDelayed({ startActivity( Intent(applicationContext, MainActivity::class.java).apply { @@ -28,5 +38,17 @@ class SplashActivity : BaseActivity(ActivitySplashBinding }, 500) } + private fun showLoginActivity() { + handler.postDelayed({ + startActivity( + Intent(applicationContext, LoginActivity::class.java).apply { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + ) + finish() + }, 500) + } + override fun setupView() {} } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/Gender.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/Gender.kt new file mode 100644 index 0000000..7a0c27f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/Gender.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.user + +import com.google.gson.annotations.SerializedName + +enum class Gender { + @SerializedName("MALE") + MALE, + @SerializedName("FEMALE") + FEMALE, + @SerializedName("NONE") + NONE +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt new file mode 100644 index 0000000..bd6bddc --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserApi.kt @@ -0,0 +1,28 @@ +package kr.co.vividnext.sodalive.user + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.user.find_password.ForgotPasswordRequest +import kr.co.vividnext.sodalive.user.login.LoginRequest +import kr.co.vividnext.sodalive.user.login.LoginResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.Body +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface UserApi { + @POST("/member/login") + fun login(@Body request: LoginRequest): Single> + + @POST("/member/signup") + @Multipart + fun signUp( + @Part profileImage: MultipartBody.Part?, + @Part("request") request: RequestBody + ): Single> + + @POST("/member/forgot-password") + fun findPassword(@Body request: ForgotPasswordRequest): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt new file mode 100644 index 0000000..e7508df --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/UserRepository.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.user + +import kr.co.vividnext.sodalive.user.find_password.ForgotPasswordRequest +import kr.co.vividnext.sodalive.user.login.LoginRequest +import okhttp3.MultipartBody +import okhttp3.RequestBody + +class UserRepository(private val userApi: UserApi) { + fun login(request: LoginRequest) = userApi.login(request) + + fun signUp(profileImage: MultipartBody.Part?, request: RequestBody) = userApi.signUp( + profileImage, + request + ) + + fun findPassword(request: ForgotPasswordRequest) = userApi.findPassword(request = request) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/FindPasswordActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/FindPasswordActivity.kt new file mode 100644 index 0000000..6b2cbdf --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/FindPasswordActivity.kt @@ -0,0 +1,67 @@ +package kr.co.vividnext.sodalive.user.find_password + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.widget.Toast +import com.jakewharton.rxbinding4.widget.textChanges +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityFindPasswordBinding +import org.koin.android.ext.android.inject + +class FindPasswordActivity : BaseActivity( + ActivityFindPasswordBinding::inflate +) { + + private val viewModel: FindPasswordViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindData() + } + + override fun setupView() { + loadingDialog = LoadingDialog(this, layoutInflater) + binding.toolbar.tvBack.text = "비밀번호 재설정" + binding.toolbar.tvBack.setOnClickListener { finish() } + + binding.tvServiceCenter.setOnClickListener { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("http://pf.kakao.com/_sZaeb") + ) + ) + } + + binding.tvFindPassword.setOnClickListener { viewModel.findPassword { finish() } } + } + + private fun bindData() { + compositeDisposable.add( + binding.etEmail.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.email = it.toString() + } + ) + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth, "") + } else { + loadingDialog.dismiss() + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/FindPasswordViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/FindPasswordViewModel.kt new file mode 100644 index 0000000..ca5338a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/FindPasswordViewModel.kt @@ -0,0 +1,61 @@ +package kr.co.vividnext.sodalive.user.find_password + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.user.UserRepository + +class FindPasswordViewModel(private val repository: UserRepository) : BaseViewModel() { + + var email = "" + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + fun findPassword(onSuccess: () -> Unit) { + if (email.isBlank()) { + _toastLiveData.postValue("이메일을 입력하세요.") + return + } + + _isLoading.value = true + val request = ForgotPasswordRequest(email = email) + compositeDisposable.add( + repository.findPassword(request) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success) { + _toastLiveData.postValue( + "임시 비밀번호가 입력하신 이메일로 발송되었습니다. 이메일을 확인해 주세요." + ) + onSuccess() + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/ForgotPasswordRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/ForgotPasswordRequest.kt new file mode 100644 index 0000000..7b6d871 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/find_password/ForgotPasswordRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.user.find_password + +import com.google.gson.annotations.SerializedName + +data class ForgotPasswordRequest(@SerializedName("email") val email: String) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt new file mode 100644 index 0000000..de37ec2 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginActivity.kt @@ -0,0 +1,123 @@ +package kr.co.vividnext.sodalive.user.login + +import android.content.Intent +import android.os.Bundle +import android.text.InputType +import android.widget.Toast +import com.jakewharton.rxbinding4.widget.textChanges +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.databinding.ActivityLoginBinding +import kr.co.vividnext.sodalive.main.MainActivity +import kr.co.vividnext.sodalive.user.find_password.FindPasswordActivity +import kr.co.vividnext.sodalive.user.signup.SignUpActivity +import org.koin.android.ext.android.inject + +class LoginActivity : BaseActivity(ActivityLoginBinding::inflate) { + + private val viewModel: LoginViewModel by inject() + + private lateinit var loadingDialog: LoadingDialog + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + bindData() + } + + override fun setupView() { + binding.tvToolbar.text = "로그인" + loadingDialog = LoadingDialog(this, layoutInflater) + + binding.tvLogin.setOnClickListener { + viewModel.login { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + finishAffinity() + val nextIntent = Intent(applicationContext, MainActivity::class.java) + val extras = intent.getBundleExtra(Constants.EXTRA_DATA) + ?: if (intent.extras != null) { + intent.extras + } else { + null + } + if (extras != null) { + nextIntent.putExtra(Constants.EXTRA_DATA, extras) + } + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + startActivity(nextIntent) + } + } + + binding.tvSignUp.setOnClickListener { + val nextIntent = Intent(applicationContext, SignUpActivity::class.java) + val extras = intent.getBundleExtra(Constants.EXTRA_DATA) + ?: if (intent.extras != null) { + intent.extras + } else { + null + } + if (extras != null) { + nextIntent.putExtra(Constants.EXTRA_DATA, extras) + } + startActivity(nextIntent) + } + + binding.tvForgotPassword.setOnClickListener { + startActivity( + Intent( + applicationContext, + FindPasswordActivity::class.java + ) + ) + } + + binding.tvVisiblePassword.setOnClickListener { viewModel.onClickVisiblePassword() } + } + + private fun bindData() { + compositeDisposable.add( + binding.etEmail.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.email = it.toString() + } + ) + + compositeDisposable.add( + binding.etPassword.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.password = it.toString() + } + ) + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.visiblePasswordLiveData.observe(this) { + binding.tvVisiblePassword.isSelected = it + if (it) { + binding.etPassword.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD + } else { + binding.etPassword.inputType = + InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD + } + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginRequest.kt new file mode 100644 index 0000000..f11f179 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.user.login + +import com.google.gson.annotations.SerializedName + +data class LoginRequest( + @SerializedName("email") val email: String, + @SerializedName("password") val password: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginResponse.kt new file mode 100644 index 0000000..cdc8b4d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginResponse.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.user.login + +import com.google.gson.annotations.SerializedName + +data class LoginResponse( + @SerializedName("userId") val userId: Long, + @SerializedName("token") val token: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("email") val email: String, + @SerializedName("profileImage") val profileImage: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt new file mode 100644 index 0000000..d2b8f5d --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/login/LoginViewModel.kt @@ -0,0 +1,78 @@ +package kr.co.vividnext.sodalive.user.login + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.user.UserRepository + +class LoginViewModel(private val repository: UserRepository) : BaseViewModel() { + + var email = "" + var password = "" + + private val _visiblePasswordLiveData = MutableLiveData(false) + val visiblePasswordLiveData: LiveData + get() = _visiblePasswordLiveData + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + fun login(onSuccess: (String?) -> Unit) { + if (email.isBlank()) { + _toastLiveData.postValue("이메일을 입력하세요.") + return + } + + if (password.isBlank()) { + _toastLiveData.postValue("비밃번호를 입력하세요.") + return + } + + _isLoading.value = true + val request = LoginRequest(email, password) + compositeDisposable.add( + repository.login(request) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + _isLoading.value = false + if (it.success && it.data != null) { + SharedPreferenceManager.token = it.data.token + SharedPreferenceManager.email = it.data.email + SharedPreferenceManager.userId = it.data.userId + SharedPreferenceManager.nickname = it.data.nickname + SharedPreferenceManager.profileImage = it.data.profileImage + onSuccess(it.message) + } else { + if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue( + "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + ) + } + } + }, + { + _isLoading.value = false + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + fun onClickVisiblePassword() { + _visiblePasswordLiveData.postValue(!_visiblePasswordLiveData.value!!) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpActivity.kt new file mode 100644 index 0000000..ea2bb51 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpActivity.kt @@ -0,0 +1,254 @@ +package kr.co.vividnext.sodalive.user.signup + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import coil.load +import coil.transform.RoundedCornersTransformation +import com.github.dhaval2404.imagepicker.ImagePicker +import com.jakewharton.rxbinding4.widget.textChanges +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.RealPathUtil +import kr.co.vividnext.sodalive.databinding.ActivitySignupBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import kr.co.vividnext.sodalive.main.MainActivity +import kr.co.vividnext.sodalive.settings.TermsActivity +import kr.co.vividnext.sodalive.user.Gender +import org.koin.android.ext.android.inject + +class SignUpActivity : BaseActivity(ActivitySignupBinding::inflate) { + + private val viewModel: SignUpViewModel by inject() + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onClickBackButton() + } + } + + private lateinit var loadingDialog: LoadingDialog + + private val imageResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val resultCode = result.resultCode + val data = result.data + + if (resultCode == RESULT_OK) { + // Image Uri will not be null for RESULT_OK + val fileUri = data?.data!! + binding.ivProfile.background = null + binding.ivProfile.load(fileUri) { + crossfade(true) + transformations(RoundedCornersTransformation(13.3f.dpToPx())) + } + viewModel.profileImageUri = fileUri + } else if (resultCode == ImagePicker.RESULT_ERROR) { + Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + viewModel.getRealPathFromURI = { + RealPathUtil.getRealPath(applicationContext, it) + } + + bindData() + } + + override fun setupView() { + binding.toolbar.tvBack.text = "회원가입" + binding.toolbar.tvBack.setOnClickListener { onClickBackButton() } + + loadingDialog = LoadingDialog(this, layoutInflater) + + binding.ivPhotoPicker.setOnClickListener { + ImagePicker.with(this) + .crop() + .galleryOnly() + .galleryMimeTypes( // Exclude gif images + mimeTypes = arrayOf( + "image/png", + "image/jpg", + "image/jpeg" + ) + ) + .createIntent { imageResult.launch(it) } + } + + binding.tvMale.setOnClickListener { + viewModel.changeGender(Gender.MALE) + } + + binding.tvFemale.setOnClickListener { + viewModel.changeGender(Gender.FEMALE) + } + + binding.tvNone.setOnClickListener { + viewModel.changeGender(Gender.NONE) + } + + binding.tvTermsOfService.setOnClickListener { + val intent = Intent(applicationContext, TermsActivity::class.java) + intent.putExtra("terms", "terms") + startActivity(intent) + } + + binding.tvPrivacyPolicy.setOnClickListener { + val intent = Intent(applicationContext, TermsActivity::class.java) + intent.putExtra("terms", "privacy") + startActivity(intent) + } + + binding.ivTermsOfService.setOnClickListener { + viewModel.onClickCheckboxTermsOfService() + } + + binding.ivPrivacyPolicy.setOnClickListener { + viewModel.onClickCheckboxPrivacyPolicy() + } + + binding.tvSignUp.setOnClickListener { + viewModel.signUp { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + finishAffinity() + val nextIntent = Intent(applicationContext, MainActivity::class.java) + val extras = intent.getBundleExtra(Constants.EXTRA_DATA) + ?: if (intent.extras != null) { + intent.extras + } else { + null + } + if (extras != null) { + nextIntent.putExtra(Constants.EXTRA_DATA, extras) + } + startActivity(nextIntent) + } + } + } + + private fun bindData() { + compositeDisposable.add( + binding.etEmail.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.email = it.toString() + } + ) + + compositeDisposable.add( + binding.etPassword.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.password = it.toString() + } + ) + + compositeDisposable.add( + binding.etPasswordRe.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.passwordRe = it.toString() + } + ) + + compositeDisposable.add( + binding.etNickname.textChanges().skip(1) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + viewModel.nickname = it.toString() + } + ) + + viewModel.isAgreeTermsOfServiceLiveData.observe(this) { + binding.ivTermsOfService.isSelected = it + } + + viewModel.isAgreePrivacyPolicyLiveData.observe(this) { + binding.ivPrivacyPolicy.isSelected = it + } + + viewModel.genderLiveData.observe(this) { + binding.tvMale.isSelected = false + binding.tvFemale.isSelected = false + binding.tvNone.isSelected = false + + when (it) { + Gender.MALE -> binding.tvMale.isSelected = true + Gender.FEMALE -> binding.tvFemale.isSelected = true + Gender.NONE -> binding.tvNone.isSelected = true + else -> { + } + } + } + + viewModel.signUpErrorLiveData.observe(this) { + Toast.makeText(applicationContext, it.message, Toast.LENGTH_LONG).show() + + when (it.errorProperty) { + "email" -> { + viewModel.setStep(step = SignUpViewModel.EmailSignUpStep.STEP_1) + binding.etEmail.error = it.message + binding.etEmail.requestFocus() + } + + "password" -> { + viewModel.setStep(step = SignUpViewModel.EmailSignUpStep.STEP_1) + binding.etPassword.error = it.message + binding.etPassword.requestFocus() + } + + "nickname" -> { + binding.etNickname.error = it.message + binding.etNickname.requestFocus() + } + } + } + + viewModel.isLoading.observe(this) { + if (it) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + viewModel.toastLiveData.observe(this) { + it?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + + viewModel.stepLiveData.observe(this) { + if (it == SignUpViewModel.EmailSignUpStep.STEP_2) { + binding.toolbar.tvBack.text = "프로필 설정" + binding.tvSignUp.text = "회원가입" + binding.llStep1.visibility = View.GONE + binding.llStep2.visibility = View.VISIBLE + } else { + binding.toolbar.tvBack.text = "회원가입" + binding.tvSignUp.text = "다음" + binding.llStep1.visibility = View.VISIBLE + binding.llStep2.visibility = View.GONE + } + } + } + + private fun onClickBackButton() { + if (viewModel.stepLiveData.value!! == SignUpViewModel.EmailSignUpStep.STEP_2) { + viewModel.setStep(SignUpViewModel.EmailSignUpStep.STEP_1) + } else { + finish() + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpError.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpError.kt new file mode 100644 index 0000000..e043ee7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpError.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.user.signup + +import com.google.gson.annotations.SerializedName + +data class SignUpError( + @SerializedName("errorProperty") val errorProperty: String, + @SerializedName("message") val message: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpRequest.kt new file mode 100644 index 0000000..0e0647e --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpRequest.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.user.signup + +import com.google.gson.annotations.SerializedName +import kr.co.vividnext.sodalive.user.Gender + +data class SignUpRequest( + @SerializedName("email") val email: String, + @SerializedName("password") val password: String, + @SerializedName("nickname") val nickname: String, + @SerializedName("gender") val gender: Gender, + @SerializedName("isAgreeTermsOfService") val isAgreeTermsOfService: Boolean, + @SerializedName("isAgreePrivacyPolicy") val isAgreePrivacyPolicy: Boolean, + @SerializedName("container") val container: String = "aos" +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpViewModel.kt new file mode 100644 index 0000000..3cabc2a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/user/signup/SignUpViewModel.kt @@ -0,0 +1,241 @@ +package kr.co.vividnext.sodalive.user.signup + +import android.net.Uri +import androidx.core.util.PatternsCompat +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.orhanobut.logger.Logger +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.schedulers.Schedulers +import kr.co.vividnext.sodalive.base.BaseViewModel +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.user.Gender +import kr.co.vividnext.sodalive.user.UserRepository +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File + +class SignUpViewModel(private val repository: UserRepository) : BaseViewModel() { + enum class EmailSignUpStep { + @SerializedName("STEP_1") STEP_1, + @SerializedName("STEP_2") STEP_2 + } + + var email = "" + var password = "" + var passwordRe = "" + var nickname = "" + var profileImageUri: Uri? = null + + private val _genderLiveData = MutableLiveData(Gender.NONE) + val genderLiveData: LiveData + get() = _genderLiveData + + private val _isAgreeTermsOfServiceLiveData = MutableLiveData(false) + val isAgreeTermsOfServiceLiveData: LiveData + get() = _isAgreeTermsOfServiceLiveData + + private val _isAgreePrivacyPolicyLiveData = MutableLiveData(false) + val isAgreePrivacyPolicyLiveData: LiveData + get() = _isAgreePrivacyPolicyLiveData + + private val _signUpErrorLiveData = MutableLiveData() + val signUpErrorLiveData: LiveData + get() = _signUpErrorLiveData + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + get() = _toastLiveData + + private var _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + private val _stepLiveData = MutableLiveData(EmailSignUpStep.STEP_1) + val stepLiveData: LiveData + get() = _stepLiveData + + lateinit var getRealPathFromURI: (Uri) -> String? + + fun setStep(step: EmailSignUpStep = EmailSignUpStep.STEP_2) { + _stepLiveData.postValue(step) + } + + fun signUp(onSuccess: (String?) -> Unit) { + if (stepLiveData.value!! == EmailSignUpStep.STEP_1) { + if (validationStep1()) return + + setStep() + return + } + + if (validationStep2()) return + + val request = SignUpRequest( + email = email, + password = password, + nickname = nickname, + gender = _genderLiveData.value!!, + isAgreeTermsOfService = _isAgreeTermsOfServiceLiveData.value!!, + isAgreePrivacyPolicy = _isAgreePrivacyPolicyLiveData.value!! + ) + + val requestJson = Gson().toJson(request) + + val profileImage = if (profileImageUri != null) { + val file = File(getRealPathFromURI(profileImageUri!!)) + MultipartBody.Part.createFormData( + "profileImage", + file.name, + file.asRequestBody("image/*".toMediaType()) + ) + } else { + null + } + + _isLoading.value = true + compositeDisposable.add( + repository.signUp( + profileImage, + requestJson.toRequestBody("text/plain".toMediaType()) + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + if (it.success && it.data != null) { + SharedPreferenceManager.token = it.data.token + SharedPreferenceManager.email = it.data.email + SharedPreferenceManager.userId = it.data.userId + SharedPreferenceManager.nickname = it.data.nickname + SharedPreferenceManager.profileImage = it.data.profileImage + _isLoading.value = false + onSuccess(it.message) + } else { + _isLoading.value = false + if (it.errorProperty != null && it.message != null) { + _signUpErrorLiveData.postValue( + SignUpError(it.errorProperty, it.message) + ) + } else if (it.message != null) { + _toastLiveData.postValue(it.message) + } else { + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + } + }, + { + it.message?.let { message -> Logger.e(message) } + _toastLiveData.postValue("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } + ) + ) + } + + private fun validationStep1(): Boolean { + if (email.isBlank()) { + _signUpErrorLiveData.postValue( + SignUpError( + "email", + "이메일을 입력하세요." + ) + ) + + return true + } + + if (password.isBlank()) { + _signUpErrorLiveData.postValue( + SignUpError( + "password", + "비밀번호를 입력하세요." + ) + ) + + return true + } + + if (password != passwordRe) { + _signUpErrorLiveData.postValue( + SignUpError( + "password", + "비밀번호가 일치하지 않습니다." + ) + ) + + return true + } + + if ( + !_isAgreePrivacyPolicyLiveData.value!! || + !_isAgreeTermsOfServiceLiveData.value!! + ) { + _signUpErrorLiveData.postValue( + SignUpError( + "", + "약관에 동의하셔야 회원가입이 가능합니다." + ) + ) + + return true + } + + if (!PatternsCompat.EMAIL_ADDRESS.matcher(email).matches()) { + _signUpErrorLiveData.postValue( + SignUpError( + "email", + "올바른 이메일을 입력해 주세요" + ) + ) + + return true + } + + if ( + "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d$@!%*#?&]{8,}$" + .toRegex() + .matches(password) + .not() + ) { + _signUpErrorLiveData.postValue( + SignUpError( + "password", + "영문, 숫자 포함 8자 이상의 비밀번호를 입력해 주세요." + ) + ) + } + + return false + } + + private fun validationStep2(): Boolean { + if (nickname.isBlank() || nickname.length < 2) { + _signUpErrorLiveData.postValue( + SignUpError( + "nickname", + "닉네임은 2자 이상 입력해 주세요." + ) + ) + + return true + } + + return false + } + + fun onClickCheckboxTermsOfService() { + _isAgreeTermsOfServiceLiveData.postValue(!_isAgreeTermsOfServiceLiveData.value!!) + } + + fun onClickCheckboxPrivacyPolicy() { + _isAgreePrivacyPolicyLiveData.postValue(!_isAgreePrivacyPolicyLiveData.value!!) + } + + fun changeGender(gender: Gender) { + _genderLiveData.postValue(gender) + } +} diff --git a/app/src/main/res/drawable-hdpi/loading_1.png b/app/src/main/res/drawable-hdpi/loading_1.png new file mode 100644 index 0000000..d916f59 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/loading_1.png differ diff --git a/app/src/main/res/drawable-hdpi/loading_2.png b/app/src/main/res/drawable-hdpi/loading_2.png new file mode 100644 index 0000000..ee58be3 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/loading_2.png differ diff --git a/app/src/main/res/drawable-hdpi/loading_3.png b/app/src/main/res/drawable-hdpi/loading_3.png new file mode 100644 index 0000000..e1e50ef Binary files /dev/null and b/app/src/main/res/drawable-hdpi/loading_3.png differ diff --git a/app/src/main/res/drawable-hdpi/loading_4.png b/app/src/main/res/drawable-hdpi/loading_4.png new file mode 100644 index 0000000..c4fc517 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/loading_4.png differ diff --git a/app/src/main/res/drawable-hdpi/loading_5.png b/app/src/main/res/drawable-hdpi/loading_5.png new file mode 100644 index 0000000..e08ba00 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/loading_5.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_select_normal.png b/app/src/main/res/drawable-xxhdpi/btn_radio_select_normal.png new file mode 100644 index 0000000..40f508a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_select_normal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_radio_select_selected.png b/app/src/main/res/drawable-xxhdpi/btn_radio_select_selected.png new file mode 100644 index 0000000..57f23d1 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_radio_select_selected.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_select_checked.png b/app/src/main/res/drawable-xxhdpi/btn_select_checked.png new file mode 100644 index 0000000..ee6d977 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_select_checked.png differ diff --git a/app/src/main/res/drawable-xxhdpi/btn_select_normal.png b/app/src/main/res/drawable-xxhdpi/btn_select_normal.png new file mode 100644 index 0000000..b13a12d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/btn_select_normal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_back.png b/app/src/main/res/drawable-xxhdpi/ic_back.png new file mode 100644 index 0000000..a0d3679 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_back.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_camera.png b/app/src/main/res/drawable-xxhdpi/ic_camera.png new file mode 100644 index 0000000..3d21d1f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_camera.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_headphones_purple.png b/app/src/main/res/drawable-xxhdpi/ic_headphones_purple.png new file mode 100644 index 0000000..d285d38 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_headphones_purple.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_logo.png b/app/src/main/res/drawable-xxxhdpi/ic_logo.png new file mode 100644 index 0000000..e56aed8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_logo.png differ diff --git a/app/src/main/res/drawable/bg_round_corner_10_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_10_9970ff.xml new file mode 100644 index 0000000..871bd7f --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_10_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_16_7_222222.xml b/app/src/main/res/drawable/bg_round_corner_16_7_222222.xml new file mode 100644 index 0000000..a00cbf1 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_16_7_222222.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_33_3_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_33_3_9970ff.xml new file mode 100644 index 0000000..64774f7 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_33_3_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_222222.xml b/app/src/main/res/drawable/bg_round_corner_6_7_222222.xml new file mode 100644 index 0000000..c383f4c --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_222222.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_6_7_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_6_7_9970ff.xml new file mode 100644 index 0000000..fdfe26c --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_6_7_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_round_corner_8_transparent_9970ff.xml b/app/src/main/res/drawable/bg_round_corner_8_transparent_9970ff.xml new file mode 100644 index 0000000..523e819 --- /dev/null +++ b/app/src/main/res/drawable/bg_round_corner_8_transparent_9970ff.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/edit_text_cursor.xml b/app/src/main/res/drawable/edit_text_cursor.xml new file mode 100644 index 0000000..b5c7323 --- /dev/null +++ b/app/src/main/res/drawable/edit_text_cursor.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/edittext_underline.xml b/app/src/main/res/drawable/edittext_underline.xml new file mode 100644 index 0000000..83f9cb5 --- /dev/null +++ b/app/src/main/res/drawable/edittext_underline.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/edittext_underline_focused.xml b/app/src/main/res/drawable/edittext_underline_focused.xml new file mode 100644 index 0000000..dfce781 --- /dev/null +++ b/app/src/main/res/drawable/edittext_underline_focused.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/edittext_underline_normal.xml b/app/src/main/res/drawable/edittext_underline_normal.xml new file mode 100644 index 0000000..bf8d2a4 --- /dev/null +++ b/app/src/main/res/drawable/edittext_underline_normal.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_radio_button_select.xml b/app/src/main/res/drawable/ic_radio_button_select.xml new file mode 100644 index 0000000..a64e922 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_button_select.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_select.xml b/app/src/main/res/drawable/ic_select.xml new file mode 100644 index 0000000..7457e3d --- /dev/null +++ b/app/src/main/res/drawable/ic_select.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/loading.xml b/app/src/main/res/drawable/loading.xml new file mode 100644 index 0000000..70b7f1e --- /dev/null +++ b/app/src/main/res/drawable/loading.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/layout/activity_find_password.xml b/app/src/main/res/layout/activity_find_password.xml new file mode 100644 index 0000000..216866f --- /dev/null +++ b/app/src/main/res/layout/activity_find_password.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..aa451eb --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_signup.xml b/app/src/main/res/layout/activity_signup.xml new file mode 100644 index 0000000..8fb178f --- /dev/null +++ b/app/src/main/res/layout/activity_signup.xml @@ -0,0 +1,448 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_terms.xml b/app/src/main/res/layout/activity_terms.xml new file mode 100644 index 0000000..7a989c5 --- /dev/null +++ b/app/src/main/res/layout/activity_terms.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/app/src/main/res/layout/detail_toolbar.xml b/app/src/main/res/layout/detail_toolbar.xml new file mode 100644 index 0000000..1005f86 --- /dev/null +++ b/app/src/main/res/layout/detail_toolbar.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml new file mode 100644 index 0000000..def0f5a --- /dev/null +++ b/app/src/main/res/layout/dialog_loading.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..817bf7a Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..475bce9 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..af71816 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..78d809b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9eb06a0..38df402 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,4 +4,12 @@ #FFFFFFFF #9970FF + #EEEEEE + #777777 + #BBBBBB + #222222 + #909090 + #3E3358 + + #B3909090 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 2c85ba2..322502a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -17,4 +17,9 @@ @color/black @color/black + + diff --git a/settings.gradle b/settings.gradle index 790a92a..1d64f28 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url 'https://jitpack.io' } } } rootProject.name = "SodaLive"