diff --git a/.github/workflows/develop_branch.yml b/.github/workflows/develop_branch.yml index 37c77efd..d8459cde 100644 --- a/.github/workflows/develop_branch.yml +++ b/.github/workflows/develop_branch.yml @@ -14,6 +14,12 @@ jobs: - name: check out repository uses: actions/checkout@v4 + - name: Generate local.properties + env: + KAKAO_NATIVE_APP_KEY: ${{ secrets.KAKAO_NATIVE_APP_KEY }} + run: | + echo "kakao.native.app.key=$KAKAO_NATIVE_APP_KEY" >> local.properties + - name: set up JDK 17 uses: actions/setup-java@v4 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aa69bd50..6720c3a2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.bitnagil.android.application) alias(libs.plugins.bitnagil.android.hilt) @@ -7,8 +9,32 @@ plugins { android { namespace = "com.threegap.bitnagil" + val properties = + Properties().apply { + val propFile = rootProject.file("local.properties") + if (propFile.exists()) { + load(propFile.inputStream()) + } + } + defaultConfig { applicationId = "com.threegap.bitnagil" + + val kakaoNativeAppKey = + (properties["kakao.native.app.key"] as? String) + ?: System.getenv("KAKAO_NATIVE_APP_KEY") + ?: throw GradleException("KAKAO_NATIVE_APP_KEY 값이 없습니다.") + + manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = kakaoNativeAppKey + buildConfigField( + type = "String", + name = "KAKAO_NATIVE_APP_KEY", + value = "\"$kakaoNativeAppKey\"", + ) + } + + buildFeatures { + buildConfig = true } } @@ -19,4 +45,5 @@ dependencies { implementation(projects.data) implementation(projects.domain) implementation(projects.presentation) + implementation(libs.kakao.v2.user) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2e5d6cea..a421af2f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,20 @@ - + + + + + + + + + + + diff --git a/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt b/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt index 2ac6eb46..0676666e 100644 --- a/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt +++ b/app/src/main/java/com/threegap/bitnagil/BitnagilApplication.kt @@ -1,11 +1,17 @@ package com.threegap.bitnagil import android.app.Application +import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class BitnagilApplication : Application() { override fun onCreate() { super.onCreate() + initKakaoSdk() + } + + private fun initKakaoSdk() { + KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY) } } diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index a9f96acc..911f76fc 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import com.threegap.bitnagil.presentation.home.HomeScreen -import com.threegap.bitnagil.presentation.login.LoginScreen +import com.threegap.bitnagil.presentation.login.LoginScreenContainer @Composable fun MainNavHost( @@ -18,9 +18,7 @@ fun MainNavHost( modifier = modifier, ) { composable { - LoginScreen( - onLoginClick = { navigator.navController.navigate(Route.Home) }, - ) + LoginScreenContainer() } composable { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b449fd4d..f2d7e8be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,7 @@ espressoCore = "3.6.1" material = "1.12.0" orbit = "6.1.0" javax = "1" +kakaoLogin = "2.21.4" [libraries] ## Android Gradle Plugin @@ -115,6 +116,7 @@ kotlin-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coro ## Other material = { group = "com.google.android.material", name = "material", version.ref = "material" } +kakao-v2-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakaoLogin" } [bundles] androidx-core = [ diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts index f5a0d017..9e2539dc 100644 --- a/presentation/build.gradle.kts +++ b/presentation/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { implementation(libs.bundles.androidx.core) implementation(libs.bundles.orbit) + implementation(libs.kakao.v2.user) testImplementation(libs.junit) testImplementation(libs.kotlin.coroutines.test) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt index c6c2f00c..dcf6ff2e 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginScreen.kt @@ -1,42 +1,81 @@ package com.threegap.bitnagil.presentation.login import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.kakao.sdk.user.UserApiClient import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.presentation.login.model.LoginIntent +import com.threegap.bitnagil.presentation.login.model.LoginSideEffect +import org.orbitmvi.orbit.compose.collectSideEffect @Composable -fun LoginScreen( - onLoginClick: () -> Unit, +fun LoginScreenContainer(viewModel: LoginViewModel = hiltViewModel()) { + val context = LocalContext.current + val client = UserApiClient.instance + + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + is LoginSideEffect.RequestKakaoTalkLogin -> { + client.loginWithKakaoTalk(context) { token, error -> + viewModel.sendIntent(LoginIntent.OnKakaoLoginResult(token, error)) + } + } + + is LoginSideEffect.RequestKakaoAccountLogin -> { + client.loginWithKakaoAccount(context) { token, error -> + viewModel.sendIntent(LoginIntent.OnKakaoLoginResult(token, error)) + } + } + } + } + + LoginScreen( + onKakaoLoginClick = { + viewModel.sendIntent( + LoginIntent.OnKakaoLoginClick(client.isKakaoTalkLoginAvailable(context)), + ) + }, + ) +} + +@Composable +private fun LoginScreen( + onKakaoLoginClick: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + Box( modifier = modifier .fillMaxSize() .background(Color.White), ) { - Text(text = "여긴 로그인 화면") - - Spacer(modifier = Modifier.height(10.dp)) + Text( + text = "빛나길 로고", + modifier = Modifier.align(Alignment.Center), + ) Button( - onClick = onLoginClick, + onClick = onKakaoLoginClick, + modifier = + Modifier + .align(Alignment.BottomCenter) + .padding(20.dp), ) { - Text("로그인 버튼 눌러보던가") + Text( + text = "카카오 로그인버튼", + ) } } } @@ -46,7 +85,7 @@ fun LoginScreen( private fun LoginScreenPreview() { BitnagilTheme { LoginScreen( - onLoginClick = {}, + onKakaoLoginClick = {}, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt new file mode 100644 index 00000000..653b8619 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/LoginViewModel.kt @@ -0,0 +1,68 @@ +package com.threegap.bitnagil.presentation.login + +import android.util.Log +import androidx.lifecycle.SavedStateHandle +import com.kakao.sdk.common.model.ClientError +import com.kakao.sdk.common.model.ClientErrorCause +import com.kakao.sdk.user.UserApiClient +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel +import com.threegap.bitnagil.presentation.login.model.LoginIntent +import com.threegap.bitnagil.presentation.login.model.LoginSideEffect +import com.threegap.bitnagil.presentation.login.model.LoginState +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.syntax.simple.SimpleSyntax +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel + @Inject + constructor( + private val savedStateHandle: SavedStateHandle, + ) : MviViewModel( + initState = LoginState(), + savedStateHandle = savedStateHandle, + ) { + override suspend fun SimpleSyntax.reduceState( + intent: LoginIntent, + state: LoginState, + ): LoginState? = + when (intent) { + is LoginIntent.OnKakaoLoginClick -> { + if (!intent.onKakaoTalkLoginAvailable) { + sendSideEffect(LoginSideEffect.RequestKakaoAccountLogin) + } else { + sendSideEffect(LoginSideEffect.RequestKakaoTalkLogin) + } + null + } + + is LoginIntent.OnKakaoLoginResult -> { + when { + intent.token != null -> { + Log.i("KakaoLogin", "로그인 성공 ${intent.token.accessToken}") + UserApiClient.instance.me { user, error -> + if (error != null) { + Log.e("KakaoLogin", "사용자 정보 요청 실패", error) + } else if (user != null) { + Log.i( + "KakaoLogin", + "사용자 정보 요청 성공" + + "\n이메일: ${user.kakaoAccount?.email}" + + "\n닉네임: ${user.kakaoAccount?.profile?.nickname}", + ) + } + } + } + + intent.error is ClientError && intent.error.reason == ClientErrorCause.Cancelled -> { + Log.e("KakaoLogin", "로그인 취소", intent.error) + } + + intent.error != null -> { + Log.e("KakaoLogin", "로그인 실패", intent.error) + } + } + null + } + } + } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt new file mode 100644 index 00000000..16a39a5f --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginIntent.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.presentation.login.model + +import com.kakao.sdk.auth.model.OAuthToken +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent + +sealed class LoginIntent : MviIntent { + data class OnKakaoLoginClick(val onKakaoTalkLoginAvailable: Boolean) : LoginIntent() + + data class OnKakaoLoginResult( + val token: OAuthToken?, + val error: Throwable?, + ) : LoginIntent() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt new file mode 100644 index 00000000..a94c2ce9 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginSideEffect.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.presentation.login.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect + +sealed interface LoginSideEffect : MviSideEffect { + data object RequestKakaoTalkLogin : LoginSideEffect + + data object RequestKakaoAccountLogin : LoginSideEffect +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt new file mode 100644 index 00000000..35dc826b --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/login/model/LoginState.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.presentation.login.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState +import kotlinx.parcelize.Parcelize + +@Parcelize +data class LoginState( + val isLoggedIn: Boolean = false, +) : MviState diff --git a/settings.gradle.kts b/settings.gradle.kts index d21f0d65..175bcc5c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = java.net.URI("https://devrepo.kakao.com/nexus/content/groups/public/") } } }