From 494a7db54d1c4300c0fa7781e97d3a9e88fd651c Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 17 Dec 2025 11:44:38 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[BOOK-474]=20feat:=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구글로 부터 idToken을 받아와 서버에 전달하는 로직 --- app/build.gradle.kts | 2 + .../data/api/repository/AuthRepository.kt | 5 +- .../impl/repository/DefaultAuthRepository.kt | 11 +-- .../component/button/ButtonColorStyle.kt | 7 +- .../booket/core/designsystem/theme/Color.kt | 1 + feature/login/build.gradle.kts | 23 ++++++ .../booket/feature/login/GoogleLoginClient.kt | 82 +++++++++++++++++++ .../feature/login/HandleLoginSideEffects.kt | 31 ++++++- .../booket/feature/login/LoginPresenter.kt | 7 +- .../ninecraft/booket/feature/login/LoginUi.kt | 22 +++++ .../booket/feature/login/LoginUiState.kt | 12 ++- .../login/src/main/res/drawable/ic_google.xml | 26 ++++++ feature/login/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 8 ++ 14 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt create mode 100644 feature/login/src/main/res/drawable/ic_google.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bebec272..00580221 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ android { getByName("debug") { isDebuggable = true applicationIdSuffix = ".dev" + buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("DEBUG_GOOGLE_WEB_CLIENT_ID")) manifestPlaceholders += mapOf( "appName" to "@string/app_name_dev", ) @@ -42,6 +43,7 @@ android { isMinifyEnabled = true isShrinkResources = true signingConfig = signingConfigs.getByName("release") + buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("RELEASE_GOOGLE_WEB_CLIENT_ID")) manifestPlaceholders += mapOf( "appName" to "@string/app_name", ) diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt index c8943668..a79b2f21 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt @@ -5,7 +5,10 @@ import com.ninecraft.booket.core.model.UserState import kotlinx.coroutines.flow.Flow interface AuthRepository { - suspend fun login(accessToken: String): Result + suspend fun login( + providerType: String, + token: String, + ): Result suspend fun logout(): Result diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt index 4172d9e0..73e53d8b 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt @@ -12,19 +12,20 @@ import com.ninecraft.booket.core.di.DataScope import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.map -private const val KAKAO_PROVIDER_TYPE = "KAKAO" - @SingleIn(DataScope::class) @Inject class DefaultAuthRepository( private val service: ReedService, private val tokenDataSource: TokenDataSource, ) : AuthRepository { - override suspend fun login(accessToken: String) = runSuspendCatching { + override suspend fun login( + providerType: String, + token: String, + ) = runSuspendCatching { val response = service.login( LoginRequest( - providerType = KAKAO_PROVIDER_TYPE, - oauthToken = accessToken, + providerType = providerType, + oauthToken = token, ), ) saveTokens(response.accessToken, response.refreshToken) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt index b4ddfb90..464f21df 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt @@ -4,20 +4,22 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.theme.Google import com.ninecraft.booket.core.designsystem.theme.Kakao import com.ninecraft.booket.core.designsystem.theme.ReedTheme enum class ReedButtonColorStyle { - PRIMARY, SECONDARY, TERTIARY, STROKE, TEXT, KAKAO; + PRIMARY, SECONDARY, TERTIARY, STROKE, TEXT, KAKAO, GOOGLE; @Composable fun containerColor(isPressed: Boolean) = when (this) { PRIMARY -> if (isPressed) ReedTheme.colors.bgPrimaryPressed else ReedTheme.colors.bgPrimary SECONDARY -> if (isPressed) ReedTheme.colors.bgSecondaryPressed else ReedTheme.colors.bgSecondary TERTIARY -> if (isPressed) ReedTheme.colors.bgTertiaryPressed else ReedTheme.colors.bgTertiary - STROKE -> if (isPressed) ReedTheme.colors.basePrimary else ReedTheme.colors.basePrimary + STROKE -> ReedTheme.colors.basePrimary TEXT -> Color.Transparent KAKAO -> Kakao + GOOGLE -> ReedTheme.colors.basePrimary } @Composable @@ -28,6 +30,7 @@ enum class ReedButtonColorStyle { STROKE -> ReedTheme.colors.contentBrand TEXT -> ReedTheme.colors.borderBrand KAKAO -> ReedTheme.colors.contentPrimary + GOOGLE -> ReedTheme.colors.contentPrimary } @Composable diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt index 61997cc9..0bc9ee0c 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt @@ -64,6 +64,7 @@ val Blue800 = Color(0xFF1269EC) val Blue900 = Color(0xFF1F47CD) val Kakao = Color(0xFFFBD300) +val Google = Color(0xFF4285F4) val Blank = Color(0xFFD6D6D6) val HomeBg = Color(0xFFF0F9E8) diff --git a/feature/login/build.gradle.kts b/feature/login/build.gradle.kts index fa25f74d..31ab2096 100644 --- a/feature/login/build.gradle.kts +++ b/feature/login/build.gradle.kts @@ -1,5 +1,7 @@ @file:Suppress("INLINE_FROM_HIGHER_PLATFORM") +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties + plugins { alias(libs.plugins.booket.android.feature) alias(libs.plugins.kotlin.serialization) @@ -8,11 +10,32 @@ plugins { android { namespace = "com.ninecraft.booket.feature.login" + + buildTypes { + getByName("debug") { + buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("DEBUG_GOOGLE_WEB_CLIENT_ID")) + } + + getByName("release") { + buildConfigField("String", "GOOGLE_WEB_CLIENT_ID", getApiKey("RELEASE_GOOGLE_WEB_CLIENT_ID")) + } + } + + buildFeatures { + buildConfig = true + } } dependencies { implementations( libs.logger, libs.kakao.auth, + libs.androidx.credentials, + libs.androidx.credentials.play.services.auth, + libs.googleid, ) } + +fun getApiKey(propertyKey: String): String { + return gradleLocalProperties(rootDir, providers).getProperty(propertyKey) +} diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt new file mode 100644 index 00000000..a18c6c95 --- /dev/null +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt @@ -0,0 +1,82 @@ +package com.ninecraft.booket.feature.login + +import android.content.Context +import androidx.credentials.CredentialManager +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialRequest +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.NoCredentialException +import com.google.android.libraries.identity.googleid.GetGoogleIdOption +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential +import com.ninecraft.booket.core.designsystem.R as designR +import com.orhanobut.logger.Logger +import dev.zacsweers.metro.Inject + +@Inject +internal class GoogleLoginClient { + suspend fun loginWithGoogle( + context: Context, + webClientId: String, + onSuccess: (String) -> Unit, + onFailure: (String) -> Unit, + ) { + val credentialManager = CredentialManager.create(context) + + val googleIdOption = GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(false) + .setServerClientId(webClientId) + .setAutoSelectEnabled(true) + .build() + + val credentialRequest = GetCredentialRequest.Builder() + .addCredentialOption(googleIdOption) + .build() + + try { + val result = credentialManager.getCredential( + request = credentialRequest, + context = context, + ) + handleSignIn(result, onSuccess, onFailure, context) + } catch (e: GetCredentialCancellationException) { + Logger.e("Google 로그인 취소됨") + onFailure(context.getString(designR.string.unknown_error_message)) + } catch (e: NoCredentialException) { + Logger.e("Google 계정을 찾을 수 없음") + onFailure(context.getString(designR.string.unknown_error_message)) + } catch (e: GetCredentialException) { + Logger.e("Google 로그인 실패: ${e.message}") + onFailure(context.getString(designR.string.network_error_message)) + } catch (e: Exception) { + Logger.e("알 수 없는 오류: ${e.message}") + onFailure(context.getString(designR.string.unknown_error_message)) + } + } + + private fun handleSignIn( + result: GetCredentialResponse, + onSuccess: (String) -> Unit, + onFailure: (String) -> Unit, + context: Context, + ) { + val credential = result.credential + + if (credential is CustomCredential && + credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + try { + val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + val idToken = googleIdTokenCredential.idToken + Logger.d("Google 로그인 성공: ${googleIdTokenCredential.id}") + onSuccess(idToken) + } catch (e: Exception) { + Logger.e("Google ID Token 파싱 실패: ${e.message}") + onFailure(context.getString(designR.string.unknown_error_message)) + } + } else { + Logger.e("예상치 못한 credential type: ${credential.type}") + onFailure(context.getString(designR.string.unknown_error_message)) + } + } +} \ No newline at end of file diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt index 2e7b48f1..f30bef77 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt @@ -3,8 +3,10 @@ package com.ninecraft.booket.feature.login import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import com.skydoves.compose.effects.RememberedEffect +import kotlinx.coroutines.launch @Composable internal fun HandleLoginSideEffects( @@ -12,7 +14,9 @@ internal fun HandleLoginSideEffects( eventSink: (LoginUiEvent) -> Unit, ) { val context = LocalContext.current + val scope = rememberCoroutineScope() val kakaoLoginClient = remember { KakaoLoginClient() } + val googleLoginClient = remember { GoogleLoginClient() } RememberedEffect(state.sideEffect) { when (state.sideEffect) { @@ -20,7 +24,12 @@ internal fun HandleLoginSideEffects( kakaoLoginClient.loginWithKakao( context = context, onSuccess = { token -> - eventSink(LoginUiEvent.Login(token)) + eventSink( + LoginUiEvent.Login( + providerType = LoginUiEvent.PROVIDER_TYPE_KAKAO, + token = token, + ), + ) }, onFailure = { errorMessage -> eventSink(LoginUiEvent.LoginFailure(errorMessage)) @@ -28,6 +37,26 @@ internal fun HandleLoginSideEffects( ) } + is LoginSideEffect.GoogleLogin -> { + scope.launch { + googleLoginClient.loginWithGoogle( + context = context, + webClientId = BuildConfig.GOOGLE_WEB_CLIENT_ID, + onSuccess = { idToken -> + eventSink( + LoginUiEvent.Login( + providerType = LoginUiEvent.PROVIDER_TYPE_GOOGLE, + token = idToken, + ) + ) + }, + onFailure = { errorMessage -> + eventSink(LoginUiEvent.LoginFailure(errorMessage)) + }, + ) + } + } + is LoginSideEffect.ShowToast -> { Toast.makeText(context, state.sideEffect.message, Toast.LENGTH_SHORT).show() } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index 6a717c49..9a7a21a0 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -85,6 +85,11 @@ class LoginPresenter( sideEffect = LoginSideEffect.KakaoLogin() } + is LoginUiEvent.OnGoogleLoginButtonClick -> { + isLoading = true + sideEffect = LoginSideEffect.GoogleLogin() + } + is LoginUiEvent.LoginFailure -> { isLoading = false analyticsHelper.logEvent(EVENT_ERROR_LOGIN) @@ -95,7 +100,7 @@ class LoginPresenter( scope.launch { try { isLoading = true - authRepository.login(event.accessToken) + authRepository.login(event.providerType, event.token) .onSuccess { userRepository.syncFcmToken() navigateAfterLogin() diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index 2b0d8f83..cdbee347 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -113,6 +113,28 @@ internal fun LoginUi( ) }, ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnGoogleLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.GOOGLE, + modifier = Modifier + .fillMaxWidth() + .padding( + start = ReedTheme.spacing.spacing5, + end = ReedTheme.spacing.spacing5, + ), + text = stringResource(id = R.string.google_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_google), + contentDescription = "Google Icon", + tint = Color.Unspecified, + ) + } + ) Spacer( modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing2 else ReedTheme.spacing.spacing8), ) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt index 1f58e248..8c49aea8 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt @@ -16,6 +16,7 @@ data class LoginUiState( @Immutable sealed interface LoginSideEffect { data class KakaoLogin(private val key: String = UUID.randomUUID().toString()) : LoginSideEffect + data class GoogleLogin(private val key: String = UUID.randomUUID().toString()) : LoginSideEffect data class ShowToast( val message: String, private val key: String = UUID.randomUUID().toString(), @@ -24,8 +25,17 @@ sealed interface LoginSideEffect { sealed interface LoginUiEvent : CircuitUiEvent { data object OnKakaoLoginButtonClick : LoginUiEvent - data class Login(val accessToken: String) : LoginUiEvent + data object OnGoogleLoginButtonClick : LoginUiEvent + data class Login( + val providerType: String, + val token: String, + ) : LoginUiEvent data class LoginFailure(val message: String) : LoginUiEvent data object OnGuestLoginButtonClick : LoginUiEvent data object OnCloseButtonClick : LoginUiEvent + + companion object { + const val PROVIDER_TYPE_KAKAO = "KAKAO" + const val PROVIDER_TYPE_GOOGLE = "GOOGLE" + } } diff --git a/feature/login/src/main/res/drawable/ic_google.xml b/feature/login/src/main/res/drawable/ic_google.xml new file mode 100644 index 00000000..4b106792 --- /dev/null +++ b/feature/login/src/main/res/drawable/ic_google.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index 51ac38df..32c3f405 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ 책 덮기 전 한 문장을 기록해보세요 카카오로 시작하기 + Google로 시작하기 약관 동의 후\n독서 기록을 남겨보세요 약관 전체 동의 시작하기 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b3f5690b..5a739351 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,6 +51,10 @@ logger = "2.2.0" ## Kakao Login kakao-core = "2.22.0" +## Google Credential Manager +androidx-credentials = "1.6.0-beta03" +googleid = "1.1.1" + ## Image Load coil-compose = "2.7.0" landscapist = "2.5.1" @@ -138,6 +142,10 @@ detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-form kakao-auth = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakao-core" } +androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "androidx-credentials" } +androidx-credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "androidx-credentials" } +googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleid" } + lottie-compose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } From 3fa8b2ddcf564115dc0d5c6c3a971d9f95220b9e Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 17 Dec 2025 12:51:20 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[BOOK-474]=20feat:=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC=20border=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EB=B3=80=EA=B2=BD=EB=90=9C=20?= =?UTF-8?q?=EC=83=89=EC=83=81=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원가입 없이 둘러보기 텍스트 버튼 contentColor 변경 및 padding 변경사항 반영 --- .../core/designsystem/component/button/ButtonColorStyle.kt | 3 ++- .../com/ninecraft/booket/core/designsystem/theme/Color.kt | 2 +- .../main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt index 464f21df..d66ff110 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt @@ -28,7 +28,7 @@ enum class ReedButtonColorStyle { SECONDARY -> ReedTheme.colors.contentPrimary TERTIARY -> ReedTheme.colors.contentBrand STROKE -> ReedTheme.colors.contentBrand - TEXT -> ReedTheme.colors.borderBrand + TEXT -> ReedTheme.colors.contentTertiary KAKAO -> ReedTheme.colors.contentPrimary GOOGLE -> ReedTheme.colors.contentPrimary } @@ -45,6 +45,7 @@ enum class ReedButtonColorStyle { @Composable fun borderStroke() = when (this) { STROKE -> BorderStroke(1.dp, ReedTheme.colors.borderBrand) + GOOGLE -> BorderStroke(1.dp, ReedTheme.colors.borderPrimary) else -> null } } diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt index 0bc9ee0c..962b9814 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt @@ -63,7 +63,7 @@ val Blue700 = Color(0xFF007BFF) val Blue800 = Color(0xFF1269EC) val Blue900 = Color(0xFF1F47CD) -val Kakao = Color(0xFFFBD300) +val Kakao = Color(0xFFFFEB00) val Google = Color(0xFF4285F4) val Blank = Color(0xFFD6D6D6) val HomeBg = Color(0xFFF0F9E8) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index cdbee347..ea6859b1 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -136,7 +136,7 @@ internal fun LoginUi( } ) Spacer( - modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing2 else ReedTheme.spacing.spacing8), + modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing3 else ReedTheme.spacing.spacing8), ) if (state.returnToScreen == null) { ReedTextButton( From 79cdf06757a3ba076cb704653dccd3ae538ad4b7 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 17 Dec 2025 12:56:00 +0900 Subject: [PATCH 3/5] [BOOK-474] chore: style check success --- .../core/designsystem/component/button/ButtonColorStyle.kt | 1 - .../com/ninecraft/booket/core/designsystem/theme/Color.kt | 1 - .../com/ninecraft/booket/feature/login/GoogleLoginClient.kt | 2 +- .../ninecraft/booket/feature/login/HandleLoginSideEffects.kt | 2 +- .../main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt | 2 +- 5 files changed, 3 insertions(+), 5 deletions(-) diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt index d66ff110..d3550498 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/component/button/ButtonColorStyle.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.ninecraft.booket.core.designsystem.theme.Google import com.ninecraft.booket.core.designsystem.theme.Kakao import com.ninecraft.booket.core.designsystem.theme.ReedTheme diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt index 962b9814..3d2d035e 100644 --- a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt @@ -64,7 +64,6 @@ val Blue800 = Color(0xFF1269EC) val Blue900 = Color(0xFF1F47CD) val Kakao = Color(0xFFFFEB00) -val Google = Color(0xFF4285F4) val Blank = Color(0xFFD6D6D6) val HomeBg = Color(0xFFF0F9E8) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt index a18c6c95..a5839133 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt @@ -79,4 +79,4 @@ internal class GoogleLoginClient { onFailure(context.getString(designR.string.unknown_error_message)) } } -} \ No newline at end of file +} diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt index f30bef77..00722db9 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt @@ -47,7 +47,7 @@ internal fun HandleLoginSideEffects( LoginUiEvent.Login( providerType = LoginUiEvent.PROVIDER_TYPE_GOOGLE, token = idToken, - ) + ), ) }, onFailure = { errorMessage -> diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index ea6859b1..82cd656c 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -133,7 +133,7 @@ internal fun LoginUi( contentDescription = "Google Icon", tint = Color.Unspecified, ) - } + }, ) Spacer( modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing3 else ReedTheme.spacing.spacing8), From f8a88c1b2e2edc444e7a54e09ca344a4f3ae791a Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 17 Dec 2025 13:05:52 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[BOOK-474]=20refactor:=20Login=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit client 패키지내에서 google/kakao LoginClient 관리 --- .../feature/login/HandleLoginSideEffects.kt | 2 ++ .../login/{ => client}/GoogleLoginClient.kt | 28 +++++++++---------- .../login/{ => client}/KakaoLoginClient.kt | 22 +++++++-------- 3 files changed, 27 insertions(+), 25 deletions(-) rename feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/{ => client}/GoogleLoginClient.kt (71%) rename feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/{ => client}/KakaoLoginClient.kt (65%) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt index 00722db9..01251b43 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/HandleLoginSideEffects.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext +import com.ninecraft.booket.feature.login.client.GoogleLoginClient +import com.ninecraft.booket.feature.login.client.KakaoLoginClient import com.skydoves.compose.effects.RememberedEffect import kotlinx.coroutines.launch diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt similarity index 71% rename from feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt rename to feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt index a5839133..86b08f99 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/GoogleLoginClient.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt @@ -1,4 +1,4 @@ -package com.ninecraft.booket.feature.login +package com.ninecraft.booket.feature.login.client import android.content.Context import androidx.credentials.CredentialManager @@ -10,7 +10,7 @@ import androidx.credentials.exceptions.GetCredentialException import androidx.credentials.exceptions.NoCredentialException import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential -import com.ninecraft.booket.core.designsystem.R as designR +import com.ninecraft.booket.core.designsystem.R import com.orhanobut.logger.Logger import dev.zacsweers.metro.Inject @@ -22,7 +22,7 @@ internal class GoogleLoginClient { onSuccess: (String) -> Unit, onFailure: (String) -> Unit, ) { - val credentialManager = CredentialManager.create(context) + val credentialManager = CredentialManager.Companion.create(context) val googleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(false) @@ -41,17 +41,17 @@ internal class GoogleLoginClient { ) handleSignIn(result, onSuccess, onFailure, context) } catch (e: GetCredentialCancellationException) { - Logger.e("Google 로그인 취소됨") - onFailure(context.getString(designR.string.unknown_error_message)) + Logger.e("Google 로그인 취소됨, ${e.message}") + onFailure(context.getString(R.string.unknown_error_message)) } catch (e: NoCredentialException) { - Logger.e("Google 계정을 찾을 수 없음") - onFailure(context.getString(designR.string.unknown_error_message)) + Logger.e("Google 계정을 찾을 수 없음, ${e.message}") + onFailure(context.getString(R.string.unknown_error_message)) } catch (e: GetCredentialException) { - Logger.e("Google 로그인 실패: ${e.message}") - onFailure(context.getString(designR.string.network_error_message)) + Logger.e("Google 로그인 실패, ${e.message}") + onFailure(context.getString(R.string.unknown_error_message)) } catch (e: Exception) { Logger.e("알 수 없는 오류: ${e.message}") - onFailure(context.getString(designR.string.unknown_error_message)) + onFailure(context.getString(R.string.unknown_error_message)) } } @@ -64,19 +64,19 @@ internal class GoogleLoginClient { val credential = result.credential if (credential is CustomCredential && - credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + credential.type == GoogleIdTokenCredential.Companion.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { try { - val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) + val googleIdTokenCredential = GoogleIdTokenCredential.Companion.createFrom(credential.data) val idToken = googleIdTokenCredential.idToken Logger.d("Google 로그인 성공: ${googleIdTokenCredential.id}") onSuccess(idToken) } catch (e: Exception) { Logger.e("Google ID Token 파싱 실패: ${e.message}") - onFailure(context.getString(designR.string.unknown_error_message)) + onFailure(context.getString(R.string.unknown_error_message)) } } else { Logger.e("예상치 못한 credential type: ${credential.type}") - onFailure(context.getString(designR.string.unknown_error_message)) + onFailure(context.getString(R.string.unknown_error_message)) } } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/KakaoLoginClient.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt similarity index 65% rename from feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/KakaoLoginClient.kt rename to feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt index 6f1b7d22..1f1ec63f 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/KakaoLoginClient.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt @@ -1,12 +1,12 @@ -package com.ninecraft.booket.feature.login +package com.ninecraft.booket.feature.login.client import android.content.Context import com.kakao.sdk.auth.model.OAuthToken import com.kakao.sdk.common.model.AuthError import com.kakao.sdk.user.UserApiClient -import com.ninecraft.booket.core.designsystem.R as designR -import dev.zacsweers.metro.Inject +import com.ninecraft.booket.core.designsystem.R import com.orhanobut.logger.Logger +import dev.zacsweers.metro.Inject @Inject internal class KakaoLoginClient { @@ -19,14 +19,14 @@ internal class KakaoLoginClient { when { error != null -> handleLoginError(context, error, onFailure) token != null -> handleLoginSuccess(token, onSuccess, onFailure, context) - else -> onFailure(context.getString(designR.string.unknown_error_message)) + else -> onFailure(context.getString(R.string.unknown_error_message)) } } - if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { - UserApiClient.instance.loginWithKakaoTalk(context, callback = kakaoCallback) + if (UserApiClient.Companion.instance.isKakaoTalkLoginAvailable(context)) { + UserApiClient.Companion.instance.loginWithKakaoTalk(context, callback = kakaoCallback) } else { - UserApiClient.instance.loginWithKakaoAccount(context, callback = kakaoCallback) + UserApiClient.Companion.instance.loginWithKakaoAccount(context, callback = kakaoCallback) } } @@ -38,12 +38,12 @@ internal class KakaoLoginClient { when { (error is AuthError && error.response.error == "ProtocolError") -> { Logger.e("로그인 실패: ${error.response.error}, ${error.response.errorDescription}") - onFailure(context.getString(designR.string.network_error_message)) + onFailure(context.getString(R.string.network_error_message)) } else -> { Logger.e("로그인 실패: ${error.message}") - onFailure(context.getString(designR.string.unknown_error_message)) + onFailure(context.getString(R.string.unknown_error_message)) } } } @@ -54,10 +54,10 @@ internal class KakaoLoginClient { onFailure: (String) -> Unit, context: Context, ) { - UserApiClient.instance.me { user, _ -> + UserApiClient.Companion.instance.me { user, _ -> user?.let { onSuccess(token.accessToken) - } ?: onFailure(context.getString(designR.string.unknown_error_message)) + } ?: onFailure(context.getString(R.string.unknown_error_message)) } } } From ca2b3f01f08222ff9a1270973930fad88c07d639 Mon Sep 17 00:00:00 2001 From: easyhooon Date: Wed, 17 Dec 2025 16:09:08 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[BOOK-474]=20chore:=20=ED=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20Companion=20=ED=82=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ninecraft/booket/core/common/constants/BookStatus.kt | 2 +- .../impl/datasource/DefaultNotificationDataSource.kt | 2 +- .../datastore/impl/datasource/DefaultTokenDataSource.kt | 2 +- .../booket/feature/detail/book/BookDetailUiState.kt | 2 +- .../ninecraft/booket/feature/library/LibraryUiState.kt | 2 +- .../booket/feature/login/client/GoogleLoginClient.kt | 6 +++--- .../booket/feature/login/client/KakaoLoginClient.kt | 8 ++++---- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt index 6745d507..2a5fe6c4 100644 --- a/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt +++ b/core/common/src/main/kotlin/com/ninecraft/booket/core/common/constants/BookStatus.kt @@ -16,7 +16,7 @@ enum class BookStatus(val value: String) { } } - companion object Companion { + companion object { fun fromValue(value: String): BookStatus? { return entries.find { it.value == value } } diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt index 579426c8..1e8d2879 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultNotificationDataSource.kt @@ -48,7 +48,7 @@ class DefaultNotificationDataSource( } } - companion object Companion { + companion object { private val USER_NOTIFICATION_ENABLED = booleanPreferencesKey("USER_NOTIFICATION_ENABLED") private val LAST_SYNCED_NOTIFICATION_ENABLED = booleanPreferencesKey("LAST_SYNCED_NOTIFICATION_ENABLED") } diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultTokenDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultTokenDataSource.kt index c4b179f2..b492fc6e 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultTokenDataSource.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultTokenDataSource.kt @@ -64,7 +64,7 @@ class DefaultTokenDataSource( }.orEmpty() } - companion object Companion { + companion object { private val ACCESS_TOKEN = stringPreferencesKey("ACCESS_TOKEN") private val REFRESH_TOKEN = stringPreferencesKey("REFRESH_TOKEN") } diff --git a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt index 550d0ee1..95343ae1 100644 --- a/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt +++ b/feature/detail/src/main/kotlin/com/ninecraft/booket/feature/detail/book/BookDetailUiState.kt @@ -98,7 +98,7 @@ enum class RecordSort(val value: String) { } } - companion object Companion { + companion object { fun fromValue(value: String): RecordSort? { return entries.find { it.value == value } } diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt index cac40f41..4ab0366b 100644 --- a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt +++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryUiState.kt @@ -85,7 +85,7 @@ enum class LibraryFilterOption(val value: String) { } } - companion object Companion { + companion object { fun fromValue(value: String): LibraryFilterOption? { return entries.find { it.value == value } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt index 86b08f99..b3712be3 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/GoogleLoginClient.kt @@ -22,7 +22,7 @@ internal class GoogleLoginClient { onSuccess: (String) -> Unit, onFailure: (String) -> Unit, ) { - val credentialManager = CredentialManager.Companion.create(context) + val credentialManager = CredentialManager.create(context) val googleIdOption = GetGoogleIdOption.Builder() .setFilterByAuthorizedAccounts(false) @@ -64,9 +64,9 @@ internal class GoogleLoginClient { val credential = result.credential if (credential is CustomCredential && - credential.type == GoogleIdTokenCredential.Companion.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { + credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) { try { - val googleIdTokenCredential = GoogleIdTokenCredential.Companion.createFrom(credential.data) + val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data) val idToken = googleIdTokenCredential.idToken Logger.d("Google 로그인 성공: ${googleIdTokenCredential.id}") onSuccess(idToken) diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt index 1f1ec63f..9334d1a6 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/client/KakaoLoginClient.kt @@ -23,10 +23,10 @@ internal class KakaoLoginClient { } } - if (UserApiClient.Companion.instance.isKakaoTalkLoginAvailable(context)) { - UserApiClient.Companion.instance.loginWithKakaoTalk(context, callback = kakaoCallback) + if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { + UserApiClient.instance.loginWithKakaoTalk(context, callback = kakaoCallback) } else { - UserApiClient.Companion.instance.loginWithKakaoAccount(context, callback = kakaoCallback) + UserApiClient.instance.loginWithKakaoAccount(context, callback = kakaoCallback) } } @@ -54,7 +54,7 @@ internal class KakaoLoginClient { onFailure: (String) -> Unit, context: Context, ) { - UserApiClient.Companion.instance.me { user, _ -> + UserApiClient.instance.me { user, _ -> user?.let { onSuccess(token.accessToken) } ?: onFailure(context.getString(R.string.unknown_error_message))