Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
import kotlin.apply

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}

val localProperties = Properties().apply {
val localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.inputStream().use { load(it) }
}
}
kotlin {
androidTarget {
compilerOptions {
Expand All @@ -29,6 +36,11 @@ kotlin {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
// 카카오 소셜 로그인
implementation(libs.kakao.user)

// Android Credential Manager (구글 소셜 로그인) - 번들 사용 시
implementation(libs.bundles.social.login.google)
}
commonMain.dependencies {
implementation(compose.runtime)
Expand All @@ -46,6 +58,8 @@ kotlin {
implementation(projects.core.ui)

implementation(projects.feature.settings.impl)
// Coroutines (버전 카탈로그 사용)
implementation(libs.kotlinx.coroutines.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand All @@ -56,13 +70,24 @@ kotlin {
android {
namespace = "com.yourssu.pingpong"
compileSdk = libs.versions.android.compileSdk.get().toInt()

buildFeatures {
buildConfig = true
}
defaultConfig {
applicationId = "com.yourssu.pingpong"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
buildConfigField("String", "KAKAO_NATIVE_APP_KEY", "\"${localProperties.getProperty("KAKAO_NATIVE_APP_KEY")}\"")
manifestPlaceholders["KAKAO_NATIVE_APP_KEY"] = localProperties.getProperty("KAKAO_NATIVE_APP_KEY") ?: ""

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// local.properties에서 읽어와서 BuildConfig에 등록
val googleClientId = localProperties.getProperty("GOOGLE_SERVER_CLIENT_ID") ?: ""
buildConfigField("String", "GOOGLE_SERVER_CLIENT_ID", "\"$googleClientId\"")
manifestPlaceholders["GOOGLE_SERVER_CLIENT_ID"] = googleClientId

}
packaging {
resources {
Expand Down
14 changes: 13 additions & 1 deletion composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand All @@ -17,6 +17,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data android:host="oauth"
android:scheme="kakao${KAKAO_NATIVE_APP_KEY}" />
</intent-filter>
</activity>
</application>

</manifest>
53 changes: 53 additions & 0 deletions composeApp/src/androidMain/kotlin/auth/AuthManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// composeApp/src/androidMain/kotlin/auth/AuthManager.kt
package auth

import android.content.Context
import androidx.credentials.ClearCredentialStateRequest
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

actual class GoogleAuthManager(private val context: Context) {
private val credentialManager = CredentialManager.create(context)

actual suspend fun signIn(): GoogleUser? = withContext(Dispatchers.IO) {
try {
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(com.yourssu.pingpong.BuildConfig.GOOGLE_SERVER_CLIENT_ID)
.setAutoSelectEnabled(false)
.build()

val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()

val result = credentialManager.getCredential(context, request)
val credential = result.credential

if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
// credential.data(Bundle)로부터 객체를 명시적으로 생성합니다.
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)

GoogleUser(
idToken = googleIdTokenCredential.idToken,
displayName = googleIdTokenCredential.displayName,
email = googleIdTokenCredential.id
)
} else {
println("DEBUG: 예상치 못한 크리덴셜 타입: ${credential.type}")
null
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}

actual suspend fun signOut() {
credentialManager.clearCredentialState(ClearCredentialStateRequest())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.yourssu.pingpong

import android.content.Context
import com.kakao.sdk.user.UserApiClient

class AndroidSocialLoginHandler(private val context: Context) : SocialLoginHandler {

override fun loginWithKakao(onSuccess: (String) -> Unit, onFailure: (Throwable) -> Unit) {
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context) { token, error ->
if (error != null) onFailure(error)
else if (token != null) onSuccess(token.accessToken)
}
} else {
UserApiClient.instance.loginWithKakaoAccount(context) { token, error ->
if (error != null) onFailure(error)
else if (token != null) onSuccess(token.accessToken)
}
}
}

override fun logout(onResult: (Throwable?) -> Unit) {
// 카카오 SDK 로그아웃 호출
UserApiClient.instance.logout { error ->
onResult(error)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,60 @@
package com.yourssu.pingpong

import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Base64
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.tooling.preview.Preview
import auth.GoogleAuthManager
import java.security.MessageDigest
import com.kakao.sdk.common.KakaoSdk //카카오용
import com.yourssu.pingpong.BuildConfig

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)

// 카카오 SDK 초기화 (네이티브 앱 키 입력)
KakaoSdk.init(this, BuildConfig.KAKAO_NATIVE_APP_KEY)
setContent {
App()
val _loginHandler = remember { AndroidSocialLoginHandler(this) }
val googleAuthManager = GoogleAuthManager(this) // Context 전달
App(
loginHandler = _loginHandler,
googleAuthManager = googleAuthManager
)
}
}
}

@Preview
@Composable
fun AppAndroidPreview() {
App()
// 1. 카카오 로그인용 가짜 핸들러 (기존)
val previewHandler = object : SocialLoginHandler {
override fun loginWithKakao(onSuccess: (String) -> Unit, onFailure: (Throwable) -> Unit) {
println("Preview: 카카오 로그인 클릭됨")
}
override fun logout(onResult: (Throwable?) -> Unit) {
onResult(null)
}
}

// 2. 구글 로그인용 가짜 매니저
// 현재 클래스 형태라면, Context가 필요한 actual class 구조상
// 프리뷰 전용으로 null이나 더미 Context를 넣은 인스턴스가 필요할 수 있습니다.
// 여기서는 로직 연결을 위해 '임시'로 생성하는 코드를 예시로 듭니다.
val context = androidx.compose.ui.platform.LocalContext.current
val previewGoogleManager = remember { GoogleAuthManager(context) }

App(
loginHandler = previewHandler,
googleAuthManager = previewGoogleManager
)
}
14 changes: 14 additions & 0 deletions composeApp/src/commonMain/kotlin/auth/AuthManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package auth

// 사용자 정보를 담을 데이터 클래스!
data class GoogleUser(
val idToken: String,
val displayName: String?,
val email: String?
)

// 플랫폼별 구현을 위한 expect 클래스
expect class GoogleAuthManager {
suspend fun signIn(): GoogleUser?
suspend fun signOut()
}
51 changes: 51 additions & 0 deletions composeApp/src/commonMain/kotlin/auth/AuthViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.yourssu.pingpong

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import auth.GoogleAuthManager
import kotlinx.coroutines.launch

class AuthViewModel : ViewModel() {
// UI에서 관찰할 상태 (내부에서만 수정 가능하도록 private set 설정)
var isLoggedIn by mutableStateOf(false)
private set

var userName by mutableStateOf("")
private set

// 카카오 로그인 로직
fun loginWithKakao(loginHandler: SocialLoginHandler) {
loginHandler.loginWithKakao(
onSuccess = {
isLoggedIn = true
},
onFailure = { error ->
println("카카오 로그인 실패: ${error.message}")
}
)
}

// 구글 로그인 로직 (코루틴 사용)
fun loginWithGoogle(googleAuthManager: GoogleAuthManager) {
viewModelScope.launch {
val user = googleAuthManager.signIn()
if (user != null) {
userName = user.displayName ?: "구글 사용자"
isLoggedIn = true
}
}
}

// 로그아웃 로직
fun logout(loginHandler: SocialLoginHandler, googleAuthManager: GoogleAuthManager) {
viewModelScope.launch {
loginHandler.logout { }
googleAuthManager.signOut()
isLoggedIn = false
userName = ""
}
}
}
Loading