-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature] 로그인 UI 구성 및 카카오 소셜 로그인 구현 #22
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
Changes from all commits
2c03a14
bd6ac87
6114ae3
22e4d3a
0d70567
b92b913
e410519
8836162
78237ca
fb89c57
0c08868
737b9c3
9f03d40
0f844a8
877388d
131cb47
0800e55
cc27edc
c3edc18
75855d3
7bcd976
2d3062d
9f3475a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,16 @@ | ||
| package com.alarmy.near | ||
|
|
||
| import android.app.Application | ||
| import com.kakao.sdk.common.KakaoSdk | ||
| import dagger.hilt.android.HiltAndroidApp | ||
|
|
||
| @HiltAndroidApp | ||
| class NearApplication : Application() | ||
| class NearApplication : Application() { | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
|
|
||
| // 카카오 SDK 초기화 | ||
| KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package com.alarmy.near.data.datasource | ||
|
|
||
| import android.content.Context | ||
| import com.alarmy.near.model.ProviderType | ||
| 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 dagger.hilt.android.qualifiers.ApplicationContext | ||
| import kotlinx.coroutines.CancellableContinuation | ||
| import kotlinx.coroutines.suspendCancellableCoroutine | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
| import kotlin.coroutines.resume | ||
|
|
||
| /** | ||
| * 카카오 로그인 데이터 소스 | ||
| * 단일 책임: 카카오 SDK만 처리 | ||
| */ | ||
| @Singleton | ||
| class KakaoDataSource | ||
| @Inject | ||
| constructor( | ||
| @ApplicationContext private val context: Context, | ||
| ) : SocialLoginDataSource { | ||
| override val supportedType: ProviderType = ProviderType.KAKAO | ||
|
|
||
| override suspend fun login(): Result<String> = | ||
| try { | ||
| val token = | ||
| if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { | ||
| loginWithKakaoTalk() | ||
| } else { | ||
| loginWithKakaoAccount() | ||
| } | ||
|
|
||
| if (token.isNotEmpty()) { | ||
| Result.success(token) | ||
| } else { | ||
| Result.failure(Exception("사용자가 로그인을 취소했습니다")) | ||
| } | ||
| } catch (exception: Exception) { | ||
| Result.failure(exception) | ||
| } | ||
|
|
||
| private suspend fun loginWithKakaoTalk(): String = | ||
| suspendCancellableCoroutine { continuation -> | ||
| UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> | ||
| when { | ||
| error != null -> { | ||
| if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { | ||
| continuation.resume("") | ||
stopstone marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| UserApiClient.instance.loginWithKakaoAccount(context) { retryToken, retryError -> | ||
| handleLoginResult(retryToken, retryError, continuation) | ||
| } | ||
| } | ||
| } | ||
| token != null -> continuation.resume(token.accessToken) | ||
| else -> continuation.resume("") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private suspend fun loginWithKakaoAccount(): String = | ||
| suspendCancellableCoroutine { continuation -> | ||
| UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> | ||
| handleLoginResult(token, error, continuation) | ||
| } | ||
| } | ||
|
|
||
| private fun handleLoginResult( | ||
| token: OAuthToken?, | ||
| error: Throwable?, | ||
| continuation: CancellableContinuation<String>, | ||
| ) { | ||
| when { | ||
| error != null -> { | ||
| if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { | ||
| continuation.resume("") | ||
| } else { | ||
| continuation.resumeWith(Result.failure(error)) | ||
| } | ||
| } | ||
| token != null -> continuation.resume(token.accessToken) | ||
| else -> continuation.resume("") | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.alarmy.near.data.datasource | ||
|
|
||
| import com.alarmy.near.model.ProviderType | ||
|
|
||
| /** | ||
| * 소셜 로그인 데이터 소스 인터페이스 | ||
| * Strategy 패턴으로 각 소셜 플랫폼별로 구현 | ||
| */ | ||
| interface SocialLoginDataSource { | ||
| /** | ||
| * 지원하는 소셜 로그인 타입 | ||
| */ | ||
| val supportedType: ProviderType | ||
|
|
||
| /** | ||
| * 소셜 로그인 수행 | ||
| * Context는 생성자에서 주입받아 사용 | ||
| */ | ||
| suspend fun login(): Result<String> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.alarmy.near.data.datasource | ||
|
|
||
|
|
||
| import com.alarmy.near.model.ProviderType | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| /** | ||
| * 소셜 로그인 프로세서 | ||
| * Strategy 패턴으로 동적으로 로그인 방식 선택 | ||
| */ | ||
| @Singleton | ||
| class SocialLoginProcessor | ||
| @Inject | ||
| constructor( | ||
| private val socialLoginDataSources: Set<@JvmSuppressWildcards SocialLoginDataSource>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Set과 JvmSuppressWildcards를 적용주신 이유가 궁금합니다!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Set을 적용한 이유 2. JvmSuppressWildcards를 적용한 이유 // JVM 바이트코드에서는 이렇게 변환돼요 여기서 ?는 wildcard로, "정확한 타입을 모르지만 SocialLoginDataSource의 하위 타입들"이라는 의미입니다. 바인딩할 때 "어? 이게 정확히 어떤 타입인지 모르겠네?" 하면서 에러가 발생할 가능성이 있어,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이해가 쏙쏙 되네요! 상세한 답변 감사합니다! |
||
| ) { | ||
| /** | ||
| * 소셜 로그인 처리 | ||
| * @param providerType 로그인 제공자 타입 | ||
| * @return 로그인 결과 | ||
| */ | ||
| suspend fun processLogin( | ||
| providerType: ProviderType, | ||
| ): Result<String> { | ||
| val dataSource = | ||
| socialLoginDataSources.find { it.supportedType == providerType } | ||
| ?: return Result.failure(Exception("지원하지 않는 로그인 타입입니다: ${providerType.name}")) | ||
|
|
||
| return dataSource.login() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.alarmy.near.data.di | ||
|
|
||
| import com.alarmy.near.data.datasource.KakaoDataSource | ||
| import com.alarmy.near.data.datasource.SocialLoginDataSource | ||
| import dagger.Binds | ||
| import dagger.Module | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.components.SingletonComponent | ||
| import dagger.multibindings.IntoSet | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| interface DataSourceModule { | ||
| @Binds | ||
| @IntoSet | ||
| abstract fun bindKakaoDataSource(kakaoDataSource: KakaoDataSource): SocialLoginDataSource | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.alarmy.near.data.di | ||
|
|
||
| import android.content.Context | ||
| import androidx.datastore.core.DataStore | ||
| import androidx.datastore.preferences.core.Preferences | ||
| import androidx.datastore.preferences.preferencesDataStore | ||
| import dagger.Module | ||
| import dagger.Provides | ||
| import dagger.hilt.InstallIn | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import dagger.hilt.components.SingletonComponent | ||
| import javax.inject.Singleton | ||
|
|
||
| // DataStore 확장 프로퍼티 | ||
| private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "auth_preferences") | ||
|
|
||
| @Module | ||
| @InstallIn(SingletonComponent::class) | ||
| object DataStoreModule { | ||
| @Provides | ||
| @Singleton | ||
| fun provideDataStore( | ||
| @ApplicationContext context: Context, | ||
| ): DataStore<Preferences> = context.dataStore | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.