Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature/#17] : 카카오 로그인, 구글 로그인 구현 #23

Merged
merged 60 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
6cc59e2
#17 [ADD] Kakao API Key 숨기기
youjin09222 Jan 10, 2025
0a85b39
#17 [ADD] Kakao Redirect URI 설정
youjin09222 Jan 10, 2025
75d90c3
#17 [FEAT] Kakao SDK 초기화
youjin09222 Jan 10, 2025
947d5c1
#17 [ADD] LoginSideEffect에 ShowToast 추가
youjin09222 Jan 10, 2025
d9aeacb
#17 [REFACTOR] authId 초기값 수정
youjin09222 Jan 10, 2025
1877ed8
#17 [ADD] 카카오 로그인 string 추가
youjin09222 Jan 10, 2025
4d5e1b4
#17 [FEAT] 카카오 로그인 구현
youjin09222 Jan 10, 2025
02e14d5
#17 [FEAT] 카카오 로그인 연동
youjin09222 Jan 10, 2025
7a94617
#17 [FIX] 앱 설치 오류 해결
youjin09222 Jan 10, 2025
c163649
#17 [REFACTOR] Kakao 로그인 로직 개선
youjin09222 Jan 11, 2025
d6fcf78
#17 [ADD] Google 라이브러리 추가
youjin09222 Jan 11, 2025
63cb4b7
#17 [ADD] GoogleClientId 숨기기
youjin09222 Jan 11, 2025
a5e8114
#17 [ADD] GoogleWebClient ID 제공 메서드 추가
youjin09222 Jan 11, 2025
f2c6643
#17 [FEAT] Google 로그인 클라이언트 초기화
youjin09222 Jan 11, 2025
a0a3cae
#17 [ADD] Google 로그인 string 추가
youjin09222 Jan 11, 2025
266ea84
#17 [FEAT] 구글 로그인 구현
youjin09222 Jan 11, 2025
51cc6f7
#17 [REFACTOR] Toast 로직 함수화
youjin09222 Jan 11, 2025
f061f87
#17 [FEAT] Google 로그인 연동
youjin09222 Jan 12, 2025
2b3d783
#17 [REFACTOR] 로그인 결과 처리 함수화
youjin09222 Jan 12, 2025
97393e4
#17 [MOD] 패딩 추가
youjin09222 Jan 12, 2025
e2defe7
#17 [FIX] 카카오 로그인 오류 해결
youjin09222 Jan 13, 2025
8eda530
#17 [CHORE] import 문 정리
youjin09222 Jan 14, 2025
1e6c6f9
#17 [ADD] DataStore 라이브러리 추가
youjin09222 Jan 15, 2025
fabd2a0
#17 [ADD] UserEntity 추가
youjin09222 Jan 15, 2025
5b68ff8
#17 [FEAT] PreferencesDataStore 구현
youjin09222 Jan 15, 2025
c1a41b2
#17 [FEAT] UserDataSource 구현
youjin09222 Jan 15, 2025
3cf60fd
#17 [FEAT] UserDataSourceImpl 구현
youjin09222 Jan 15, 2025
3fc19ba
#17 [FEAT] UserInfoRepository 구현
youjin09222 Jan 15, 2025
9061573
#17 [FEAT] UserInfoRepositoryImpl 구현
youjin09222 Jan 15, 2025
dfeb545
#17 [FEAT] UserInfoModule 구현
youjin09222 Jan 15, 2025
f98694d
#17 [RENAME] AuthModule to GoogleIdModule
youjin09222 Jan 15, 2025
fbc7d96
#17 [DEL] 필요없는 변수 제거
youjin09222 Jan 15, 2025
291d673
#17 [REFACTOR] 로그인 로직 개선
youjin09222 Jan 15, 2025
cc0e210
#17 [REFACTOR] 서버 통신을 위한 postLogin 함수 분리
youjin09222 Jan 15, 2025
58eb933
#17 [ADD] SocialType enum class 추가
youjin09222 Jan 15, 2025
116ba83
#17 [REFACTOR] SocialType 적용
youjin09222 Jan 15, 2025
7703990
#17 [FEAT] 사용자 정보 저장 기능 추가
youjin09222 Jan 15, 2025
37978ae
#17 [MOD] profileImage nullable 처리
youjin09222 Jan 15, 2025
2c39516
#17 [DEL] 불필요한 변수 제거
youjin09222 Jan 15, 2025
0beec3c
#17 [FEAT] 신규 유저 체크 및 자동 로그인 구현
youjin09222 Jan 15, 2025
dca3aad
#17 [FEAT] 닉네임, 프로필 저장 구현
youjin09222 Jan 15, 2025
add7a4a
#17 [REFACTOR] SignUpViewModel 비동기 처리 로직 개선
youjin09222 Jan 15, 2025
ce0045f
#17 [REFACTOR] 카카오 로그인 콜백 로직 분리 및 중복 제거
youjin09222 Jan 15, 2025
2048ee1
#17 [REFACTOR] Google 로그인 결과 처리 비동기화
youjin09222 Jan 15, 2025
2d9b6a2
#17 [CHORE] checkIsNewUser 위치 변경
youjin09222 Jan 15, 2025
6de643c
#17 [REFACTOR] 불필요한 함수 제거
youjin09222 Jan 15, 2025
5efe337
#17 [REFACTOR] BEARER 상수 내부 관리
youjin09222 Jan 15, 2025
b0697ae
#17 [REFACTOR] Kakao 로그인 콜백 로직 개선 및 중복 제거
youjin09222 Jan 15, 2025
653148e
#17 [REFACTOR] Google 로그인 요청 생성 로직 분리
youjin09222 Jan 15, 2025
8392c52
#17 [REFACTOR] CredentialManager 사용
youjin09222 Jan 16, 2025
7def1e9
#17 [DEL] 불필요한 코드 삭제
youjin09222 Jan 16, 2025
e13ed44
#17 [REFACTOR] Google 로그인 로직 분리
youjin09222 Jan 16, 2025
4ef97c2
#17 [REFACTOR] 로그인 성공/실패 로직 통합
youjin09222 Jan 16, 2025
9b6a716
#17 [UI] 로그인 실패 다이얼로그
youjin09222 Jan 16, 2025
6b01f92
#17 [FEAT] 로그인 실패 다이얼로그 구현
youjin09222 Jan 16, 2025
193ed71
#17 [FEAT] 로그인 실패 다이얼로그 연결
youjin09222 Jan 16, 2025
a57b103
#17 [REFACTOR] 로그인 에러 처리 로직 개선
youjin09222 Jan 16, 2025
ebf1064
#17 [MOD] ktlint 적용
youjin09222 Jan 16, 2025
a2c272a
#17 [ADD] 로그인 string 추가
youjin09222 Jan 16, 2025
a079317
Merge branch 'develop' of https://github.com/Noostak/Noostak_Android …
youjin09222 Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import java.util.Properties

plugins {
Expand Down Expand Up @@ -30,6 +29,9 @@ android {
useSupportLibrary = true
}
buildConfigField("String", "BASE_URL", properties["base.url"].toString())
buildConfigField("String", "KAKAO_API_KEY", properties["KAKAO_API_KEY"].toString())
buildConfigField("String", "GOOGLE_CLIENT_ID", properties["GOOGLE_CLIENT_ID"].toString())
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = properties["KAKAO_NATIVE_APP_KEY"].toString()
}

buildTypes {
Expand Down Expand Up @@ -130,4 +132,8 @@ dependencies {
// Kakao
implementation(libs.kakao.all)
implementation(libs.kakao.user)

// Google
implementation(libs.play.services.auth)
implementation(libs.google.id)
}
22 changes: 19 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<application
android:name="com.sopt.noostak.MyApp"
Expand All @@ -29,6 +30,21 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="oauth"
android:scheme="${KAKAO_NATIVE_APP_KEY}" />
</intent-filter>
</activity>
</application>

</manifest>
6 changes: 6 additions & 0 deletions app/src/main/java/com/sopt/noostak/MyApp.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sopt.noostak

import android.app.Application
import com.kakao.sdk.common.KakaoSdk
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

Expand All @@ -9,9 +10,14 @@ class MyApp : Application() {
override fun onCreate() {
super.onCreate()
setTimber()
initKakao()
}

private fun setTimber() {
Timber.plant(Timber.DebugTree())
}

private fun initKakao() {
KakaoSdk.init(this, BuildConfig.KAKAO_API_KEY)
}
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/sopt/noostak/di/AuthModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sopt.noostak.di

import com.sopt.noostak.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Named
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AuthModule {
@Provides
@Singleton
@Named("GoogleClientId")
fun provideGoogleClientId(): String {
return BuildConfig.GOOGLE_CLIENT_ID
}
}
8 changes: 8 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ landscapist = "2.3.6"
# Kakao
kakao = "2.20.1"

# Google
google-id = "1.1.1"
play-services-auth = "21.1.0"

viewpager-indicator = "5.0"

# Hilt
Expand Down Expand Up @@ -155,6 +159,10 @@ landscapist-animation = { group = "com.github.skydoves", name = "landscapist-ani
kakao-all = { group = "com.kakao.sdk", name = "v2-all", version.ref = "kakao" } # Kakao SDK 라이브러리
kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao" } # Kakao 로그인 라이브러리

# Google
play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" }
google-id = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "google-id" }

# Hilt
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } # Hilt Android 라이브러리
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" } # Hilt Core 라이브러리
Expand Down
4 changes: 4 additions & 0 deletions presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,8 @@ dependencies {
// Kakao
implementation(libs.kakao.all)
implementation(libs.kakao.user)

// Google
implementation(libs.play.services.auth)
implementation(libs.google.id)
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package com.sopt.presentation.auth.login

import android.app.Activity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.android.gms.auth.api.identity.Identity
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
import com.sopt.core.designsystem.theme.NoostakTheme
import com.sopt.core.extension.toast
import com.sopt.presentation.R
import com.sopt.presentation.auth.component.LoginButton

Expand All @@ -30,18 +38,37 @@ fun LoginRoute(
navigateToSignUp: (String) -> Unit,
loginViewModel: LoginViewModel = hiltViewModel()
) {
val context = LocalContext.current

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data != null) {
val credential = Identity.getSignInClient(context)
.getSignInCredentialFromIntent(result.data)
loginViewModel.handleGoogleLoginResult(credential)
} else {
context.toast(R.string.toast_login_cancelled)
}
}

LaunchedEffect(Unit) {
loginViewModel.initializeGoogleSignIn(context)
}

LaunchedEffect(loginViewModel.sideEffects) {
loginViewModel.sideEffects.collect { sideEffect ->
when (sideEffect) {
is LoginSideEffect.NavigateToHome -> navigateToHome()
is LoginSideEffect.NavigateSignUp -> navigateToSignUp(sideEffect.authId)
is LoginSideEffect.ShowToast -> context.toast(sideEffect.message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 : 실패했을 때 뜨는 스낵바나 다이얼로그 GUI 필요할듯요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 추가하겠습니다!

}
}
}

LoginScreen(
onKakaoLoginClick = loginViewModel::kakaoLogin,
onGoogleLoginClick = loginViewModel::googleLogin
onKakaoLoginClick = { loginViewModel.kakaoLogin(context) },
onGoogleLoginClick = { loginViewModel.googleLogin(launcher) }
)
}

Expand All @@ -55,7 +82,9 @@ fun LoginScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(dimensionResource(R.dimen.horizontal_padding)),
.padding(dimensionResource(R.dimen.horizontal_padding))
.statusBarsPadding()
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package com.sopt.presentation.auth.login

import androidx.annotation.StringRes

sealed class LoginSideEffect {
data object NavigateToHome : LoginSideEffect()
data class NavigateSignUp(val authId: String) : LoginSideEffect()
data class ShowToast(
@StringRes val message: Int,
val args: String? = null
) : LoginSideEffect()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 : 로컬에 token 저장하는 로직 필요할 것 같아요 member id랑.. refresh token은 안받나요?

Copy link
Contributor Author

@youjin09222 youjin09222 Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서버 연결 로직 작성 시 발급받는 accessToken과 refreshToken를 저장하려고 하는데
이것과 별개로 카카오 및 구글 토큰도 저장해야하나요??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아아 생각해보니 아직 서버 연결 전이네용 ㅋㅋㅋ 서버 연결 기준으로 말한거였어요! 흠 그래도 로컬에 토큰 제외하고 유저 정보 저장하는건 구현해놓으면 좋을 것 같아용 member id, nickname, ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 해당 부분 수정하겠습니다!!

Original file line number Diff line number Diff line change
@@ -1,26 +1,94 @@
package com.sopt.presentation.auth.login

import android.content.Context
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.annotation.StringRes
import com.google.android.gms.auth.api.identity.Identity
import com.google.android.gms.auth.api.identity.SignInClient
import com.google.android.gms.auth.api.identity.SignInCredential
import com.kakao.sdk.auth.model.OAuthToken
import com.kakao.sdk.common.model.ClientError
import com.kakao.sdk.common.model.ClientErrorCause
import com.kakao.sdk.user.UserApiClient
import com.sopt.core.util.BaseViewModel
import com.sopt.presentation.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Named

@HiltViewModel
class LoginViewModel @Inject constructor() : BaseViewModel<LoginSideEffect>() {
class LoginViewModel @Inject constructor(
@Named("GoogleClientId") private val googleClientId: String
) : BaseViewModel<LoginSideEffect>() {
private val _authId = MutableStateFlow("")
val authId: StateFlow<String> = _authId
private lateinit var oneTapClient: SignInClient

private val _authId = MutableStateFlow<String?>(null)
val authId: StateFlow<String?> = _authId
fun initializeGoogleSignIn(context: Context) {
oneTapClient = Identity.getSignInClient(context)
}

// Kakao Login
fun kakaoLogin(context: Context) {
val loginCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
handleError(error, R.string.toast_kakao_login_failed)
} else if (token != null) {
handleSuccess(token.accessToken, R.string.toast_kakao_login_success)
}
}

if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context, callback = loginCallback)
} else {
UserApiClient.instance.loginWithKakaoAccount(context, callback = loginCallback)
}
}

// Google Login
fun googleLogin(launcher: ActivityResultLauncher<IntentSenderRequest>) {
val signInRequest = com.google.android.gms.auth.api.identity.BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
com.google.android.gms.auth.api.identity.BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: 요거 임포트 해주세요!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Credential Manager로 리팩해보면 좋을 것 같아요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네ㅔㅔㅔ 리팩해놓겠슴다ㅏㅏ

.setSupported(true)
.setServerClientId(googleClientId)
.setFilterByAuthorizedAccounts(false)
.build()
)
.build()

fun kakaoLogin() {
// TODO: 카카오 로그인
navigateToHome()
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener { result ->
launcher.launch(IntentSenderRequest.Builder(result.pendingIntent).build())
}
.addOnFailureListener { exception ->
handleError(exception, R.string.toast_google_login_failed)
}
}

fun googleLogin() {
// TODO: 구글 로그인
_authId.value = "google_access_token"
navigateToSignup(_authId.value.orEmpty())
fun handleGoogleLoginResult(credential: SignInCredential) {
if (!credential.googleIdToken.isNullOrEmpty()) {
handleSuccess(credential.googleIdToken.toString(), R.string.toast_google_login_success)
} else {
showToast(R.string.toast_google_login_failed)
}
}

private fun handleSuccess(authId: String, successMessageResId: Int) {
_authId.value = authId
showToast(successMessageResId)
navigateToSignup(authId)
}

private fun handleError(error: Throwable, @StringRes errorMessageResId: Int) {
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
showToast(R.string.toast_login_cancelled)
} else {
showToast(errorMessageResId, error.localizedMessage.orEmpty())
}
}

private fun navigateToSignup(authId: String) {
Expand All @@ -30,4 +98,13 @@ class LoginViewModel @Inject constructor() : BaseViewModel<LoginSideEffect>() {
private fun navigateToHome() {
emitSideEffect(LoginSideEffect.NavigateToHome)
}

private fun showToast(@StringRes messageResId: Int, vararg formatArgs: String) {
emitSideEffect(
LoginSideEffect.ShowToast(
message = messageResId,
args = formatArgs.joinToString(separator = ", ")
)
)
}
}
5 changes: 5 additions & 0 deletions presentation/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
<string name="hint_signup_name">이름을 입력해주세요</string>
<string name="tv_signup_count">%1$d / %2$d</string>
<string name="toast_permission_gallery">갤러리 권한이 필요합니다.</string>
<string name="toast_kakao_login_success">카카오 로그인 성공</string>
<string name="toast_kakao_login_failed">카카오 로그인 실패: %s</string>
<string name="toast_login_cancelled">로그인이 취소되었습니다.</string>
<string name="toast_google_login_success">구글 로그인 성공</string>
<string name="toast_google_login_failed">구글 로그인 실패: %s</string>

<!-- 그룹 초대 입력 -->
<string name="iv_invite_description">그룹 초대 설명 이미지</string>
Expand Down
Loading