Skip to content

Commit 425c71d

Browse files
committed
[BOOK-62] feat: 클라이언트 카카오 로그인 구현
1 parent a669f2d commit 425c71d

File tree

7 files changed

+211
-49
lines changed

7 files changed

+211
-49
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
<resources>
22
<string name="app_name">Booket</string>
3+
<string name="network_error_message">네트워크 연결이 불안해요.\n잠시후 다시 이용해주세요.</string>
4+
<string name="server_error_message">이용에 불편을 드려 죄송합니다.\n잠시후 다시 이용해주세요.</string>
5+
<string name="unknown_error_message">알 수 없는 오류가 발생하였습니다.</string>
36
</resources>

core/ui/src/main/kotlin/com/ninecraft/booket/core/ui/component/Button.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding
55
import androidx.compose.foundation.layout.sizeIn
66
import androidx.compose.foundation.shape.RoundedCornerShape
77
import androidx.compose.material.icons.Icons
8-
import androidx.compose.material.icons.filled.ArrowBackIosNew
98
import androidx.compose.material.icons.filled.Check
109
import androidx.compose.material3.Button
1110
import androidx.compose.material3.ButtonDefaults

feature/login/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ ksp {
2020

2121
dependencies {
2222
implementations(
23+
projects.feature.home,
24+
2325
libs.logger,
26+
libs.kakao.auth,
2427
)
2528
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.ninecraft.booket.feature.login
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.ui.platform.LocalContext
7+
8+
@Composable
9+
internal fun HandleLoginEffects(
10+
state: LoginScreen.State,
11+
eventSink: (LoginScreen.Event) -> Unit,
12+
) {
13+
val context = LocalContext.current
14+
val kakaoAuthClient = remember { KakaoAuthClient() }
15+
16+
LaunchedEffect(state.sideEffect) {
17+
when (state.sideEffect) {
18+
is LoginScreen.SideEffect.KakaoLogin -> {
19+
kakaoAuthClient.loginWithKakao(
20+
context = context,
21+
onSuccess = { token ->
22+
eventSink(LoginScreen.Event.LoginSuccess(token))
23+
},
24+
onFailure = { errorMessage ->
25+
eventSink(LoginScreen.Event.LoginFailure(errorMessage))
26+
},
27+
)
28+
}
29+
30+
else -> {}
31+
}
32+
}
33+
34+
if (state.sideEffect != null) {
35+
eventSink(LoginScreen.Event.InitSideEffect)
36+
}
37+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.ninecraft.booket.feature.login
2+
3+
import android.content.Context
4+
import com.kakao.sdk.auth.model.OAuthToken
5+
import com.kakao.sdk.common.model.AuthError
6+
import com.kakao.sdk.user.UserApiClient
7+
import com.ninecraft.booket.core.designsystem.R as designR
8+
import javax.inject.Inject
9+
import com.orhanobut.logger.Logger
10+
11+
internal class KakaoAuthClient @Inject constructor() {
12+
fun loginWithKakao(
13+
context: Context,
14+
onSuccess: (String) -> Unit,
15+
onFailure: (String) -> Unit,
16+
) {
17+
val kakaoCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
18+
when {
19+
error != null -> handleLoginError(context, error, onFailure)
20+
token != null -> handleLoginSuccess(token, onSuccess, onFailure, context)
21+
else -> onFailure(context.getString(designR.string.unknown_error_message))
22+
}
23+
}
24+
25+
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
26+
UserApiClient.instance.loginWithKakaoTalk(context, callback = kakaoCallback)
27+
} else {
28+
UserApiClient.instance.loginWithKakaoAccount(context, callback = kakaoCallback)
29+
}
30+
}
31+
32+
private fun handleLoginError(
33+
context: Context,
34+
error: Throwable,
35+
onFailure: (String) -> Unit,
36+
) {
37+
when {
38+
(error is AuthError && error.response.error == "ProtocolError") -> {
39+
Logger.e("로그인 실패: ${error.response.error}, ${error.response.errorDescription}")
40+
onFailure(context.getString(designR.string.network_error_message))
41+
}
42+
43+
else -> {
44+
Logger.e("로그인 실패: ${error.message}")
45+
onFailure(context.getString(designR.string.unknown_error_message))
46+
}
47+
}
48+
}
49+
50+
private fun handleLoginSuccess(
51+
token: OAuthToken,
52+
onSuccess: (String) -> Unit,
53+
onFailure: (String) -> Unit,
54+
context: Context,
55+
) {
56+
UserApiClient.instance.me { user, _ ->
57+
user?.let {
58+
Logger.d("로그인 성공: ${token.accessToken}, ${"${it.id}"} ${it.kakaoAccount?.profile?.nickname}, ${it.kakaoAccount?.profile?.profileImageUrl}")
59+
onSuccess(token.accessToken)
60+
// TODO: 서버 로그인 로직 추가
61+
// eventSink(serverLogin(token.accessToken))
62+
} ?: onFailure(context.getString(designR.string.unknown_error_message))
63+
}
64+
}
65+
}

feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.ninecraft.booket.feature.login
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.getValue
5+
import androidx.compose.runtime.mutableStateOf
46
import androidx.compose.runtime.rememberCoroutineScope
7+
import androidx.compose.runtime.setValue
8+
import com.ninecraft.booket.feature.home.HomeScreen
59
import com.slack.circuit.codegen.annotations.CircuitInject
10+
import com.slack.circuit.retained.rememberRetained
611
import com.slack.circuit.runtime.Navigator
712
import com.slack.circuit.runtime.presenter.Presenter
813
import dagger.assisted.Assisted
@@ -18,8 +23,46 @@ class LoginPresenter @AssistedInject constructor(
1823
@Composable
1924
override fun present(): LoginScreen.State {
2025
val scope = rememberCoroutineScope()
26+
var isLoading by rememberRetained { mutableStateOf(false) }
27+
var sideEffect by rememberRetained { mutableStateOf<LoginScreen.SideEffect?>(null) }
2128

22-
return LoginScreen.State {}
29+
fun showLoading() {
30+
isLoading = true
31+
}
32+
33+
fun hideLoading() {
34+
isLoading = false
35+
}
36+
37+
fun clearSideEffect() {
38+
sideEffect = null
39+
}
40+
41+
fun handleEvent(event: LoginScreen.Event) {
42+
when (event) {
43+
is LoginScreen.Event.InitSideEffect -> clearSideEffect()
44+
is LoginScreen.Event.OnKakaoLoginButtonClick -> {
45+
showLoading()
46+
sideEffect = LoginScreen.SideEffect.KakaoLogin
47+
}
48+
49+
is LoginScreen.Event.LoginFailure -> {
50+
hideLoading()
51+
sideEffect = LoginScreen.SideEffect.ShowToast(event.message)
52+
}
53+
54+
is LoginScreen.Event.LoginSuccess -> {
55+
hideLoading()
56+
navigator.resetRoot(HomeScreen)
57+
}
58+
}
59+
}
60+
61+
return LoginScreen.State(
62+
isLoading = isLoading,
63+
sideEffect = sideEffect,
64+
eventSink = ::handleEvent,
65+
)
2366
}
2467

2568
@CircuitInject(LoginScreen::class, ActivityRetainedComponent::class)

feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginScreen.kt

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
77
import androidx.compose.foundation.layout.fillMaxWidth
88
import androidx.compose.foundation.layout.height
99
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.material3.CircularProgressIndicator
1011
import androidx.compose.material3.Icon
1112
import androidx.compose.material3.Text
1213
import androidx.compose.runtime.Composable
@@ -34,10 +35,22 @@ import kotlinx.parcelize.Parcelize
3435
@Parcelize
3536
data object LoginScreen : Screen {
3637
data class State(
38+
val isLoading: Boolean = false,
39+
val sideEffect: SideEffect? = null,
3740
val eventSink: (Event) -> Unit,
3841
) : CircuitUiState
3942

40-
sealed interface Event : CircuitUiEvent
43+
sealed interface SideEffect {
44+
data object KakaoLogin : SideEffect
45+
data class ShowToast(val message: String) : SideEffect
46+
}
47+
48+
sealed interface Event : CircuitUiEvent {
49+
data object InitSideEffect : Event
50+
data object OnKakaoLoginButtonClick : Event
51+
data class LoginSuccess(val accessToken: String) : Event
52+
data class LoginFailure(val message: String) : Event
53+
}
4154
}
4255

4356
@CircuitInject(LoginScreen::class, ActivityRetainedComponent::class)
@@ -46,63 +59,62 @@ internal fun Login(
4659
state: LoginScreen.State,
4760
modifier: Modifier = Modifier,
4861
) {
62+
HandleLoginEffects(
63+
state = state,
64+
eventSink = state.eventSink,
65+
)
66+
4967
Column(
5068
modifier = modifier.fillMaxSize(),
5169
horizontalAlignment = Alignment.CenterHorizontally,
5270
verticalArrangement = Arrangement.Center,
5371
) {
54-
LoginContent(
55-
state = state,
56-
modifier = modifier,
57-
)
58-
}
59-
}
60-
61-
@Suppress("unused")
62-
@Composable
63-
internal fun LoginContent(
64-
state: LoginScreen.State,
65-
modifier: Modifier = Modifier,
66-
) {
67-
Box(modifier = modifier.fillMaxSize()) {
68-
Text(
69-
text = "로그인",
70-
modifier = Modifier.align(Alignment.Center),
71-
)
72-
BooketButton(
73-
onClick = {},
74-
modifier = Modifier
75-
.fillMaxWidth()
76-
.padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
77-
.height(56.dp)
78-
.align(Alignment.BottomCenter),
79-
containerColor = Kakao,
80-
contentColor = Color(0xFF121212),
81-
text = {
82-
Text(
83-
text = stringResource(id = R.string.kakao_login),
84-
fontSize = 18.sp,
85-
style = TextStyle(
86-
fontWeight = FontWeight.SemiBold,
72+
Box(modifier = modifier.fillMaxSize()) {
73+
Text(
74+
text = "로그인",
75+
modifier = Modifier.align(Alignment.Center),
76+
)
77+
BooketButton(
78+
onClick = {
79+
state.eventSink(LoginScreen.Event.OnKakaoLoginButtonClick)
80+
},
81+
modifier = Modifier
82+
.fillMaxWidth()
83+
.padding(start = 32.dp, end = 32.dp, bottom = 32.dp)
84+
.height(56.dp)
85+
.align(Alignment.BottomCenter),
86+
containerColor = Kakao,
87+
contentColor = Color(0xFF121212),
88+
text = {
89+
Text(
90+
text = stringResource(id = R.string.kakao_login),
8791
fontSize = 18.sp,
88-
lineHeight = 25.sp,
89-
),
90-
)
91-
},
92-
leadingIcon = {
93-
Icon(
94-
imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao),
95-
contentDescription = "Kakao Icon",
96-
tint = Color.Unspecified,
97-
)
98-
},
99-
)
92+
style = TextStyle(
93+
fontWeight = FontWeight.SemiBold,
94+
fontSize = 18.sp,
95+
lineHeight = 25.sp,
96+
),
97+
)
98+
},
99+
leadingIcon = {
100+
Icon(
101+
imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao),
102+
contentDescription = "Kakao Icon",
103+
tint = Color.Unspecified,
104+
)
105+
},
106+
)
107+
108+
if (state.isLoading) {
109+
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
110+
}
111+
}
100112
}
101113
}
102114

103115
@DevicePreview
104116
@Composable
105-
private fun LibraryPreview() {
117+
private fun LoginPreview() {
106118
BooketTheme {
107119
Login(
108120
state = LoginScreen.State(

0 commit comments

Comments
 (0)