diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4a074d97a..93facf65f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,6 +23,14 @@ android { vectorDrawables { useSupportLibrary = true } + + buildConfigField( + "String", + "NATIVE_APP_KEY", + gradleLocalProperties(rootDir, providers).getProperty("native.app.key"), + ) + manifestPlaceholders["NATIVE_APP_KEY"] = + gradleLocalProperties(rootDir, providers).getProperty("nativeAppKey") } buildTypes { @@ -104,4 +112,8 @@ dependencies { debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + // KakaoDependencies + implementation(libs.kakao.user) + } diff --git a/app/src/main/java/com/terning/point/MyApp.kt b/app/src/main/java/com/terning/point/MyApp.kt index 98ac3bc11..2d991562c 100644 --- a/app/src/main/java/com/terning/point/MyApp.kt +++ b/app/src/main/java/com/terning/point/MyApp.kt @@ -2,6 +2,7 @@ package com.terning.point import android.app.Application import androidx.appcompat.app.AppCompatDelegate +import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp import timber.log.Timber @@ -13,6 +14,7 @@ class MyApp : Application() { initTimber() setDayMode() + initKakoSdk() } private fun initTimber() { @@ -22,4 +24,8 @@ class MyApp : Application() { private fun setDayMode() { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) } + + private fun initKakoSdk() { + KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY) + } } \ No newline at end of file diff --git a/core/src/main/java/com/terning/core/extension/ContextExt.kt b/core/src/main/java/com/terning/core/extension/ContextExt.kt index 7deeaaec9..370fb9f06 100644 --- a/core/src/main/java/com/terning/core/extension/ContextExt.kt +++ b/core/src/main/java/com/terning/core/extension/ContextExt.kt @@ -1,9 +1,6 @@ package com.terning.core.extension -import android.app.Activity import android.content.Context -import android.view.View -import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.annotation.StringRes diff --git a/feature/build.gradle.kts b/feature/build.gradle.kts index 56209c2ce..d76df3c05 100644 --- a/feature/build.gradle.kts +++ b/feature/build.gradle.kts @@ -91,4 +91,7 @@ dependencies { implementation(libs.ossLicense) implementation(libs.lottie) + // KakaoDependencies + implementation(libs.kakao.user) + } \ No newline at end of file diff --git a/feature/src/main/AndroidManifest.xml b/feature/src/main/AndroidManifest.xml index f2df3a230..1e31d319a 100644 --- a/feature/src/main/AndroidManifest.xml +++ b/feature/src/main/AndroidManifest.xml @@ -14,5 +14,21 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/main/MainNavigator.kt b/feature/src/main/java/com/terning/feature/main/MainNavigator.kt index a885ac342..472557036 100644 --- a/feature/src/main/java/com/terning/feature/main/MainNavigator.kt +++ b/feature/src/main/java/com/terning/feature/main/MainNavigator.kt @@ -10,9 +10,9 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.terning.feature.calendar.navigation.navigateCalendar -import com.terning.feature.home.navigation.Home import com.terning.feature.home.navigation.navigateHome import com.terning.feature.mypage.navigation.navigateMyPage +import com.terning.feature.onboarding.signin.navigation.SignIn import com.terning.feature.search.navigation.navigateSearch class MainNavigator( @@ -22,7 +22,7 @@ class MainNavigator( @Composable get() = navController .currentBackStackEntryAsState().value?.destination - val startDestination = Home + val startDestination = SignIn val currentTab: MainTab? @Composable get() = MainTab.find { tab -> diff --git a/feature/src/main/java/com/terning/feature/main/MainScreen.kt b/feature/src/main/java/com/terning/feature/main/MainScreen.kt index 257bf2e23..4e3e13d6f 100644 --- a/feature/src/main/java/com/terning/feature/main/MainScreen.kt +++ b/feature/src/main/java/com/terning/feature/main/MainScreen.kt @@ -7,13 +7,10 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon -import androidx.compose.material3.LocalAbsoluteTonalElevation -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -27,6 +24,8 @@ import com.terning.core.designsystem.theme.White import com.terning.feature.calendar.navigation.calendarNavGraph import com.terning.feature.home.navigation.homeNavGraph import com.terning.feature.mypage.navigation.myPageNavGraph +import com.terning.feature.onboarding.signin.navigation.signInNavGraph +import com.terning.feature.onboarding.signup.navigation.signUpNavGraph import com.terning.feature.search.navigation.searchNavGraph import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -58,6 +57,8 @@ fun MainScreen( calendarNavGraph() searchNavGraph() myPageNavGraph() + signInNavGraph(navHostController = navigator.navController) + signUpNavGraph() } } } @@ -108,7 +109,6 @@ private fun MainBottomBar( } } - private object NoRippleInteractionSource : MutableInteractionSource { override val interactions: Flow = emptyFlow() diff --git a/feature/src/main/java/com/terning/feature/mypage/MyPageRoute.kt b/feature/src/main/java/com/terning/feature/mypage/MyPageRoute.kt index f72da0835..2302237e4 100644 --- a/feature/src/main/java/com/terning/feature/mypage/MyPageRoute.kt +++ b/feature/src/main/java/com/terning/feature/mypage/MyPageRoute.kt @@ -26,7 +26,7 @@ fun MyPageRoute( val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - val state by viewModel.state.collectAsStateWithLifecycle(lifecycleOwner = LocalLifecycleOwner.current) + val state by viewModel.state.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) LaunchedEffect(key1 = true) { viewModel.getFriendsInfo(2) diff --git a/feature/src/main/java/com/terning/feature/onboarding/filtering/FilteringRoute.kt b/feature/src/main/java/com/terning/feature/onboarding/filtering/FilteringRoute.kt new file mode 100644 index 000000000..bf1e1ec23 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/filtering/FilteringRoute.kt @@ -0,0 +1,9 @@ +package com.terning.feature.onboarding.filtering + +import androidx.compose.runtime.Composable +import com.terning.feature.onboarding.filtering.navigation.FilteringNavigation + +@Composable +fun FilteringRoute(){ + +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/filtering/navigation/FilteringNavigation.kt b/feature/src/main/java/com/terning/feature/onboarding/filtering/navigation/FilteringNavigation.kt new file mode 100644 index 000000000..6dcc12e7b --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/filtering/navigation/FilteringNavigation.kt @@ -0,0 +1,4 @@ +package com.terning.feature.onboarding.filtering.navigation + +class FilteringNavigation { +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signin/SignInRoute.kt b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInRoute.kt new file mode 100644 index 000000000..707a13a21 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInRoute.kt @@ -0,0 +1,83 @@ +package com.terning.feature.onboarding.signin + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +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 androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.flowWithLifecycle +import androidx.navigation.NavHostController +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.core.extension.toast +import com.terning.feature.R +import com.terning.feature.home.navigation.navigateHome +import com.terning.feature.onboarding.signin.component.KakaoButton + +@Composable +fun SignInRoute( + viewModel: SignInViewModel = hiltViewModel(), + navController: NavHostController, +) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + val signInState by viewModel.signInState.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) + + LaunchedEffect(viewModel.signInSideEffects, lifecycleOwner) { + viewModel.signInSideEffects.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { sideEffect -> + when (sideEffect) { + is SignInSideEffect.ShowToast -> context.toast(sideEffect.message) + is SignInSideEffect.NavigateToHome -> navController.navigateHome() + } + } + } + + SignInScreen( + onSignInClick = { viewModel.startKakaoLogIn(context) } + ) +} + +@Composable +fun SignInScreen( + modifier: Modifier = Modifier, + onSignInClick: () -> Unit = {} +) { + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.img_terning_point), + contentDescription = null, + modifier = Modifier + .size(500.dp), + ) + KakaoButton( + title = stringResource(id = R.string.sign_in_kakao_button), + onSignInClick = { onSignInClick() }, + modifier = modifier.padding(horizontal = 20.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SignInScreenPreview() { + TerningTheme { + SignInScreen() + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signin/SignInSideEffect.kt b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInSideEffect.kt new file mode 100644 index 000000000..412652e66 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInSideEffect.kt @@ -0,0 +1,8 @@ +package com.terning.feature.onboarding.signin + +import androidx.annotation.StringRes + +sealed class SignInSideEffect { + data object NavigateToHome : SignInSideEffect() + data class ShowToast(@StringRes val message: Int) : SignInSideEffect() +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signin/SignInState.kt b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInState.kt new file mode 100644 index 000000000..f517d6e63 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInState.kt @@ -0,0 +1,7 @@ +package com.terning.feature.onboarding.signin + +import com.terning.core.state.UiState + +data class SignInState ( + val accessToken: UiState = UiState.Loading +) \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signin/SignInViewModel.kt b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInViewModel.kt new file mode 100644 index 000000000..639b9eb71 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signin/SignInViewModel.kt @@ -0,0 +1,86 @@ +package com.terning.feature.onboarding.signin + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.terning.core.state.UiState +import com.terning.feature.R +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SignInViewModel @Inject constructor() : ViewModel() { + + private val _signInState = MutableStateFlow(SignInState()) + val signInState: StateFlow + get() = _signInState.asStateFlow() + + private val _signInSideEffects = MutableSharedFlow() + val signInSideEffects: SharedFlow + get() = _signInSideEffects.asSharedFlow() + + fun startKakaoLogIn(context: Context) { + if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { + UserApiClient.instance.loginWithKakaoTalk(context) { token, error -> + signInResult(context, token, error) + } + } else { + UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> + signInResult(context, token, error) + } + } + } + + private fun signInResult(context: Context, token: OAuthToken?, error: Throwable?) { + viewModelScope.launch { + if (error != null) { + signInFailure(context, error) + } else if (token != null) { + signInSuccess(token) + } + } + } + + private fun signInFailure(context: Context, error: Throwable?) { + if (error.toString().contains(KAKAO_NOT_LOGGED_IN)) { + UserApiClient.instance.loginWithKakaoAccount(context) { token, error -> + signInResult(context, token, error) + } + } else { + sigInCancellationOrError(error) + } + } + + private fun sigInCancellationOrError(error: Throwable?) { + viewModelScope.launch { + if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { + _signInSideEffects.emit(SignInSideEffect.ShowToast(R.string.sign_in_kakao_cancel)) + } else { + _signInSideEffects.emit(SignInSideEffect.ShowToast(R.string.sign_in_kakao_login_fail)) + } + } + } + + private fun signInSuccess(token: OAuthToken) { + viewModelScope.launch { + _signInState.value = + _signInState.value.copy(accessToken = UiState.Success(token.accessToken)) + _signInSideEffects.emit(SignInSideEffect.NavigateToHome) + } + } + + companion object { + private const val KAKAO_NOT_LOGGED_IN = "statusCode=302" + } +} diff --git a/feature/src/main/java/com/terning/feature/onboarding/signin/component/KakaoButton.kt b/feature/src/main/java/com/terning/feature/onboarding/signin/component/KakaoButton.kt new file mode 100644 index 000000000..97591bb83 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signin/component/KakaoButton.kt @@ -0,0 +1,57 @@ +package com.terning.feature.onboarding.signin.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.core.extension.noRippleClickable +import com.terning.feature.R + +@Composable +fun KakaoButton( + title: String, + modifier: Modifier = Modifier, + onSignInClick: () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(Color.Yellow) + .noRippleClickable { onSignInClick() } + .padding(vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_signin_kakao), + contentDescription = null, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Text( + text = title, + // TODO : style 추가하기 + ) + } +} + +@Preview(showBackground = true) +@Composable +fun KakaoButtonPreview() { + TerningTheme { + KakaoButton( + title = "카카오로 로그인하기", + onSignInClick = {} + ) + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signin/navigation/SignInNavigation.kt b/feature/src/main/java/com/terning/feature/onboarding/signin/navigation/SignInNavigation.kt new file mode 100644 index 000000000..0c3d35760 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signin/navigation/SignInNavigation.kt @@ -0,0 +1,30 @@ +package com.terning.feature.onboarding.signin.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.terning.core.navigation.MainTabRoute +import com.terning.feature.onboarding.signin.SignInRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateSignIn(navOptions: NavOptions? = null) { + navigate( + route = SignIn, + navOptions = navOptions + ) +} + +fun NavGraphBuilder.signInNavGraph( + navHostController: NavHostController +) { + composable { + SignInRoute( + navController = navHostController + ) + } +} + +@Serializable +data object SignIn : MainTabRoute \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signup/SignUpRoute.kt b/feature/src/main/java/com/terning/feature/onboarding/signup/SignUpRoute.kt new file mode 100644 index 000000000..9d3d3d411 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signup/SignUpRoute.kt @@ -0,0 +1,13 @@ +package com.terning.feature.onboarding.signup + +import androidx.compose.runtime.Composable + +@Composable +fun SignUpRoute() { + SignUpScreen() +} + +@Composable +fun SignUpScreen() { + +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/onboarding/signup/navigation/SignUpNavigation.kt b/feature/src/main/java/com/terning/feature/onboarding/signup/navigation/SignUpNavigation.kt new file mode 100644 index 000000000..1379d0a64 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/onboarding/signup/navigation/SignUpNavigation.kt @@ -0,0 +1,25 @@ +package com.terning.feature.onboarding.signup.navigation + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.terning.core.navigation.MainTabRoute +import com.terning.feature.onboarding.signup.SignUpRoute +import kotlinx.serialization.Serializable + +fun NavController.navigateSignUp(navOptions: NavOptions? = null) { + navigate( + route = SignUp, + navOptions = navOptions + ) +} + +fun NavGraphBuilder.signUpNavGraph() { + composable { + SignUpRoute() + } +} + +@Serializable +data object SignUp : MainTabRoute \ No newline at end of file diff --git a/feature/src/main/res/drawable/ic_signin_kakao.xml b/feature/src/main/res/drawable/ic_signin_kakao.xml new file mode 100644 index 000000000..cbe75d056 --- /dev/null +++ b/feature/src/main/res/drawable/ic_signin_kakao.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/feature/src/main/res/drawable/img_terning_point.png b/feature/src/main/res/drawable/img_terning_point.png new file mode 100644 index 000000000..fc2158319 Binary files /dev/null and b/feature/src/main/res/drawable/img_terning_point.png differ diff --git a/feature/src/main/res/values/strings.xml b/feature/src/main/res/values/strings.xml index 133c0442b..71aa3bf69 100644 --- a/feature/src/main/res/values/strings.xml +++ b/feature/src/main/res/values/strings.xml @@ -10,4 +10,9 @@ 탐색 마이페이지 + + 카카오로 로그인하기 + 카카오톡 로그인에 실패했습니다 + 로그인을 취소하였습니다 + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bcce3048f..1a57fcdb2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,9 @@ foundationAndroid = "1.6.8" material3Android = "1.2.1" lifecycleRuntimeComposeAndroid = "2.8.2" +## Kakao +kakaoVersion = "2.20.1" + [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } @@ -149,6 +152,8 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } ossLicense = {group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "ossVersion"} lottie = {group = "com.airbnb.android", name = "lottie", version.ref = "lottieVersion"} +kakao-user = {group = "com.kakao.sdk", name = "v2-user", version.ref = "kakaoVersion"} + [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 8a2c140ec..bd88f3f5d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,6 +16,9 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + + // KakaoSDK repository + maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") } }