diff --git a/app/build.gradle b/app/build.gradle
index 41d5050..0f28187 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,6 +2,7 @@ plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
+ id 'kotlin-parcelize'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id 'dagger.hilt.android.plugin'
@@ -22,6 +23,7 @@ android {
def localProperties = new Properties()
localProperties.load(rootProject.file('./local.properties').newDataInputStream())
buildConfigField("String", "BASE_URL", localProperties['baseUrl'])
+ buildConfigField("String", "WEB_CLIENT_ID", localProperties["webClientId"])
}
signingConfigs {
@@ -65,6 +67,7 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
+
buildFeatures {
viewBinding true
}
@@ -78,17 +81,27 @@ dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'androidx.activity:activity-ktx:1.4.0'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ // Lifecycle
+ def lifecycle_version = '2.4.1'
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
+
// Firebase
implementation platform('com.google.firebase:firebase-bom:29.2.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
+ // Google Login
+ implementation 'com.google.android.gms:play-services-auth:20.1.0'
+
// Hilt
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 19ebc43..5a3f2e3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,7 +2,7 @@
-
+
+ android:theme="@style/Theme.MoyeoRunandroid"
+ android:usesCleartextTraffic="true">
+
@@ -21,6 +25,9 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/MainActivity.kt b/app/src/main/java/com/moyerun/moyeorun_android/MainActivity.kt
index f04e0c1..dd97be1 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/MainActivity.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/MainActivity.kt
@@ -1,7 +1,7 @@
package com.moyerun.moyeorun_android
-import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt
new file mode 100644
index 0000000..ea6e1b5
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/EventLiveData.kt
@@ -0,0 +1,65 @@
+package com.moyerun.moyeorun_android.common
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+
+typealias EventLiveData = LiveData>
+
+class MutableEventLiveData : MutableLiveData>() {
+
+ var event: T?
+ @Deprecated("getter is NOT supported!", level = DeprecationLevel.ERROR)
+ get() = throw UnsupportedOperationException()
+ set(value) {
+ if (value != null) {
+ setValue(Event(value))
+ }
+ }
+
+ fun postEvent(value: T?) {
+ if (value != null) {
+ postValue(Event(value))
+ }
+ }
+}
+
+/**
+ * Used as a wrapper for data that is exposed via a LiveData that represents an event.
+ */
+open class Event(private val content: T) {
+
+ var hasBeenHandled = false
+ private set // Allow external read but not write
+
+ /**
+ * Returns the content and prevents its use again.
+ */
+ fun getContentIfNotHandled(): T? {
+ return if (hasBeenHandled) {
+ null
+ } else {
+ hasBeenHandled = true
+ content
+ }
+ }
+
+ /**
+ * Returns the content, even if it's already been handled.
+ */
+ fun peekContent(): T = content
+}
+
+/**
+ * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has
+ * already been handled.
+ *
+ * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled.
+ */
+class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> {
+ override fun onChanged(event: Event?) {
+ event?.getContentIfNotHandled()?.let { value ->
+ onEventUnhandledContent(value)
+ }
+ }
+}
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/di/CoroutineDispatcherModule.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/di/CoroutineDispatcherModule.kt
new file mode 100644
index 0000000..e1a0d39
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/di/CoroutineDispatcherModule.kt
@@ -0,0 +1,28 @@
+package com.moyerun.moyeorun_android.common.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Qualifier
+
+@Qualifier
+annotation class IODispatcher
+
+@Qualifier
+annotation class DefaultDispatcher
+
+@InstallIn(SingletonComponent::class)
+@Module
+object CoroutineDispatcherModule {
+
+ @IODispatcher
+ @Provides
+ fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+ @DefaultDispatcher
+ @Provides
+ fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/dialog/RoundDialogFragment.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/dialog/RoundDialogFragment.kt
index 24c4ab4..b0d7d9d 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/common/dialog/RoundDialogFragment.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/dialog/RoundDialogFragment.kt
@@ -9,7 +9,7 @@ import android.view.Window
import androidx.fragment.app.DialogFragment
import com.moyerun.moyeorun_android.common.extension.isActivityDestroyed
-open class RoundDialogFragment : DialogFragment() {
+abstract class RoundDialogFragment : DialogFragment() {
private var dismissOnPause = false
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/exceptions/ApiException.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/exceptions/ApiException.kt
index 75c2ab2..990fd23 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/common/exceptions/ApiException.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/exceptions/ApiException.kt
@@ -2,10 +2,14 @@ package com.moyerun.moyeorun_android.common.exceptions
import com.moyerun.moyeorun_android.network.api.Error
-class ApiException(val url: String, val error: Error) : RuntimeException() {
+open class ApiException(val url: String, val error: Error) : RuntimeException() {
val case: String
get() = error.case
override val message: String?
get() = error.message
+
+ override fun toString(): String {
+ return "${super.toString()} url: $url, error: $error"
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt
index af4d5e4..afa7533 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ActivityExtension.kt
@@ -4,6 +4,15 @@ import android.app.Activity
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.moyerun.moyeorun_android.R
+import com.moyerun.moyeorun_android.common.EventLiveData
+import com.moyerun.moyeorun_android.common.EventObserver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
fun Activity.toast(msg: String, isShort: Boolean = true) {
Toast.makeText(this, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show()
@@ -14,4 +23,19 @@ inline fun FragmentActivity.showAllowingStateLoss(
dialogFragmentFactory: () -> DialogFragment
) {
supportFragmentManager.showAllowingStateLoss(tag, dialogFragmentFactory)
+}
+
+fun Activity.showNetworkErrorToast() {
+ toast(getString(R.string.toast_network_error))
+}
+
+fun ComponentActivity.repeatOnStart(block: suspend CoroutineScope.() -> Unit) {
+ lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED, block) }
+}
+
+fun ComponentActivity.observeEvent(
+ event: EventLiveData,
+ onEventUnhandledContent: (T) -> Unit
+) {
+ event.observe(this, EventObserver(onEventUnhandledContent))
}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FirebaseExctension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FirebaseExctension.kt
index 16e6fc4..fae4d4b 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FirebaseExctension.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FirebaseExctension.kt
@@ -21,7 +21,7 @@ fun FirebaseCrashlytics.recordException(logLevel: LogLevel, message: String, thr
class FirebaseLogException : Exception {
constructor(logLevel: LogLevel, message: String) : super("$logLevel : $message")
- constructor(logLevel: LogLevel, cause: Throwable) : super(logLevel.toString(), cause)
+ constructor(logLevel: LogLevel, cause: Throwable) : super("$logLevel : $cause", cause)
constructor(
logLevel: LogLevel,
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt
index e36ee18..46be75b 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/FragmentExtension.kt
@@ -4,6 +4,14 @@ import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.moyerun.moyeorun_android.R
+import com.moyerun.moyeorun_android.common.EventLiveData
+import com.moyerun.moyeorun_android.common.EventObserver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
fun Fragment.toast(msg: String, isShort: Boolean = false) {
Toast.makeText(context, msg, if (isShort) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show()
@@ -19,4 +27,18 @@ inline fun FragmentManager?.showAllowingStateLoss(
val transaction = beginTransaction()
transaction.add(dialogFragmentFactory(), tag)
transaction.commitAllowingStateLoss()
+}
+
+fun Fragment.showNetworkErrorToast() {
+ toast(getString(R.string.toast_network_error))
+}
+
+fun Fragment.repeatOnStart(block: suspend CoroutineScope.() -> Unit) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block)
+ }
+}
+
+fun Fragment.observeEvent(event: EventLiveData, onEventUnhandledContent: (T) -> Unit) {
+ event.observe(viewLifecycleOwner, EventObserver(onEventUnhandledContent))
}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt
index bb0e66e..909c80b 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/common/extension/ViewExtension.kt
@@ -1,6 +1,8 @@
package com.moyerun.moyeorun_android.common.extension
import android.view.View
+import android.widget.RadioButton
+import android.widget.TextView
fun View.setOnDebounceClickListener(interval: Long = 1000L, action: (View?) -> Unit) {
val debounceClickListener = object : View.OnClickListener {
@@ -16,4 +18,17 @@ fun View.setOnDebounceClickListener(interval: Long = 1000L, action: (View?) -> U
}
}
setOnClickListener(debounceClickListener)
+}
+
+fun TextView.setTextIfNew(text: CharSequence?) {
+ if (this.text.contentEquals(text).not()) {
+ setText(text)
+ }
+}
+
+fun RadioButton.setCheckIfNew(check: Boolean) {
+ val oldValue = isChecked
+ if (oldValue != check) {
+ isChecked = check
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/AuthException.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/AuthException.kt
new file mode 100644
index 0000000..5b27baf
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/AuthException.kt
@@ -0,0 +1,6 @@
+package com.moyerun.moyeorun_android.login
+
+import com.moyerun.moyeorun_android.common.exceptions.ApiException
+import com.moyerun.moyeorun_android.network.api.Error
+
+class AuthException(url: String, error: Error): ApiException(url, error)
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/ProviderType.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/ProviderType.kt
new file mode 100644
index 0000000..524ee3b
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/ProviderType.kt
@@ -0,0 +1,9 @@
+package com.moyerun.moyeorun_android.login
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+enum class ProviderType : Parcelable {
+ GOOGLE
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/SignUpMetaData.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/SignUpMetaData.kt
new file mode 100644
index 0000000..62d3796
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/SignUpMetaData.kt
@@ -0,0 +1,16 @@
+package com.moyerun.moyeorun_android.login
+
+import android.os.Parcelable
+import com.moyerun.moyeorun_android.login.ProviderType
+import kotlinx.parcelize.Parcelize
+
+/**
+ * 회원가입을 위해 프로필 설정 화면으로 진입 시
+ * 회원가입 API 호출에 필요한 메타 데이터를 전달하기 위한
+ * DataHolder
+ */
+@Parcelize
+data class SignUpMetaData(
+ val idToken: String,
+ val providerType: ProviderType
+) : Parcelable
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/data/AuthModule.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/data/AuthModule.kt
new file mode 100644
index 0000000..f32dcae
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/data/AuthModule.kt
@@ -0,0 +1,15 @@
+package com.moyerun.moyeorun_android.login.data
+
+import com.moyerun.moyeorun_android.login.data.impl.AuthRepositoryImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+abstract class AuthModule {
+
+ @Binds
+ abstract fun bindsAuthRepository(repository: AuthRepositoryImpl): AuthRepository
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/data/AuthRepository.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/data/AuthRepository.kt
new file mode 100644
index 0000000..d39db81
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/data/AuthRepository.kt
@@ -0,0 +1,16 @@
+package com.moyerun.moyeorun_android.login.data
+
+import com.moyerun.moyeorun_android.login.ProviderType
+import com.moyerun.moyeorun_android.login.data.model.SignInResponse
+import com.moyerun.moyeorun_android.login.data.model.SignUpRequest
+import com.moyerun.moyeorun_android.network.api.Success
+import com.moyerun.moyeorun_android.network.calladapter.ApiResult
+
+interface AuthRepository {
+ suspend fun signIn(
+ idToken: String,
+ providerType: ProviderType
+ ): ApiResult
+
+ suspend fun signUp(signUpRequest: SignUpRequest): ApiResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/data/impl/AuthRepositoryImpl.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/data/impl/AuthRepositoryImpl.kt
new file mode 100644
index 0000000..7742e9f
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/data/impl/AuthRepositoryImpl.kt
@@ -0,0 +1,33 @@
+package com.moyerun.moyeorun_android.login.data.impl
+
+import com.moyerun.moyeorun_android.common.di.IODispatcher
+import com.moyerun.moyeorun_android.login.ProviderType
+import com.moyerun.moyeorun_android.login.data.AuthRepository
+import com.moyerun.moyeorun_android.login.data.model.SignInResponse
+import com.moyerun.moyeorun_android.login.data.model.SignUpRequest
+import com.moyerun.moyeorun_android.network.MoyeorunNetworkDataSource
+import com.moyerun.moyeorun_android.network.calladapter.ApiResult
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+class AuthRepositoryImpl @Inject constructor(
+ private val network: MoyeorunNetworkDataSource,
+ @IODispatcher private val coroutineDispatcher: CoroutineDispatcher
+) : AuthRepository {
+
+ override suspend fun signIn(
+ idToken: String,
+ providerType: ProviderType
+ ): ApiResult {
+ return withContext(coroutineDispatcher) {
+ network.signIn(idToken = idToken, providerType = providerType)
+ }
+ }
+
+ override suspend fun signUp(signUpRequest: SignUpRequest): ApiResult {
+ return withContext(coroutineDispatcher) {
+ network.signUp(signUpRequest)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignInRequest.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignInRequest.kt
new file mode 100644
index 0000000..d7a8ff3
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignInRequest.kt
@@ -0,0 +1,6 @@
+package com.moyerun.moyeorun_android.login.data.model
+
+data class SignInRequest(
+ val idToken: String,
+ val providerType: String
+)
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignInResponse.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignInResponse.kt
new file mode 100644
index 0000000..35c9936
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignInResponse.kt
@@ -0,0 +1,12 @@
+package com.moyerun.moyeorun_android.login.data.model
+
+data class SignInResponse(
+ val token: TokenPair,
+ val userId: String,
+ val isNewUser: Boolean,
+)
+
+data class TokenPair(
+ val accessToken: String,
+ val refreshToken: String,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignUpRequest.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignUpRequest.kt
new file mode 100644
index 0000000..0790c40
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/data/model/SignUpRequest.kt
@@ -0,0 +1,9 @@
+package com.moyerun.moyeorun_android.login.data.model
+
+data class SignUpRequest(
+ val idToken: String,
+ val providerType: String,
+ val nickName: String,
+ val gender: String,
+ val image: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginActivity.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginActivity.kt
new file mode 100644
index 0000000..c418ff2
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginActivity.kt
@@ -0,0 +1,132 @@
+package com.moyerun.moyeorun_android.login.ui
+
+import android.content.Intent
+import android.content.IntentSender
+import android.os.Bundle
+import android.provider.Settings
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.Identity
+import com.google.android.gms.auth.api.identity.SignInClient
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes
+import com.moyerun.moyeorun_android.BuildConfig
+import com.moyerun.moyeorun_android.MainActivity
+import com.moyerun.moyeorun_android.R
+import com.moyerun.moyeorun_android.common.Lg
+import com.moyerun.moyeorun_android.common.extension.observeEvent
+import com.moyerun.moyeorun_android.common.extension.showNetworkErrorToast
+import com.moyerun.moyeorun_android.common.extension.toast
+import com.moyerun.moyeorun_android.databinding.ActivityLoginBinding
+import com.moyerun.moyeorun_android.profile.ui.ProfileEditActivity
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class LoginActivity : AppCompatActivity() {
+
+ private val viewModel: LoginViewModel by viewModels()
+
+ private val oneTapClient: SignInClient by lazy { Identity.getSignInClient(this) }
+ private val signInRequest: BeginSignInRequest by lazy { getBeginSignInRequest() }
+
+ private val beginSignInResultLauncher =
+ registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
+ if (result != null) {
+ try {
+ val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
+ val idToken = credential.googleIdToken
+ if (idToken != null) {
+ Lg.i("Success. token : $idToken")
+ viewModel.googleSignIn(idToken)
+ } else {
+ showUnknownErrorToast()
+ Lg.fe("No ID token")
+ }
+ } catch (e: ApiException) {
+ when (e.statusCode) {
+ CommonStatusCodes.CANCELED -> { /*Doing nothing*/ }
+ CommonStatusCodes.NETWORK_ERROR -> {
+ showNetworkErrorToast()
+ Lg.e("One-tap encountered a network error. $e")
+ }
+ else -> {
+ showUnknownErrorToast()
+ Lg.fe("Couldn't get credential from result.", e)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val binding = ActivityLoginBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ observeEvent(viewModel.loginEvent) { event ->
+ when (event) {
+ is LoginEvent.RegisteredUser -> {
+ startActivity(Intent(this, MainActivity::class.java))
+ finish()
+ }
+ is LoginEvent.NewUser -> {
+ ProfileEditActivity.startActivity(this, event.signUpMetaData)
+ }
+ is LoginEvent.Error -> {
+ showUnknownErrorToast()
+ }
+ }
+ }
+
+ binding.buttonLoginGoogle.setOnClickListener {
+ oneTapClient.beginSignIn(signInRequest)
+ .addOnSuccessListener(this) { result ->
+ try {
+ val intentSenderRequest =
+ IntentSenderRequest.Builder(result.pendingIntent.intentSender).build()
+ beginSignInResultLauncher.launch(intentSenderRequest)
+ } catch (e: IntentSender.SendIntentException) {
+ showUnknownErrorToast()
+ Lg.fe("Couldn't start One Tab UI", e)
+ }
+ }
+ .addOnFailureListener(this) {
+ // 기기에 등록된 계정이 없는 경우 호출
+ startDeviceGoogleSignInActivity()
+ // 간혹 등록된 계정이 있는데도 해당 콜백을 타는 경우가 있어서 로깅
+ Lg.fe("No Google Accounts found", it)
+ }
+ }
+ }
+
+ private fun startDeviceGoogleSignInActivity() {
+ startActivity(Intent(Settings.ACTION_ADD_ACCOUNT).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google"))
+ })
+ }
+
+ private fun getBeginSignInRequest(): BeginSignInRequest {
+ return BeginSignInRequest.builder()
+ .setGoogleIdTokenRequestOptions(
+ BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
+ .setSupported(true)
+ .setServerClientId(BuildConfig.WEB_CLIENT_ID)
+ // false 로 설정해서 앱에 로그인한 적이 있는 계정뿐만 아니라
+ // 기기에 등록된 구글 계정을 모두 보여준다
+ .setFilterByAuthorizedAccounts(false)
+ .build()
+ )
+ // 하나의 계정만 있다면 자동으로 선택
+ .setAutoSelectEnabled(true)
+ .build()
+ }
+
+ private fun showUnknownErrorToast() {
+ toast(getString(R.string.login_toast_unknown_error))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginEvent.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginEvent.kt
new file mode 100644
index 0000000..a5e634b
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginEvent.kt
@@ -0,0 +1,9 @@
+package com.moyerun.moyeorun_android.login.ui
+
+import com.moyerun.moyeorun_android.login.SignUpMetaData
+
+sealed class LoginEvent {
+ object RegisteredUser : LoginEvent()
+ data class NewUser(val signUpMetaData: SignUpMetaData) : LoginEvent()
+ object Error : LoginEvent()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginViewModel.kt b/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginViewModel.kt
new file mode 100644
index 0000000..29132fc
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/login/ui/LoginViewModel.kt
@@ -0,0 +1,53 @@
+package com.moyerun.moyeorun_android.login.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.moyerun.moyeorun_android.common.EventLiveData
+import com.moyerun.moyeorun_android.common.Lg
+import com.moyerun.moyeorun_android.common.MutableEventLiveData
+import com.moyerun.moyeorun_android.login.ProviderType
+import com.moyerun.moyeorun_android.login.SignUpMetaData
+import com.moyerun.moyeorun_android.login.data.AuthRepository
+import com.moyerun.moyeorun_android.network.calladapter.onFailure
+import com.moyerun.moyeorun_android.network.calladapter.onSuccess
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class LoginViewModel @Inject constructor(
+ private val authRepository: AuthRepository
+) : ViewModel() {
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading: StateFlow
+ get() = _isLoading
+
+ private val _loginEvent = MutableEventLiveData()
+ val loginEvent: EventLiveData
+ get() = _loginEvent
+
+ fun googleSignIn(idToken: String) {
+ signInInternal(idToken, ProviderType.GOOGLE)
+ }
+
+ private fun signInInternal(idToken: String, providerType: ProviderType) {
+ viewModelScope.launch {
+ _isLoading.value = true
+ authRepository.signIn(idToken, providerType)
+ .onSuccess {
+ _loginEvent.event = if (it.isNewUser) {
+ LoginEvent.NewUser(SignUpMetaData(idToken, providerType))
+ } else {
+ LoginEvent.RegisteredUser
+ }
+ }.onFailure {
+ Lg.fe(it)
+ _loginEvent.event = LoginEvent.Error
+ }
+ _isLoading.value = false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunNetworkDataSource.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunNetworkDataSource.kt
new file mode 100644
index 0000000..4a0d909
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunNetworkDataSource.kt
@@ -0,0 +1,17 @@
+package com.moyerun.moyeorun_android.network
+
+import com.moyerun.moyeorun_android.login.ProviderType
+import com.moyerun.moyeorun_android.login.data.model.SignInResponse
+import com.moyerun.moyeorun_android.login.data.model.SignUpRequest
+import com.moyerun.moyeorun_android.network.api.Success
+import com.moyerun.moyeorun_android.network.calladapter.ApiResult
+
+interface MoyeorunNetworkDataSource {
+
+ suspend fun signIn(
+ idToken: String,
+ providerType: ProviderType
+ ): ApiResult
+
+ suspend fun signUp(signUpRequest: SignUpRequest): ApiResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunNetworkDataSourceImpl.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunNetworkDataSourceImpl.kt
new file mode 100644
index 0000000..a16e6fa
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunNetworkDataSourceImpl.kt
@@ -0,0 +1,29 @@
+package com.moyerun.moyeorun_android.network
+
+import com.moyerun.moyeorun_android.login.ProviderType
+import com.moyerun.moyeorun_android.login.data.model.SignInRequest
+import com.moyerun.moyeorun_android.login.data.model.SignInResponse
+import com.moyerun.moyeorun_android.login.data.model.SignUpRequest
+import com.moyerun.moyeorun_android.network.api.Success
+import com.moyerun.moyeorun_android.network.calladapter.ApiResult
+
+class MoyeorunNetworkDataSourceImpl(
+ private val moyeorunService: MoyeorunService
+) : MoyeorunNetworkDataSource {
+
+ override suspend fun signIn(
+ idToken: String,
+ providerType: ProviderType
+ ): ApiResult {
+ return moyeorunService.signIn(
+ SignInRequest(
+ idToken = idToken,
+ providerType = providerType.name.uppercase()
+ )
+ )
+ }
+
+ override suspend fun signUp(signUpRequest: SignUpRequest): ApiResult {
+ return moyeorunService.signUp(signUpRequest)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunService.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunService.kt
new file mode 100644
index 0000000..7e00e6c
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/MoyeorunService.kt
@@ -0,0 +1,18 @@
+package com.moyerun.moyeorun_android.network
+
+import com.moyerun.moyeorun_android.login.data.model.SignInRequest
+import com.moyerun.moyeorun_android.login.data.model.SignInResponse
+import com.moyerun.moyeorun_android.login.data.model.SignUpRequest
+import com.moyerun.moyeorun_android.network.api.Success
+import com.moyerun.moyeorun_android.network.calladapter.ApiResult
+import retrofit2.http.Body
+import retrofit2.http.POST
+
+interface MoyeorunService {
+
+ @POST("/api/auth/sign-in")
+ suspend fun signIn(@Body signInRequest: SignInRequest): ApiResult
+
+ @POST("/api/auth/sign-up")
+ suspend fun signUp(@Body signUpRequest: SignUpRequest): ApiResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiErrorCase.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiErrorCase.kt
index abba91d..53d3934 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiErrorCase.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiErrorCase.kt
@@ -1,20 +1,29 @@
package com.moyerun.moyeorun_android.network.api
import com.moyerun.moyeorun_android.common.exceptions.ApiException
+import com.moyerun.moyeorun_android.login.AuthException
enum class ApiErrorCase(val case: String) {
// 서버와 합의된 에러 케이스들을 정의합니다.
- NOT_LOGIN("100"), // TODO : 예시입니다. 실제 로그인 작업 시 수정해주세요.
+ AUTHORIZATION_FAIL("101"),
+ EXPIRE_JWT("104"),
+ NOT_LOGIN("105"),
+ DUPLICATE_NICKNAME("110"),
+ DUPLICATE_USER("111"),
UNKNOWN("999");
companion object {
- private fun findError(case: String): ApiErrorCase {
+ fun findError(case: String): ApiErrorCase {
return values().find { it.case == case } ?: UNKNOWN
}
fun getException(url: String, error: Error, cause: Throwable? = null): Throwable {
- return when(findError(error.case)) {
- NOT_LOGIN -> { TODO() }
+ return when (findError(error.case)) {
+ AUTHORIZATION_FAIL,
+ EXPIRE_JWT,
+ NOT_LOGIN,
+ DUPLICATE_NICKNAME,
+ DUPLICATE_USER -> AuthException(url, error)
else -> ApiException(url, error)
}
}
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiService.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/api/NetworkResponse.kt
similarity index 50%
rename from app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiService.kt
rename to app/src/main/java/com/moyerun/moyeorun_android/network/api/NetworkResponse.kt
index 9b3d4dc..1d31e05 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/network/api/ApiService.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/api/NetworkResponse.kt
@@ -1,5 +1,5 @@
package com.moyerun.moyeorun_android.network.api
-interface ApiService {
-
-}
\ No newline at end of file
+data class NetworkResponse(
+ val data: T
+)
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt
index e21e8cf..a887750 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/calladapter/ApiResult.kt
@@ -1,5 +1,8 @@
package com.moyerun.moyeorun_android.network.calladapter
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
/**
* Success : API 호출 성공 시, body를 Wrapping 합니다.
* Failure : API 호출 실패 시, throwable을 Wrpping 합니다.
@@ -7,4 +10,25 @@ package com.moyerun.moyeorun_android.network.calladapter
sealed class ApiResult {
data class Success(val body: T) : ApiResult()
data class Failure(val throwable: Throwable) : ApiResult()
+}
+
+inline fun ApiResult.onSuccess(action: (data: T) -> Unit): ApiResult {
+ if (this is ApiResult.Success) {
+ action.invoke(body)
+ }
+ return this
+}
+
+inline fun ApiResult.onFailure(action: (throwable: Throwable) -> Unit): ApiResult {
+ if (this is ApiResult.Failure) {
+ action.invoke(throwable)
+ }
+ return this
+}
+
+inline fun ApiResult.map(transform: (value: T) -> R): ApiResult {
+ return when (this) {
+ is ApiResult.Success -> ApiResult.Success(transform(body))
+ is ApiResult.Failure -> ApiResult.Failure(throwable)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/client/Retrofit.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/client/Retrofit.kt
deleted file mode 100644
index f0146ef..0000000
--- a/app/src/main/java/com/moyerun/moyeorun_android/network/client/Retrofit.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.moyerun.moyeorun_android.network.client
-
-import com.moyerun.moyeorun_android.BuildConfig
-import com.moyerun.moyeorun_android.network.MoyeorunJsonConverterFactory
-import com.moyerun.moyeorun_android.network.api.ApiService
-import com.moyerun.moyeorun_android.network.calladapter.ApiResultCallAdapterFactory
-import retrofit2.Retrofit
-
-private const val BASE_URL = BuildConfig.BASE_URL
-
-val retrofit: Retrofit = Retrofit.Builder()
- .baseUrl(BASE_URL)
- .addCallAdapterFactory(ApiResultCallAdapterFactory())
- .addConverterFactory(MoyeorunJsonConverterFactory.create())
- .build()
-
-val apiService: ApiService = retrofit.create(ApiService::class.java)
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/network/di/NetworkModule.kt b/app/src/main/java/com/moyerun/moyeorun_android/network/di/NetworkModule.kt
new file mode 100644
index 0000000..e862ca2
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/network/di/NetworkModule.kt
@@ -0,0 +1,34 @@
+package com.moyerun.moyeorun_android.network.di
+
+import com.moyerun.moyeorun_android.BuildConfig
+import com.moyerun.moyeorun_android.network.MoyeorunJsonConverterFactory
+import com.moyerun.moyeorun_android.network.MoyeorunNetworkDataSource
+import com.moyerun.moyeorun_android.network.MoyeorunNetworkDataSourceImpl
+import com.moyerun.moyeorun_android.network.MoyeorunService
+import com.moyerun.moyeorun_android.network.calladapter.ApiResultCallAdapterFactory
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import retrofit2.Retrofit
+
+private const val BASE_URL = BuildConfig.BASE_URL
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NetworkModule {
+
+ @Provides
+ fun providesRetrofit(): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl(BASE_URL)
+ .addCallAdapterFactory(ApiResultCallAdapterFactory())
+ .addConverterFactory(MoyeorunJsonConverterFactory.create())
+ .build()
+ }
+
+ @Provides
+ fun providesMoyeorunNetworkDataSource(retrofit: Retrofit): MoyeorunNetworkDataSource {
+ return MoyeorunNetworkDataSourceImpl(retrofit.create(MoyeorunService::class.java))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileEditActivity.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileEditActivity.kt
new file mode 100644
index 0000000..027170d
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileEditActivity.kt
@@ -0,0 +1,172 @@
+package com.moyerun.moyeorun_android.profile.ui
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.widget.doAfterTextChanged
+import com.moyerun.moyeorun_android.MainActivity
+import com.moyerun.moyeorun_android.R
+import com.moyerun.moyeorun_android.common.Lg
+import com.moyerun.moyeorun_android.common.extension.*
+import com.moyerun.moyeorun_android.databinding.ActivityProfileBinding
+import com.moyerun.moyeorun_android.login.SignUpMetaData
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class ProfileEditActivity : AppCompatActivity() {
+
+ private val viewModel: ProfileEditViewModel by viewModels()
+
+ private val galleryLauncher =
+ registerForActivityResult(ActivityResultContracts.GetContent()) { imageUri ->
+ if (imageUri != null) {
+ viewModel.onImageUrlChanged(imageUri)
+ } else {
+ Lg.fw("Cannot get Image Uri from gallery")
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val binding = ActivityProfileBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val signUpMetaData: SignUpMetaData? = intent.getParcelableExtra(EXTRA_SIGN_UP_META_DATA)
+ val originalProfile: ProfileUiModel? = intent.getParcelableExtra(EXTRA_PROFILE_UI_MODEL)
+ val isNewProfile = (originalProfile == null && signUpMetaData != null)
+
+ if (isNewProfile) {
+ binding.textviewProfileTitle.text = "기본 정보"
+ binding.buttonProfileConfirm.text = "다음"
+ } else {
+ binding.textviewProfileTitle.text = "프로필"
+ binding.buttonProfileConfirm.text = "완료"
+ }
+
+ viewModel.updateData(signUpMetaData, originalProfile)
+
+ binding.edittextProfileNickname.doAfterTextChanged {
+ viewModel.onNicknameChanged(it?.toString().orEmpty())
+ }
+
+ binding.badgeimageviewProfileImage.setOnDebounceClickListener {
+ showAllowingStateLoss("selectImage") {
+ ProfileImageSelectDialogFragment.getInstance(
+ onGalleryClick = {
+ galleryLauncher.launch("image/*")
+ },
+ onDefaultImagesClick = {
+ // Todo : 기본 이미지 선택 화면
+ }
+ )
+ }
+ }
+
+ binding.radiogroupProfileGender.setOnCheckedChangeListener { _, checkedId ->
+ val gender = when (checkedId) {
+ R.id.radiobutton_profile_man -> Gender.MALE
+ R.id.radiobutton_profile_woman -> Gender.FEMALE
+ else -> Gender.NONE
+ }
+ viewModel.onGenderChanged(gender)
+ }
+
+ binding.buttonProfileConfirm.setOnDebounceClickListener {
+ if (isNewProfile) {
+ viewModel.signUp()
+ }
+ }
+
+ repeatOnStart {
+ launch {
+ viewModel.profileUiModel
+ .map { it.nickname }
+ .distinctUntilChanged()
+ .collect {
+ binding.edittextProfileNickname.setTextIfNew(it)
+ }
+ }
+ launch {
+ viewModel.profileUiModel
+ .map { it.imageUri }
+ .distinctUntilChanged()
+ .collect {
+ binding.badgeimageviewProfileImage.setBigCircleImgSrc(it)
+ }
+ }
+ launch {
+ viewModel.profileUiModel
+ .map { it.gender }
+ .distinctUntilChanged()
+ .collect {
+ when (it) {
+ Gender.MALE -> binding.radiobuttonProfileMan.setCheckIfNew(true)
+ Gender.FEMALE -> binding.radiobuttonProfileWoman.setCheckIfNew(true)
+ Gender.NONE -> binding.radiogroupProfileGender.clearCheck()
+ }
+ }
+ }
+ launch {
+ viewModel.isButtonEnabled
+ .collect { buttonEnabled ->
+ binding.buttonProfileConfirm.isEnabled = buttonEnabled
+ }
+ }
+ }
+
+ observeEvent(viewModel.profileEvent) {
+ when (it) {
+ ProfileEvent.SUCCESS_SIGN_UP -> {
+ startActivity(Intent(this, MainActivity::class.java))
+ }
+ }
+ }
+
+ observeEvent(viewModel.profileErrorEvent) {
+ when (it) {
+ ProfileError.DUPLICATE_NICKNAME -> {
+ binding.alertDuplicate()
+ }
+ ProfileError.WRONG_ACCESS -> {
+ toast(getString(R.string.profile_toast_wrong_access))
+ finish()
+ }
+ ProfileError.UNKNOWN, ProfileError.UNKNOWN_AUTH -> {
+ toast(getString(R.string.profile_toast_unknown_error))
+ }
+ ProfileError.NETWORK -> {
+ showNetworkErrorToast()
+ }
+ }
+ }
+ }
+
+ private fun ActivityProfileBinding.alertDuplicate() {
+ edittextProfileNickname.setTextAppearance(R.style.Profile_Input_Wrong)
+ }
+
+ companion object {
+ private const val EXTRA_PROFILE_UI_MODEL = "profileUiModel"
+ private const val EXTRA_SIGN_UP_META_DATA = "signUpMetaData"
+
+ // 프로필 수정 시 사용
+ fun startActivity(context: Context, profileUiModel: ProfileUiModel) {
+ context.startActivity(Intent(context, ProfileEditActivity::class.java).apply {
+ putExtra(EXTRA_PROFILE_UI_MODEL, profileUiModel)
+ })
+ }
+
+ // 회원가입 시 사용
+ fun startActivity(context: Context, signUpMetaData: SignUpMetaData) {
+ context.startActivity(Intent(context, ProfileEditActivity::class.java).apply {
+ putExtra(EXTRA_SIGN_UP_META_DATA, signUpMetaData)
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileEditViewModel.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileEditViewModel.kt
new file mode 100644
index 0000000..7c386f2
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileEditViewModel.kt
@@ -0,0 +1,148 @@
+package com.moyerun.moyeorun_android.profile.ui
+
+import android.net.Uri
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.moyerun.moyeorun_android.common.EventLiveData
+import com.moyerun.moyeorun_android.common.Lg
+import com.moyerun.moyeorun_android.common.MutableEventLiveData
+import com.moyerun.moyeorun_android.common.exceptions.NetworkException
+import com.moyerun.moyeorun_android.login.AuthException
+import com.moyerun.moyeorun_android.login.data.AuthRepository
+import com.moyerun.moyeorun_android.login.SignUpMetaData
+import com.moyerun.moyeorun_android.network.api.ApiErrorCase
+import com.moyerun.moyeorun_android.network.calladapter.onFailure
+import com.moyerun.moyeorun_android.network.calladapter.onSuccess
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class ProfileEditViewModel @Inject constructor(
+ private val authRepository: AuthRepository
+) : ViewModel() {
+
+ private val _profileUiModel = MutableStateFlow(ProfileUiModel())
+ val profileUiModel: StateFlow
+ get() = _profileUiModel
+
+ private val _profileEvent = MutableEventLiveData()
+ val profileEvent: EventLiveData
+ get() = _profileEvent
+
+ private val _profileErrorEvent = MutableEventLiveData()
+ val profileErrorEvent: EventLiveData
+ get() = _profileErrorEvent
+
+ private val _isButtonEnabled = MutableStateFlow(false)
+ val isButtonEnabled: StateFlow
+ get() = _isButtonEnabled
+
+ private var signUpMetaData: SignUpMetaData? = null
+ private var oldProfileUiModel: ProfileUiModel? = null
+
+ private var isNewProfile = true
+
+ fun updateData(signUpMetaData: SignUpMetaData?, profileUiModel: ProfileUiModel?) {
+ when {
+ signUpMetaData == null && profileUiModel != null -> {
+ _profileUiModel.update { profileUiModel }
+ oldProfileUiModel = profileUiModel
+ isNewProfile = false
+ }
+ signUpMetaData != null && profileUiModel == null -> {
+ this.signUpMetaData = signUpMetaData
+ isNewProfile = true
+ }
+ else -> {
+ Lg.fw("Wrong access. signUpMetadata: $signUpMetaData, originProfile: $profileUiModel")
+ _profileErrorEvent.event = ProfileError.WRONG_ACCESS
+ }
+ }
+ }
+
+ fun onNicknameChanged(nickname: String) = updateWithValidate {
+ _profileUiModel.update {
+ it.copy(nickname = nickname)
+ }
+ _profileUiModel.value
+ }
+
+ fun onImageUrlChanged(imageUri: Uri) {
+ _profileUiModel.update {
+ it.copy(imageUri = imageUri)
+ }
+ }
+
+ fun onGenderChanged(gender: Gender) = updateWithValidate {
+ _profileUiModel.update {
+ it.copy(gender = gender)
+ }
+ _profileUiModel.value
+ }
+
+ fun signUp() {
+ val metaData = signUpMetaData
+ if (metaData == null) {
+ Lg.fw("SignUp Failure. meta data is null")
+ _profileErrorEvent.event = ProfileError.UNKNOWN
+ return
+ }
+ viewModelScope.launch {
+ authRepository.signUp(profileUiModel.value.toSignUpRequest(metaData))
+ .onSuccess {
+ _profileEvent.event = ProfileEvent.SUCCESS_SIGN_UP
+ Lg.d(it.toString())
+ }
+ .onFailure { throwable ->
+ when (throwable) {
+ is AuthException -> throwable.onAuthFailure()
+ is NetworkException -> throwable.onNetworkFailure()
+ else -> throwable.onUnknownFailure()
+ }
+ }
+ }
+ }
+
+ private fun updateWithValidate(updateAction: () -> ProfileUiModel) {
+ val updatedProfileUiModel = updateAction.invoke()
+ _isButtonEnabled.update {
+ updatedProfileUiModel.validate()
+ }
+ }
+
+ private fun ProfileUiModel.validate(): Boolean {
+ return nickname.validateNickname()
+ && gender != Gender.NONE
+ }
+
+ private fun String.validateNickname(): Boolean {
+ return isNotEmpty()
+ }
+
+ private fun AuthException.onAuthFailure() {
+ when (ApiErrorCase.findError(case)) {
+ ApiErrorCase.DUPLICATE_NICKNAME -> {
+ _profileErrorEvent.event = ProfileError.DUPLICATE_NICKNAME
+ Lg.e(this)
+ }
+ else -> {
+ _profileErrorEvent.event = ProfileError.UNKNOWN_AUTH
+ Lg.fe(this)
+ }
+ }
+ }
+
+ private fun NetworkException.onNetworkFailure() {
+ _profileErrorEvent.event = ProfileError.NETWORK
+ Lg.e(this)
+ }
+
+ private fun Throwable.onUnknownFailure() {
+ _profileErrorEvent.event = ProfileError.UNKNOWN
+ Lg.fe(this)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileImageSelectDialogFragment.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileImageSelectDialogFragment.kt
new file mode 100644
index 0000000..89f7f14
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileImageSelectDialogFragment.kt
@@ -0,0 +1,50 @@
+package com.moyerun.moyeorun_android.profile.ui
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.moyerun.moyeorun_android.common.dialog.RoundDialogFragment
+import com.moyerun.moyeorun_android.common.extension.setOnDebounceClickListener
+import com.moyerun.moyeorun_android.databinding.DialogProfileImageSelectBinding
+
+class ProfileImageSelectDialogFragment : RoundDialogFragment() {
+
+ private var onGalleryClick: () -> Unit = {}
+ private var onDefaultImagesClick: () -> Unit = {}
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ return DialogProfileImageSelectBinding.inflate(inflater, container, false).root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val binding = DialogProfileImageSelectBinding.bind(view)
+
+ binding.buttonImageSelectGallery.setOnDebounceClickListener {
+ onGalleryClick.invoke()
+ dismissAllowingStateLoss()
+ }
+
+ binding.buttonImageSelectDefault.setOnDebounceClickListener {
+ onDefaultImagesClick.invoke()
+ dismissAllowingStateLoss()
+ }
+ }
+
+ companion object {
+ fun getInstance(
+ onGalleryClick: () -> Unit,
+ onDefaultImagesClick: () -> Unit
+ ): ProfileImageSelectDialogFragment {
+ return ProfileImageSelectDialogFragment().apply {
+ this.onGalleryClick = onGalleryClick
+ this.onDefaultImagesClick = onDefaultImagesClick
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileUiModel.kt b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileUiModel.kt
new file mode 100644
index 0000000..1d10c14
--- /dev/null
+++ b/app/src/main/java/com/moyerun/moyeorun_android/profile/ui/ProfileUiModel.kt
@@ -0,0 +1,40 @@
+package com.moyerun.moyeorun_android.profile.ui
+
+import android.net.Uri
+import android.os.Parcelable
+import com.moyerun.moyeorun_android.login.data.model.SignUpRequest
+import com.moyerun.moyeorun_android.login.SignUpMetaData
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class ProfileUiModel(
+ val imageUri: Uri = Uri.EMPTY,
+ val nickname: String = "",
+ val gender: Gender = Gender.NONE
+) : Parcelable
+
+fun ProfileUiModel.toSignUpRequest(signUpMetaData: SignUpMetaData): SignUpRequest {
+ return SignUpRequest(
+ idToken = signUpMetaData.idToken,
+ providerType = signUpMetaData.providerType.name,
+ nickName = nickname,
+ gender = gender.name,
+ image = "TEST" //TODO: 이미지 Uri 넣기
+ )
+}
+
+enum class ProfileEvent {
+ SUCCESS_SIGN_UP
+}
+
+enum class ProfileError {
+ WRONG_ACCESS,
+ DUPLICATE_NICKNAME,
+ UNKNOWN_AUTH,
+ NETWORK,
+ UNKNOWN
+}
+
+enum class Gender {
+ MALE, FEMALE, NONE
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt b/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt
index 9108791..59b41e3 100644
--- a/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt
+++ b/app/src/main/java/com/moyerun/moyeorun_android/views/BadgeRoundImageView.kt
@@ -2,6 +2,7 @@ package com.moyerun.moyeorun_android.views
import android.Manifest
import android.content.Context
+import android.net.Uri
import android.util.AttributeSet
import android.util.Patterns
import android.view.LayoutInflater
@@ -88,9 +89,17 @@ class BadgeRoundImageView @JvmOverloads constructor(
@RequiresPermission(Manifest.permission.INTERNET)
fun setBigCircleImgSrc(imgUrl: String, isImageCropped: Boolean = false) {
if (isImageCropped) {
- Glide.with(context).load(imgUrl).centerCrop().into(binding.imgBigCircle)
+ Glide.with(context).load(imgUrl).centerCrop()
+ .placeholder(R.drawable.user_profile_image_default_112dp).into(binding.imgBigCircle)
} else
- Glide.with(context).load(imgUrl).into(binding.imgBigCircle)
+ Glide.with(context).load(imgUrl)
+ .placeholder(R.drawable.user_profile_image_default_112dp).into(binding.imgBigCircle)
+ }
+
+ fun setBigCircleImgSrc(imageUri: Uri) {
+ Glide.with(context).load(imageUri).centerCrop()
+ .placeholder(R.drawable.user_profile_image_default_112dp)
+ .into(binding.imgBigCircle)
}
fun setBigCircleImageBg(@ColorRes bgResId: Int) {
diff --git a/app/src/main/res/drawable/gender_background.xml b/app/src/main/res/drawable/gender_background.xml
new file mode 100644
index 0000000..9a187a5
--- /dev/null
+++ b/app/src/main/res/drawable/gender_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/gender_background_checked.xml b/app/src/main/res/drawable/gender_background_checked.xml
new file mode 100644
index 0000000..9503dee
--- /dev/null
+++ b/app/src/main/res/drawable/gender_background_checked.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/gender_button_man.xml b/app/src/main/res/drawable/gender_button_man.xml
new file mode 100644
index 0000000..7aaffa5
--- /dev/null
+++ b/app/src/main/res/drawable/gender_button_man.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/gender_button_man_checked.xml b/app/src/main/res/drawable/gender_button_man_checked.xml
new file mode 100644
index 0000000..1238092
--- /dev/null
+++ b/app/src/main/res/drawable/gender_button_man_checked.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/gender_button_woman.xml b/app/src/main/res/drawable/gender_button_woman.xml
new file mode 100644
index 0000000..1b3c591
--- /dev/null
+++ b/app/src/main/res/drawable/gender_button_woman.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/gender_button_woman_checked.xml b/app/src/main/res/drawable/gender_button_woman_checked.xml
new file mode 100644
index 0000000..1c8be3a
--- /dev/null
+++ b/app/src/main/res/drawable/gender_button_woman_checked.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_gender_man.xml b/app/src/main/res/drawable/ic_gender_man.xml
new file mode 100644
index 0000000..95fc05c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gender_man.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_gender_man_white.xml b/app/src/main/res/drawable/ic_gender_man_white.xml
new file mode 100644
index 0000000..71745b9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gender_man_white.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_gender_woman.xml b/app/src/main/res/drawable/ic_gender_woman.xml
new file mode 100644
index 0000000..1c22bb6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gender_woman.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_gender_woman_white.xml b/app/src/main/res/drawable/ic_gender_woman_white.xml
new file mode 100644
index 0000000..87715ff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gender_woman_white.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_toolbar_back.xml b/app/src/main/res/drawable/ic_toolbar_back.xml
new file mode 100644
index 0000000..128ab24
--- /dev/null
+++ b/app/src/main/res/drawable/ic_toolbar_back.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/login_google_logo.xml b/app/src/main/res/drawable/login_google_logo.xml
new file mode 100644
index 0000000..5051ec3
--- /dev/null
+++ b/app/src/main/res/drawable/login_google_logo.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/login_logo.xml b/app/src/main/res/drawable/login_logo.xml
new file mode 100644
index 0000000..f901f5d
--- /dev/null
+++ b/app/src/main/res/drawable/login_logo.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/round_background_12.xml b/app/src/main/res/drawable/round_background_12.xml
index cfb6cbf..06b257f 100644
--- a/app/src/main/res/drawable/round_background_12.xml
+++ b/app/src/main/res/drawable/round_background_12.xml
@@ -1,9 +1,6 @@
-
- -
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selector_gender_man.xml b/app/src/main/res/drawable/selector_gender_man.xml
new file mode 100644
index 0000000..3dd0ff1
--- /dev/null
+++ b/app/src/main/res/drawable/selector_gender_man.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selector_gender_woman.xml b/app/src/main/res/drawable/selector_gender_woman.xml
new file mode 100644
index 0000000..136e3c9
--- /dev/null
+++ b/app/src/main/res/drawable/selector_gender_woman.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/selector_round_button_text.xml b/app/src/main/res/drawable/selector_round_button_text.xml
deleted file mode 100644
index 5ef8ee7..0000000
--- a/app/src/main/res/drawable/selector_round_button_text.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/shape_border_warning_edittext.xml b/app/src/main/res/drawable/shape_border_warning_edittext.xml
new file mode 100644
index 0000000..750652b
--- /dev/null
+++ b/app/src/main/res/drawable/shape_border_warning_edittext.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/toolbar_divider_background.xml b/app/src/main/res/drawable/toolbar_divider_background.xml
new file mode 100644
index 0000000..3a8ba01
--- /dev/null
+++ b/app/src/main/res/drawable/toolbar_divider_background.xml
@@ -0,0 +1,15 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
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..773fbc8
--- /dev/null
+++ b/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml
new file mode 100644
index 0000000..12891b8
--- /dev/null
+++ b/app/src/main/res/layout/activity_profile.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_profile_image_select.xml b/app/src/main/res/layout/dialog_profile_image_select.xml
new file mode 100644
index 0000000..a34e39d
--- /dev/null
+++ b/app/src/main/res/layout/dialog_profile_image_select.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index ab23f44..6f6bb5e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -14,9 +14,12 @@
#333333
#828282
#A9A9A9
+ #686868
#EBECEF
+ #D4D4D4
+ #D4D4D4
#0047D0
@@ -25,4 +28,6 @@
#D4D4D4
+ #FF3A3A
+ #FFF8F8
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 09e4982..7f69461 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,5 +4,20 @@
취소
확인
+ 네트워크 연결 상태를 확인해주세요.
+
+
+ 시작하기
+ 구글 계정으로 로그인
+ 로그인에 실패하였습니다.
+
+
+ 이름
+ 닉네임
+ 성별
+ 갤러리에서 선택
+ 기본 이미지에서 선택
+ 잘못된 접근입니다.
+ 회원가입에 실패했습니다.
\ No newline at end of file
diff --git a/app/src/main/res/values/styles_button.xml b/app/src/main/res/values/styles_button.xml
index 1841097..97cfbd2 100644
--- a/app/src/main/res/values/styles_button.xml
+++ b/app/src/main/res/values/styles_button.xml
@@ -1,8 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles_dialog.xml b/app/src/main/res/values/styles_dialog.xml
index 55a64cf..61c771f 100644
--- a/app/src/main/res/values/styles_dialog.xml
+++ b/app/src/main/res/values/styles_dialog.xml
@@ -27,6 +27,12 @@
- 0dp
- 21dp
- 16dp
+ - @android:color/transparent
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles_toolbar.xml b/app/src/main/res/values/styles_toolbar.xml
new file mode 100644
index 0000000..d2d2534
--- /dev/null
+++ b/app/src/main/res/values/styles_toolbar.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index edf283a..7088d0b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,7 +12,7 @@ buildscript {
plugins {
id 'com.android.application' version '7.1.0' apply false
id 'com.android.library' version '7.1.0' apply false
- id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
+ id 'org.jetbrains.kotlin.android' version '1.6.20' apply false
}
task clean(type: Delete) {