diff --git a/app/build.gradle b/app/build.gradle index ebde7f8..12e2727 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,8 @@ plugins { id 'kotlin-kapt' id 'com.google.dagger.hilt.android' id 'androidx.navigation.safeargs.kotlin' + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' } android { @@ -99,4 +101,15 @@ dependencies { // logging implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" + // 구글 로그인 모듈 + implementation 'com.google.android.gms:play-services-auth:20.5.0' + + implementation platform('com.google.firebase:firebase-bom:32.1.0') + implementation 'com.google.firebase:firebase-analytics-ktx' + implementation 'com.google.firebase:firebase-crashlytics-ktx' + implementation 'com.google.firebase:firebase-messaging-ktx' + implementation 'com.google.firebase:firebase-auth-ktx' + + // 카카오 로그인 모듈 + implementation "com.kakao.sdk:v2-user:2.14.0" } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f996e05..84040ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -56,6 +56,28 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/dining/coach/ui/login/LoginActivity.kt b/app/src/main/java/com/dining/coach/ui/login/LoginActivity.kt index 6c442a9..ce3ab1a 100644 --- a/app/src/main/java/com/dining/coach/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/dining/coach/ui/login/LoginActivity.kt @@ -1,19 +1,297 @@ package com.dining.coach.ui.login +import android.content.Intent +import android.util.Log +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import com.dining.coach.R import com.dining.coach.base.BaseActivity import com.dining.coach.base.BaseViewModel import com.dining.coach.databinding.ActivityLoginBinding -import com.dining.coach.ui.splash.SplashViewModel +import com.dining.coach.ui.main.MainActivity +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.tasks.Task +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.GoogleAuthProvider +import com.kakao.sdk.auth.AuthApiClient +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.KakaoSdk +import com.kakao.sdk.common.model.AuthErrorCause +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.common.model.KakaoSdkError +import com.kakao.sdk.common.util.Utility +import com.kakao.sdk.user.UserApiClient import dagger.hilt.android.AndroidEntryPoint + @AndroidEntryPoint -class LoginActivity: BaseActivity(R.layout.activity_login) { +class LoginActivity : BaseActivity(R.layout.activity_login) { + private lateinit var googleSignInClient: GoogleSignInClient + private val auth = FirebaseAuth.getInstance() + private val user = auth.currentUser + private val viewModel: LoginViewModel by viewModels() override fun createActivity(): BaseViewModel { + initKakaoSignInSetting() + initGoogleSignInSetting() + setOnClickListeners() + bind.vm = viewModel return viewModel } + + private fun initKakaoSignInSetting() { + val keyHash = Utility.getKeyHash(this) + Log.e("Key", "keyHash: $keyHash") + /** Kakao SDK init */ + KakaoSdk.init(this, this.getString(R.string.kakao_app_key)) + } + + private fun initGoogleSignInSetting() { + // 앱에 필요한 사용자 데이터를 요청하도록 로그인 옵션을 설정. + // 유저 아이디와 기본 프로필 정보 요청 + val signIn = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() + + googleSignInClient = GoogleSignIn.getClient(this, signIn) + } + + private fun setOnClickListeners() { + bind.btnKakao.setOnClickListener { + signInWithKakao() + } + + bind.btnGoogle.setOnClickListener { + signInWithGoogle() + } + } + + override fun onResume() { + super.onResume() +// autoSignInWithKakao() +// autoSignInWithGoogle() + } + + + /* + * 카카오 소셜 로그인 + */ + + private fun signInWithKakao() { + // 카카오 계정으로 로그인 공통 callback 구성 + // 카카오톡으로 로그인 할 수 없어 카카오 계정으로 로그인할 경우 사용됨 + val callback: (OAuthToken?, Throwable?) -> Unit = { tokenInfo, error -> + if (error != null) { + Log.e("signInWithKakao - 1", "카카오 계정으로 로그인 실패", error) + when { + error.toString() == AuthErrorCause.AccessDenied.toString() -> + Toast.makeText(this, getString(R.string.access_denied), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidClient.toString() -> + Toast.makeText(this, getString(R.string.invalid_client), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidGrant.toString() -> + Toast.makeText(this, getString(R.string.invalid_grant), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidRequest.toString() -> + Toast.makeText(this, getString(R.string.invalid_request), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidScope.toString() -> + Toast.makeText(this, getString(R.string.invalid_scope), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.Misconfigured.toString() -> + Toast.makeText(this, getString(R.string.mis_configured), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.ServerError.toString() -> + Toast.makeText(this, getString(R.string.server_error), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.Unauthorized.toString() -> + Toast.makeText(this, getString(R.string.unauthorized), Toast.LENGTH_SHORT).show() + else -> + Toast.makeText(this, getString(R.string.etc_error), Toast.LENGTH_SHORT).show() + } + } + else if (tokenInfo != null) { + onSignInSuccess() + Log.i("signInWithKakao - 2", "카카오 계정으로 로그인 성공 ${tokenInfo.accessToken}") + } + } + + // 로그인을 통해 발급 받은 토큰이 있는지 확인 +// checkAuthenticatedToken(callback) + + // 해당 기기에 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오 계정으로 로그인 + if (UserApiClient.instance.isKakaoTalkLoginAvailable(this)) { + UserApiClient.instance.loginWithKakaoTalk(this) { tokenInfo, error -> + if (error != null) { + Log.e("signInWithKakao - 3", "카카오톡으로 로그인 실패", error) + + // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우, + // 의도적인 로그인 취소로 보고 카카오 계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기) + if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { + return@loginWithKakaoTalk + } + + // 카카오톡에 연결된 카카오 계정이 없는 경우, 카카오 계정으로 로그인 시도 + UserApiClient.instance.loginWithKakaoAccount(this, callback = callback) + } else if (tokenInfo != null) { + onSignInSuccess() + Log.i("signInWithKakao - 4", "카카오톡으로 로그인 성공 : ${tokenInfo.accessToken}") + } + } + } else { + UserApiClient.instance.loginWithKakaoAccount(this, callback = callback) + } + } + + private fun checkAuthenticatedToken(callback: (OAuthToken?, Throwable?) -> Unit) { + if (AuthApiClient.instance.hasToken()) { // hasToken()의 결과가 true라도 현재 사용자가 로그인 상태임을 보장하지 + UserApiClient.instance.accessTokenInfo { tokenInfo, error -> + if (error != null) { + if (error is KakaoSdkError && error.isInvalidTokenError()) { + // 로그인 필요 + UserApiClient.instance.loginWithKakaoAccount(this, callback = callback) + } else { + // 기타 에러 + Log.e("checkAuthenticatedToken - 1", "카카오 로그인 실패", error) + } + } else if (tokenInfo != null) { + Log.i( + "checkAuthenticatedToken - 2", "토큰 정보 보기 성공" + + "\n회원번호: ${tokenInfo.id}" + + "\n만료시간: ${tokenInfo.expiresIn} 초" + ) + } else { + // 토큰 유효성 체크 성공(필요 시 토큰 갱신됨) + } + } + } else { + //로그인 필요 + UserApiClient.instance.loginWithKakaoAccount(this, callback = callback) + } + } + + private fun autoSignInWithKakao() { + val callback: (OAuthToken?, Throwable?) -> Unit = { tokenInfo, error -> + if (error != null) { + Log.e("signInKakao - 1", "카카오 계정으로 로그인 실패", error) + when { + error.toString() == AuthErrorCause.AccessDenied.toString() -> + Toast.makeText(this, getString(R.string.access_denied), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidClient.toString() -> + Toast.makeText(this, getString(R.string.invalid_client), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidGrant.toString() -> + Toast.makeText(this, getString(R.string.invalid_grant), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidRequest.toString() -> + Toast.makeText(this, getString(R.string.invalid_request), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidScope.toString() -> + Toast.makeText(this, getString(R.string.invalid_scope), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.Misconfigured.toString() -> + Toast.makeText(this, getString(R.string.mis_configured), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.ServerError.toString() -> + Toast.makeText(this, getString(R.string.server_error), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.Unauthorized.toString() -> + Toast.makeText(this, getString(R.string.unauthorized), Toast.LENGTH_SHORT).show() + else -> + Toast.makeText(this, getString(R.string.etc_error), Toast.LENGTH_SHORT).show() + } + } + else if (tokenInfo != null) { + onSignInSuccess() + Log.i("autoSignInWithKakao", "카카오 계정으로 자동 로그인 성공 ${tokenInfo.accessToken}") + } + } + + // 사용자 정보 요청 (기본) + UserApiClient.instance.me { user, error -> + // 에러가 존재할 경우(로그인 정보 없음) -> 로그인 시도 + if (error != null) { + Log.e("autoSignInWithKakao", "autoSignInKakao 사용자 정보 요청 실패", error) + UserApiClient.instance.run { + if(isKakaoTalkLoginAvailable(this@LoginActivity)) + loginWithKakaoTalk(this@LoginActivity, callback = callback) + else + loginWithKakaoAccount(this@LoginActivity, callback = callback) + } + } + else if (user != null) { // 유저 정보가 존재할 경우 -> kakaoId로 개인 API 서버에서 유저 정보 획득 + Log.i("autoSignInWithKakao", "autoSignInKakao 사용자 정보 요청 성공" + + "\n회원번호: ${user.id}" + + "\n이메일: ${user.kakaoAccount?.email}" + + "\n닉네임: ${user.kakaoAccount?.profile?.nickname}" + + "\n프로필사진: ${user.kakaoAccount?.profile?.thumbnailImageUrl}") + onSignInSuccess() + } + } + } + + + /* + * 구글 소셜 로그인 + */ + + private fun signInWithGoogle() { + val signIntent = googleSignInClient.signInIntent + googleSignInResultLauncher.launch(signIntent) + } + + private val googleSignInResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + Log.e(javaClass.simpleName, "result.resultCode : ${result.resultCode}!!!") + Log.e(javaClass.simpleName, "RESULT_OK : $RESULT_OK") + + if (result.resultCode == RESULT_OK) { + val task = GoogleSignIn.getSignedInAccountFromIntent(result.data) + Log.e(javaClass.simpleName, "result.data : ${result.data}") + Log.e(javaClass.simpleName, "task : $task") + handleGoogleSignInResult(task) + } else { + Log.e(javaClass.simpleName, "googleSignInResultLauncher: error") + } + } + + private fun handleGoogleSignInResult(task: Task) { + Log.e(javaClass.simpleName, "handleGoogleSignInResult : 들어옴!!!") + Log.e(javaClass.simpleName, "task : $task!!!") + + if (task.isSuccessful) { + val account = task.result + val email = account?.email + val familyName = account?.familyName + if (account != null && email != null && familyName != null) { + firebaseUserAuthWithGoogle(account) + } + } + } + + private fun firebaseUserAuthWithGoogle(account: GoogleSignInAccount) { + Log.e(javaClass.simpleName, "firebaseUserAuthWithGoogle : 들어옴!!!") + val credential = GoogleAuthProvider.getCredential(account.idToken, null) + auth.signInWithCredential(credential).addOnCompleteListener { + if (it.isSuccessful) { + onSignInSuccess() + Log.d(javaClass.simpleName, "firebaseUserAuthWithGoogle : 성공!!!") + } else { + Toast.makeText(this, it.exception.toString(), Toast.LENGTH_SHORT).show() + } + } + } + + private fun autoSignInWithGoogle() { + user?.getIdToken(true) + ?.addOnCompleteListener { task -> + if (task.isSuccessful) { + val idToken = task.result.token + Log.d("autoSignInWithGoogle", "idToken: $idToken") + onSignInSuccess() + } else { + task.exception?.printStackTrace() + } + } + } + + private fun onSignInSuccess() { + val intent = MainActivity.getIntent(this) + startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/dining/coach/ui/main/MainActivity.kt b/app/src/main/java/com/dining/coach/ui/main/MainActivity.kt index a8fd8ca..1584ea5 100644 --- a/app/src/main/java/com/dining/coach/ui/main/MainActivity.kt +++ b/app/src/main/java/com/dining/coach/ui/main/MainActivity.kt @@ -1,5 +1,7 @@ package com.dining.coach.ui.main +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import com.dining.coach.R @@ -17,4 +19,9 @@ class MainActivity : BaseActivity(R.layout.activity_main) { override fun createActivity(): BaseViewModel { return viewModel } + + companion object { + fun getIntent(context: Context) = + Intent(context, MainActivity::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/dining/coach/ui/splash/SplashActivity.kt b/app/src/main/java/com/dining/coach/ui/splash/SplashActivity.kt index 3fd641a..db75ddc 100644 --- a/app/src/main/java/com/dining/coach/ui/splash/SplashActivity.kt +++ b/app/src/main/java/com/dining/coach/ui/splash/SplashActivity.kt @@ -1,6 +1,9 @@ package com.dining.coach.ui.splash import android.annotation.SuppressLint +import android.content.Intent +import android.util.Log +import android.widget.Toast import androidx.activity.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver @@ -12,6 +15,15 @@ import com.dining.coach.base.BaseViewModel import com.dining.coach.databinding.ActivitySplashBinding import com.dining.coach.ui.login.LoginActivity import com.dining.coach.ui.main.MainActivity +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.firebase.auth.FirebaseAuth +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.common.KakaoSdk +import com.kakao.sdk.common.model.AuthErrorCause +import com.kakao.sdk.common.util.Utility +import com.kakao.sdk.user.UserApiClient import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -23,13 +35,25 @@ import kotlinx.coroutines.launch @SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity: BaseActivity(R.layout.activity_splash) { + private lateinit var googleSignInClient: GoogleSignInClient + private val auth = FirebaseAuth.getInstance() + private val user = auth.currentUser private val viewModel: SplashViewModel by viewModels() override fun createActivity(): BaseViewModel { + initKakaoSignInSetting() + initGoogleSignInSetting() return viewModel } + override fun onStart() { + super.onStart() + +// autoSignInWithKakao() // 카카오 자동 로그인 +// autoSignInWithGoogle() // 구글 자동 로그인 + } + override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) @@ -51,4 +75,95 @@ class SplashActivity: BaseActivity(R.layout.activity_spla } } } + + private fun initKakaoSignInSetting() { + val keyHash = Utility.getKeyHash(this) + Log.e("Key", "keyHash: $keyHash") + /** Kakao SDK init */ + KakaoSdk.init(this, this.getString(R.string.kakao_app_key)) + } + + private fun initGoogleSignInSetting() { + // 앱에 필요한 사용자 데이터를 요청하도록 로그인 옵션을 설정. + // 유저 아이디와 기본 프로필 정보 요청 + val signIn = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(getString(R.string.default_web_client_id)) + .requestEmail() + .build() + + googleSignInClient = GoogleSignIn.getClient(this, signIn) + } + + + private fun autoSignInWithKakao() { + val callback: (OAuthToken?, Throwable?) -> Unit = { tokenInfo, error -> + if (error != null) { + Log.e("signInKakao - 1", "카카오 계정으로 로그인 실패", error) + when { + error.toString() == AuthErrorCause.AccessDenied.toString() -> + Toast.makeText(this, getString(R.string.access_denied), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidClient.toString() -> + Toast.makeText(this, getString(R.string.invalid_client), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidGrant.toString() -> + Toast.makeText(this, getString(R.string.invalid_grant), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidRequest.toString() -> + Toast.makeText(this, getString(R.string.invalid_request), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.InvalidScope.toString() -> + Toast.makeText(this, getString(R.string.invalid_scope), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.Misconfigured.toString() -> + Toast.makeText(this, getString(R.string.mis_configured), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.ServerError.toString() -> + Toast.makeText(this, getString(R.string.server_error), Toast.LENGTH_SHORT).show() + error.toString() == AuthErrorCause.Unauthorized.toString() -> + Toast.makeText(this, getString(R.string.unauthorized), Toast.LENGTH_SHORT).show() + else -> + Toast.makeText(this, getString(R.string.etc_error), Toast.LENGTH_SHORT).show() + } + } + else if (tokenInfo != null) { + onSignInSuccess() + Log.i("autoSignInWithKakao", "카카오 계정으로 자동 로그인 성공 ${tokenInfo.accessToken}") + } + } + + // 사용자 정보 요청 (기본) + UserApiClient.instance.me { user, error -> + // 에러가 존재할 경우(로그인 정보 없음) -> 로그인 시도 + if (error != null) { + Log.e("autoSignInWithKakao", "autoSignInKakao 사용자 정보 요청 실패", error) + UserApiClient.instance.run { + if(isKakaoTalkLoginAvailable(this@SplashActivity)) + loginWithKakaoTalk(this@SplashActivity, callback = callback) + else + loginWithKakaoAccount(this@SplashActivity, callback = callback) + } + } + else if (user != null) { // 유저 정보가 존재할 경우 -> kakaoId로 개인 API 서버에서 유저 정보 획득 + Log.i("autoSignInWithKakao", "autoSignInKakao 사용자 정보 요청 성공" + + "\n회원번호: ${user.id}" + + "\n이메일: ${user.kakaoAccount?.email}" + + "\n닉네임: ${user.kakaoAccount?.profile?.nickname}" + + "\n프로필사진: ${user.kakaoAccount?.profile?.thumbnailImageUrl}") + onSignInSuccess() + } + } + } + + private fun autoSignInWithGoogle() { + user?.getIdToken(true) + ?.addOnCompleteListener { task -> + if (task.isSuccessful) { + val idToken = task.result.token + Log.d("autoSignInWithGoogle", "idToken: $idToken") + onSignInSuccess() + } else { + task.exception?.printStackTrace() + } + } + } + + private fun onSignInSuccess() { + val intent = MainActivity.getIntent(this) + startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)) + } } \ 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 index a61a737..f678d4c 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -192,17 +192,18 @@ android:id="@+id/btn_kakao" android:layout_width="@dimen/login_button_round_size" android:layout_height="@dimen/login_button_round_size" - android:layout_marginEnd="16dp" + android:layout_marginEnd="8dp" android:src="@drawable/ic_kakao_round"/> - + + + + 이메일을 입력해 주세요 로그인에 문제가 있으신가요? + 토큰 정보 보기 실패 + 토큰 정보 보기 성공 + + 접근이 거부됨(동의 취소) + 유효하지 않은 앱 + + 인증 수단이 유효하지 않아 인증할 수 없는 상태 + 요청 파라미터 오류 + 유효하지 않은 scope ID + 설정이 올바르지 않음(android key hash error)" + 서버 내부 에러 + 앱이 요청 권한이 없음 + 기타 에러 + + 로그인에 성공하였습니다. + + 877625a3bd221ae6e830c9a1efcc0f06 + kakao877625a3bd221ae6e830c9a1efcc0f06 Dining Coach Alarm 설명 문구 diff --git a/build.gradle b/build.gradle index 41d9ba4..9fc864e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,10 +3,13 @@ buildscript { repositories { google() mavenCentral() + maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/'} } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3" + classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' } } plugins { @@ -14,4 +17,5 @@ plugins { id 'com.android.library' version '7.3.1' apply false id 'org.jetbrains.kotlin.android' version '1.8.20' apply false id 'com.google.dagger.hilt.android' version '2.45' apply false + id 'com.google.gms.google-services' version '4.3.8' apply false } \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 215bb73..7ea6683 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/'} } } rootProject.name = "DiningCoach-Android"