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/") }
}
}