Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#45 깃허브 로그인 구현 #103

Binary file added .DS_Store
Binary file not shown.
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/2023-emmsale.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions android/2023-emmsale/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("org.jetbrains.kotlin.android")
id("com.android.application") version "8.0.2"
kotlin("plugin.serialization") version "1.8.21"
}

android {
Expand All @@ -15,6 +18,16 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

buildConfigField(
"String",
"GITHUB_CLIENT_ID",
getApiKey("GITHUB_CLIENT_ID")
)
}

buildFeatures {
buildConfig = true
}

buildTypes {
Expand All @@ -38,12 +51,22 @@ android {
}
}

fun getApiKey(propertyKey: String): String {
return gradleLocalProperties(rootDir).getProperty(propertyKey)
}

dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.browser:browser:1.5.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("androidx.fragment:fragment-ktx:1.6.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
}
17 changes: 16 additions & 1 deletion android/2023-emmsale/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -11,15 +13,28 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Emmsale"
android:usesCleartextTraffic="true"
tools:targetApi="33">
<activity
android:name="com.emmsale.MainActivity"
android:name=".presentation.ui.login.LoginActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<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="github-auth"
android:scheme="kerdy" />
</intent-filter>
</activity>
</application>

Expand Down
12 changes: 0 additions & 12 deletions android/2023-emmsale/app/src/main/java/com/emmsale/MainActivity.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.emmsale.data.common

import retrofit2.HttpException
import retrofit2.Response

sealed interface ApiResult<T : Any>
class ApiSuccess<T : Any>(val data: T) : ApiResult<T>
class ApiError<T : Any>(val code: Int, val message: String?) : ApiResult<T>
class ApiException<T : Any>(val e: Throwable) : ApiResult<T>

suspend fun <T : Any> handleApi(
execute: suspend () -> Response<T>,
): ApiResult<T> {
return try {
val response = execute()
val body = response.body()

when {
response.isSuccessful && body != null -> ApiSuccess(body)
else -> ApiError(code = response.code(), message = response.message())
}
} catch (e: HttpException) {
ApiError(code = e.code(), message = e.message())
} catch (e: Throwable) {
ApiException(e)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.emmsale.data.common

import com.emmsale.data.login.LoginService
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit

object RetrofitProvider {
private const val BASE_URL = "https://kerdy.kro.kr/"
private val jsonMediaType = "application/json".toMediaType()
private val json = Json {
coerceInputValues = true
encodeDefaults = true
isLenient = true
}
private val jsonConverterFactory = json.asConverterFactory(jsonMediaType)

private val okhttpClient = OkHttpClient.Builder()
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(120, TimeUnit.SECONDS)
.build()

private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(jsonConverterFactory)
.client(okhttpClient)
.build()

val loginService: LoginService = retrofit.create(LoginService::class.java)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.emmsale.data.login

import com.emmsale.data.login.dto.LoginApiModel

data class Login(
val uid: Long,
val accessToken: String,
val isNewMember: Boolean,
) {
companion object {
fun from(apiModel: LoginApiModel): Login = Login(
accessToken = apiModel.accessToken,
uid = apiModel.uid,
isNewMember = apiModel.isNewMember,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.emmsale.data.login

import com.emmsale.data.common.ApiResult

interface LoginRepository {
suspend fun saveGithubCode(code: String): ApiResult<Login>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.emmsale.data.login

import com.emmsale.data.common.ApiError
import com.emmsale.data.common.ApiException
import com.emmsale.data.common.ApiResult
import com.emmsale.data.common.ApiSuccess
import com.emmsale.data.common.handleApi
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext

@OptIn(DelicateCoroutinesApi::class)
class LoginRepositoryImpl(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val coroutineScope: CoroutineScope = GlobalScope,
private val loginService: LoginService,
) : LoginRepository {
override suspend fun saveGithubCode(code: String): ApiResult<Login> =
withContext(coroutineScope.coroutineContext + dispatcher) {
when (val loginResponse = handleApi { loginService.saveGithubCode(code) }) {
is ApiSuccess -> ApiSuccess(Login.from(loginResponse.data))
is ApiError -> ApiError(loginResponse.code, loginResponse.message)
is ApiException -> ApiException(loginResponse.e)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.emmsale.data.login

import com.emmsale.data.login.dto.LoginApiModel
import retrofit2.Response
import retrofit2.http.POST
import retrofit2.http.Query

interface LoginService {
@POST("/login/github/callback")
suspend fun saveGithubCode(
@Query("code") code: String
): Response<LoginApiModel>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.emmsale.data.login.dto

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class LoginApiModel(
@SerialName("memberId")
val uid: Long,
@SerialName("accessToken")
val accessToken: String,
@SerialName("newMember")
val isNewMember: Boolean,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.emmsale.data.token

import com.emmsale.data.login.Login

data class Token(
val uid: Long,
val accessToken: String,
) {
companion object {
fun from(login: Login): Token = Token(
accessToken = login.accessToken,
uid = login.uid,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.emmsale.data.token

interface TokenRepository {
suspend fun saveToken(token: Token)
suspend fun getToken(): Token?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.emmsale.data.token

import android.content.SharedPreferences
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class TokenRepositoryImpl(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val preference: SharedPreferences,
) : TokenRepository {
override suspend fun saveToken(token: Token) = withContext(dispatcher) {
preference.edit().putLong(UID_KEY, token.uid).apply()
preference.edit().putString(ACCESS_TOKEN_KEY, token.accessToken).apply()
// preference.edit().putString(REFRESH_TOKEN_KEY, token.accessToken).apply()
}

override suspend fun getToken(): Token? = withContext(dispatcher) {
val uid = preference.getLong(UID_KEY, DEFAULT_UID_VALUE)
val accessToken = preference.getString(ACCESS_TOKEN_KEY, DEFAULT_TOKEN_VALUE)
// val refreshToken = preference.getString(REFRESH_TOKEN_KEY, DEFAULT_TOKEN_VALUE)

if (uid == DEFAULT_UID_VALUE) return@withContext null
if (accessToken == null || accessToken == DEFAULT_TOKEN_VALUE) return@withContext null
// if (refreshToken == null || refreshToken == DEFAULT_TOKEN_VALUE) return@withContext null

Token(uid, accessToken)
}

companion object {
private const val UID_KEY = "uid_key"
private const val ACCESS_TOKEN_KEY = "access_token_key"
// private const val REFRESH_TOKEN_KEY = "refresh_token_key"

private const val DEFAULT_UID_VALUE = -1L
private const val DEFAULT_TOKEN_VALUE = "default"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.emmsale.presentation.base.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

open class BaseViewModel(
private val dispatcherProvider: DispatcherProvider,
) : ViewModel(), DispatcherProvider by dispatcherProvider {
private val _loadingState: MutableLiveData<LoadingUiState> = MutableLiveData<LoadingUiState>()
val loadingState: LiveData<LoadingUiState> = _loadingState

protected fun changeLoadingState(loadingUiState: LoadingUiState) {
_loadingState.postValue(loadingUiState)
}

protected inline fun onMain(
crossinline body: suspend CoroutineScope.() -> Unit,
): Job = viewModelScope.launch(main) {
body(this)
}

protected inline fun onIo(
crossinline body: suspend CoroutineScope.() -> Unit,
): Job = viewModelScope.launch(io) {
body(this)
}

protected inline fun onDefault(
crossinline body: suspend CoroutineScope.() -> Unit,
): Job = viewModelScope.launch(default) {
body(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.emmsale.presentation.base.viewmodel

import kotlinx.coroutines.CoroutineDispatcher

interface DispatcherProvider {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
}
Loading
Loading