From e9ff104a1007d966ba7723e235071181dbfc6720 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Tue, 20 Jan 2026 23:17:46 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[NDGL-17]=20chore:=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EB=A0=88=EC=9D=B4=EC=96=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=B9=8C=EB=93=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 6 ++++ .../java/com/yapp/ndgl/NDGLApplication.kt | 11 ++++++- .../main/kotlin/NDGLAndroidLibraryPlugin.kt | 2 ++ build-logic/src/main/kotlin/NDGLDataPlugin.kt | 4 +++ data/core/build.gradle.kts | 29 +++++++++++++++++++ gradle/libs.versions.toml | 15 ++++++++-- 6 files changed, 63 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 90ab0433..b9ff797b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,16 @@ plugins { id("ndgl.application") + alias(libs.plugins.google.services) + alias(libs.plugins.firebase.crashlytics) } android { namespace = Configuration.APPLICATION_ID + buildFeatures { + buildConfig = true + } + buildTypes { release { isMinifyEnabled = true diff --git a/app/src/main/java/com/yapp/ndgl/NDGLApplication.kt b/app/src/main/java/com/yapp/ndgl/NDGLApplication.kt index 3ab95ace..b3c356d0 100644 --- a/app/src/main/java/com/yapp/ndgl/NDGLApplication.kt +++ b/app/src/main/java/com/yapp/ndgl/NDGLApplication.kt @@ -2,6 +2,15 @@ package com.yapp.ndgl import android.app.Application import dagger.hilt.android.HiltAndroidApp +import timber.log.Timber @HiltAndroidApp -class NDGLApplication : Application() +class NDGLApplication : Application() { + override fun onCreate() { + super.onCreate() + + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } +} diff --git a/build-logic/src/main/kotlin/NDGLAndroidLibraryPlugin.kt b/build-logic/src/main/kotlin/NDGLAndroidLibraryPlugin.kt index 6b5d82f5..9e306ad0 100644 --- a/build-logic/src/main/kotlin/NDGLAndroidLibraryPlugin.kt +++ b/build-logic/src/main/kotlin/NDGLAndroidLibraryPlugin.kt @@ -1,6 +1,7 @@ import convention.configureComposeAndroid import convention.configureFirebase import convention.configureKotlinAndroid +import convention.configureTimber import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies @@ -16,6 +17,7 @@ class NDGLAndroidLibraryPlugin : Plugin { configureKotlinAndroid() configureFirebase() configureComposeAndroid() + configureTimber() dependencies { "implementation"(libs.findLibrary("kotlinx-immutable").get()) diff --git a/build-logic/src/main/kotlin/NDGLDataPlugin.kt b/build-logic/src/main/kotlin/NDGLDataPlugin.kt index 71affa75..5d581f37 100644 --- a/build-logic/src/main/kotlin/NDGLDataPlugin.kt +++ b/build-logic/src/main/kotlin/NDGLDataPlugin.kt @@ -13,5 +13,9 @@ class NDGLDataPlugin : Plugin { configureKotlinAndroid() configureHiltAndroid() configureCoroutineAndroid() + + if (path != ":data:core") { + dependencies.add("implementation", project(":data:core")) + } } } diff --git a/data/core/build.gradle.kts b/data/core/build.gradle.kts index 1b88e1e2..89d2451f 100644 --- a/data/core/build.gradle.kts +++ b/data/core/build.gradle.kts @@ -1,7 +1,36 @@ +import java.util.Properties +import kotlin.apply + plugins { id("ndgl.data") + alias(libs.plugins.kotlin.serialization) } android { namespace = "com.yapp.ndgl.data.core" + + defaultConfig { + val localProperties = Properties().apply { + load(rootProject.file("local.properties").bufferedReader()) + } + + buildConfigField( + "String", + "NDGL_BASE_URL", + localProperties["NDGL_BASE_URL"] as String, + ) + } + + buildFeatures { + buildConfig = true + } +} + +dependencies { + implementation(libs.androidx.datastore) + implementation(libs.retrofit) + implementation(libs.retrofit.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.json) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 47615e63..3ad8f1c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ hiltNavigationCompose = "1.3.0" # Retrofit retrofit = "3.0.0" -okhttp = "5.3.2" +okhttp = "4.12.0" # Kotlinx Serialization kotlinxSerializationJson = "1.7.3" @@ -30,12 +30,17 @@ nav3Core = "1.0.0" # Coroutines coroutines = "1.10.2" +# DataStore +datastore = "1.1.1" + # Lifecycle lifecycleViewmodelCompose = "2.10.0" lifecycleViewmodelNav3 = "2.10.0" # Firebase firebaseBom = "34.5.0" +googleServices = "4.4.4" +firebaseCrashlytics = "3.0.6" # Other timber = "5.0.1" @@ -43,7 +48,6 @@ desugarJdkLibs = "2.1.5" appcompat = "1.7.1" material = "1.13.0" - # Static Analysis detekt = "1.23.8" ktlint-gradle = "12.1.1" @@ -88,6 +92,9 @@ androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecy kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +# DataStore +androidx-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } + # Lifecycle lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleViewmodelCompose" } @@ -102,6 +109,7 @@ kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collectio firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } # Other timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } @@ -116,7 +124,6 @@ ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devto androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } - [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } @@ -128,3 +135,5 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint-gradle" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } From a3e36dfec3aafd3635b02d6c76f3ef7408490adf Mon Sep 17 00:00:00 2001 From: mj010504 Date: Tue, 20 Jan 2026 23:17:55 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[NDGL-17]=20feat:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EB=B0=8F=20=EC=9C=A0=ED=8B=B8=EB=A6=AC?= =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/yapp/ndgl/core/util/ResultUtil.kt | 13 +++++++++++++ .../yapp/ndgl/data/core/model/BaseResponse.kt | 14 ++++++++++++++ .../ndgl/data/core/model/error/ErrorResponse.kt | 16 ++++++++++++++++ .../core/model/error/HttpResponseException.kt | 7 +++++++ 4 files changed, 50 insertions(+) create mode 100644 core/util/src/main/java/com/yapp/ndgl/core/util/ResultUtil.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/model/error/ErrorResponse.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/model/error/HttpResponseException.kt diff --git a/core/util/src/main/java/com/yapp/ndgl/core/util/ResultUtil.kt b/core/util/src/main/java/com/yapp/ndgl/core/util/ResultUtil.kt new file mode 100644 index 00000000..61e77e2a --- /dev/null +++ b/core/util/src/main/java/com/yapp/ndgl/core/util/ResultUtil.kt @@ -0,0 +1,13 @@ +package com.yapp.ndgl.core.util + +import kotlin.coroutines.cancellation.CancellationException + +suspend inline fun T.suspendRunCatching(crossinline block: suspend T.() -> R): Result { + return try { + Result.success(block()) + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + Result.failure(t) + } +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt new file mode 100644 index 00000000..115ceb15 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt @@ -0,0 +1,14 @@ +package com.yapp.ndgl.data.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class BaseResponse( + val code: String, + val message: String, + val data: T?, +) + +fun BaseResponse.getData(): T { + return data ?: Unit as T +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/error/ErrorResponse.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/error/ErrorResponse.kt new file mode 100644 index 00000000..79c3c618 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/error/ErrorResponse.kt @@ -0,0 +1,16 @@ +package com.yapp.ndgl.data.core.model.error + +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorResponse( + val code: String, + val message: String, + val errors: List? = null, +) + +@Serializable +data class FieldError( + val field: String, + val message: String, +) diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/error/HttpResponseException.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/error/HttpResponseException.kt new file mode 100644 index 00000000..93ca5b5c --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/error/HttpResponseException.kt @@ -0,0 +1,7 @@ +package com.yapp.ndgl.data.core.model.error + +class HttpResponseException( + val code: String, + val errorMessage: String, + val fieldErrors: List? = null, +) : Exception(errorMessage) From 8948a5a94ae7eadea00d3d28db23431fdf2d8520 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Tue, 20 Jan 2026 23:21:41 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[NDGL-17]=20feat:=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EB=AA=A8=EB=93=88=20=EB=B0=8F=20=EB=84=A4?= =?UTF-8?q?=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EB=AF=B8=EB=93=A4=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/adapter/NDGLCallAdapterFactory.kt | 91 +++++++++++++++++++ .../com/yapp/ndgl/data/core/api/NDGLApi.kt | 21 +++++ .../core/authenticator/NDGLAuthenticator.kt | 70 ++++++++++++++ .../yapp/ndgl/data/core/di/NetworkModule.kt | 61 +++++++++++++ .../data/core/interceptor/NDGLInterceptor.kt | 34 +++++++ 5 files changed, 277 insertions(+) create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt new file mode 100644 index 00000000..bcb9d081 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt @@ -0,0 +1,91 @@ +package com.yapp.ndgl.data.core.adapter + +import com.yapp.ndgl.data.core.model.BaseResponse +import com.yapp.ndgl.data.core.model.error.ErrorResponse +import com.yapp.ndgl.data.core.model.error.HttpResponseException +import kotlinx.serialization.json.Json +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Callback +import retrofit2.Response +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NDGLCallAdapterFactory @Inject constructor() : CallAdapter.Factory() { + override fun get( + type: Type, + annotations: Array, + retrofit: Retrofit, + ): CallAdapter<*, *>? { + if (getRawType(type) != Call::class.java) return null + + val wrapperType = getParameterUpperBound(0, type as ParameterizedType) + if (getRawType(wrapperType) != BaseResponse::class.java) return null + + return NDGLCallAdapter(wrapperType) + } +} + +private class NDGLCallAdapter( + private val resultType: Type, +) : CallAdapter> { + override fun responseType(): Type = resultType + override fun adapt(call: Call): Call = NDGLCall(call) +} + +private class NDGLCall( + private val delegate: Call, +) : Call { + private val json = Json { ignoreUnknownKeys = true } + + override fun enqueue(callback: Callback) { + delegate.enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + val body = response.body() + + if (response.isSuccessful && body != null) { + callback.onResponse(this@NDGLCall, response) + } else { + val errorBody = response.errorBody()?.string() ?: "" + + // errorBody를 ErrorResponse로 디코딩 + val errorResponse = try { + json.decodeFromString(errorBody) + } catch (e: Exception) { + null + } + + val exception = HttpResponseException( + code = errorResponse?.code ?: response.code().toString(), + errorMessage = errorResponse?.message ?: errorBody, + fieldErrors = errorResponse?.errors, + ) + + callback.onFailure(this@NDGLCall, exception) + } + } + + override fun onFailure(call: Call, throwable: Throwable) { + callback.onFailure(this@NDGLCall, throwable) + } + }, + ) + } + + override fun clone(): Call = NDGLCall(delegate.clone()) + override fun execute(): Response = + throw NotImplementedError("NDGLCall doesn't support execute()") + + override fun isExecuted(): Boolean = delegate.isExecuted + override fun cancel() = delegate.cancel() + override fun isCanceled(): Boolean = delegate.isCanceled + override fun request(): Request = delegate.request() + override fun timeout(): Timeout = delegate.timeout() +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt new file mode 100644 index 00000000..d4c2ec05 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt @@ -0,0 +1,21 @@ +package com.yapp.ndgl.data.core.api + +import com.yapp.ndgl.data.core.model.BaseResponse +import com.yapp.ndgl.data.core.model.auth.AuthResponse +import com.yapp.ndgl.data.core.model.auth.CreateUserRequest +import com.yapp.ndgl.data.core.model.auth.LoginRequest +import retrofit2.http.Body +import retrofit2.http.POST + +interface NDGLApi { + // Auth + @POST("/api/v1/auth/users") + suspend fun createUser( + @Body request: CreateUserRequest, + ): BaseResponse + + @POST("/api/v1/auth/login") + suspend fun login( + @Body request: LoginRequest, + ): BaseResponse +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt new file mode 100644 index 00000000..a4805b8b --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt @@ -0,0 +1,70 @@ +package com.yapp.ndgl.data.core.authenticator + +import com.yapp.ndgl.data.core.api.NDGLApi +import com.yapp.ndgl.data.core.local.datasource.LocalAuthDataSource +import com.yapp.ndgl.data.core.model.auth.LoginRequest +import com.yapp.ndgl.data.core.model.getData +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import javax.inject.Inject +import javax.inject.Provider + +class NDGLAuthenticator @Inject constructor( + private val localAuthDataSource: LocalAuthDataSource, + private val ndglApi: Provider, +) : Authenticator { + private val mutex = Mutex() + + override fun authenticate(route: Route?, response: Response): Request? { + val originRequest = response.request + + if (originRequest.header("Authorization").isNullOrEmpty()) { + return null + } + + if (originRequest.url.encodedPath.contains("/api/v1/auth/login")) { + runBlocking { + localAuthDataSource.clearSession() + } + + return null + } + + val retryCount = originRequest.header(RETRY_HEADER)?.toIntOrNull() ?: 0 + if (retryCount >= MAX_RETRY_COUNT) { + return null + } + + val uuid = runBlocking { localAuthDataSource.getUuid() } + + val authResponse = runBlocking { + mutex.withLock { + ndglApi.get().login(LoginRequest(uuid)) + } + }.getData() + + runBlocking { + localAuthDataSource.apply { + setAccessToken(authResponse.accessToken) + setUuid(authResponse.uuid) + } + } + + val newRequest = originRequest.newBuilder() + .header(RETRY_HEADER, (retryCount + 1).toString()) + .header("Authorization", "Bearer ${authResponse.accessToken}") + .build() + + return newRequest + } + + companion object { + private const val MAX_RETRY_COUNT = 3 + private const val RETRY_HEADER = "Retry-Count" + } +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt new file mode 100644 index 00000000..893fc746 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt @@ -0,0 +1,61 @@ +package com.yapp.ndgl.data.core.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.yapp.ndgl.data.core.BuildConfig +import com.yapp.ndgl.data.core.adapter.NDGLCallAdapterFactory +import com.yapp.ndgl.data.core.api.NDGLApi +import com.yapp.ndgl.data.core.authenticator.NDGLAuthenticator +import com.yapp.ndgl.data.core.interceptor.NDGLInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + @Singleton + @Provides + fun provideJson(): Json = Json { + ignoreUnknownKeys = true + } + + @Singleton + @Provides + fun provideDefaultOkHttpClient( + interceptor: NDGLInterceptor, + authenticator: NDGLAuthenticator, + ): OkHttpClient { + val builder = OkHttpClient.Builder() + .addInterceptor(interceptor) + .authenticator(authenticator) + + if (BuildConfig.DEBUG) { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + builder.addInterceptor(loggingInterceptor) + } + + return builder.build() + } + + @Singleton + @Provides + fun provideNDGLApi( + json: Json, + okHttpClient: OkHttpClient, + callAdapterFactory: NDGLCallAdapterFactory, + ): NDGLApi = Retrofit.Builder() + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .addCallAdapterFactory(callAdapterFactory) + .baseUrl(BuildConfig.NDGL_BASE_URL) + .build() + .create(NDGLApi::class.java) +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt new file mode 100644 index 00000000..15f8e3c0 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt @@ -0,0 +1,34 @@ +package com.yapp.ndgl.data.core.interceptor + +import com.yapp.ndgl.data.core.local.datasource.LocalAuthDataSource +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import javax.inject.Inject + +class NDGLInterceptor @Inject constructor( + private val localAuthDataSource: LocalAuthDataSource, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val originRequest = chain.request() + val requestBuilder = originRequest.newBuilder() + + if (isAccessTokenUsed(originRequest)) { + requestBuilder.addHeader( + "Authorization", + "Bearer ${runBlocking { localAuthDataSource.getAccessToken() }}", + ) + } + + return chain.proceed(requestBuilder.build()) + } + + private fun isAccessTokenUsed(request: Request): Boolean { + return when (request.url.encodedPath) { + "/api/v1/auth/users" -> false + "/api/v1/auth/login" -> false + else -> true + } + } +} From 856aba594d807780952ed45fb860f82feaa76a76 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Tue, 20 Jan 2026 23:21:47 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[NDGL-17]=20feat:=20DataStore=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EB=A1=9C=EC=BB=AC=20=EC=9D=B8=EC=A6=9D=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=EC=86=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../local/datasource/LocalAuthDataSource.kt | 57 +++++++++++++++++++ .../data/core/local/di/DataStoreModule.kt | 28 +++++++++ .../data/core/local/util/DataStoreUtil.kt | 16 ++++++ 3 files changed, 101 insertions(+) create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt new file mode 100644 index 00000000..5e66a01f --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt @@ -0,0 +1,57 @@ +package com.yapp.ndgl.data.core.local.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.yapp.ndgl.data.core.local.util.handleException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocalAuthDataSource @Inject constructor( + private val dataStore: DataStore, +) { + private val accessToken: Flow = dataStore.data + .handleException() + .map { preferences -> + preferences[ACCESS_TOKEN_KEY] ?: "" + } + + private val uuid: Flow = dataStore.data + .handleException() + .map { preferences -> + preferences[UUID_KEY] ?: "" + } + + suspend fun getAccessToken(): String = accessToken.first() + + suspend fun getUuid(): String = uuid.first() + + suspend fun setAccessToken(token: String) { + dataStore.edit { preferences -> + preferences[ACCESS_TOKEN_KEY] = token + } + } + + suspend fun setUuid(uuid: String) { + dataStore.edit { preferences -> + preferences[UUID_KEY] = uuid + } + } + + suspend fun clearSession() { + dataStore.edit { preferences -> + preferences.remove(ACCESS_TOKEN_KEY) + preferences.remove(UUID_KEY) + } + } + + private companion object { + private val ACCESS_TOKEN_KEY = stringPreferencesKey("access_token") + private val UUID_KEY = stringPreferencesKey("uuid") + } +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt new file mode 100644 index 00000000..212c038c --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt @@ -0,0 +1,28 @@ +package com.yapp.ndgl.data.core.local.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DataStoreModule { + + private const val TOKEN_PREFERENCES = "token_preferences" + private val Context.tokenDataStore: DataStore by preferencesDataStore(name = TOKEN_PREFERENCES) + + @Provides + @Singleton + fun provideTokenDataStore( + @ApplicationContext context: Context, + ): DataStore { + return context.tokenDataStore + } +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt new file mode 100644 index 00000000..e9e40f5b --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt @@ -0,0 +1,16 @@ +package com.yapp.ndgl.data.core.local.util + +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.emptyPreferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import java.io.IOException + +internal fun Flow.handleException(): Flow = + this.catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } From 9535b626efa98ceb1cb47e8194b47a2495251b1c Mon Sep 17 00:00:00 2001 From: mj010504 Date: Wed, 21 Jan 2026 21:29:30 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[NDGL-17]=20chore:=20auth=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/ndgl/data/core/model/auth/AuthResponse.kt | 10 ++++++++++ .../ndgl/data/core/model/auth/CreateUserRequest.kt | 12 ++++++++++++ .../yapp/ndgl/data/core/model/auth/LoginRequest.kt | 8 ++++++++ 3 files changed, 30 insertions(+) create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt new file mode 100644 index 00000000..b0be40f9 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt @@ -0,0 +1,10 @@ +package com.yapp.ndgl.data.core.model.auth + +import kotlinx.serialization.Serializable + +@Serializable +data class AuthResponse( + val uuid: String, + val accessToken: String, + val nickname: String, +) diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt new file mode 100644 index 00000000..b877eda1 --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt @@ -0,0 +1,12 @@ +package com.yapp.ndgl.data.core.model.auth + +import kotlinx.serialization.Serializable + +@Serializable +data class CreateUserRequest( + val fcmToken: String, + val deviceModel: String, + val deviceOs: String, + val deviceOsVersion: String, + val appVersion: String, +) diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt new file mode 100644 index 00000000..a83ff81c --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt @@ -0,0 +1,8 @@ +package com.yapp.ndgl.data.core.model.auth + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginRequest( + val uuid: String, +) From 7f4cce00b7c89f82076b4c7569db6022e6adaea0 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Sun, 25 Jan 2026 15:53:29 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[NDGL-17]=20chore:=20dataPlugin=EC=97=90?= =?UTF-8?q?=20timber=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20adapter=EC=97=90=20=EB=A1=9C=EA=B9=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-logic/src/main/kotlin/NDGLDataPlugin.kt | 2 ++ .../com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/build-logic/src/main/kotlin/NDGLDataPlugin.kt b/build-logic/src/main/kotlin/NDGLDataPlugin.kt index 5d581f37..b7aed8b5 100644 --- a/build-logic/src/main/kotlin/NDGLDataPlugin.kt +++ b/build-logic/src/main/kotlin/NDGLDataPlugin.kt @@ -1,6 +1,7 @@ import convention.configureCoroutineAndroid import convention.configureHiltAndroid import convention.configureKotlinAndroid +import convention.configureTimber import org.gradle.api.Plugin import org.gradle.api.Project @@ -13,6 +14,7 @@ class NDGLDataPlugin : Plugin { configureKotlinAndroid() configureHiltAndroid() configureCoroutineAndroid() + configureTimber() if (path != ":data:core") { dependencies.add("implementation", project(":data:core")) diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt index bcb9d081..46115098 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/adapter/NDGLCallAdapterFactory.kt @@ -11,6 +11,7 @@ import retrofit2.CallAdapter import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit +import timber.log.Timber import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import javax.inject.Inject @@ -59,6 +60,7 @@ private class NDGLCall( val errorResponse = try { json.decodeFromString(errorBody) } catch (e: Exception) { + Timber.e(e, "Failed to parse error response") null } From d530bbd1b413ba9677a7ffa12e8e593f000b472a Mon Sep 17 00:00:00 2001 From: mj010504 Date: Sun, 25 Jan 2026 17:41:25 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[NDGL-17]=20chore:=20Set=20up=20google-se?= =?UTF-8?q?rvices.json=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android_ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index bb59fb2d..e0e343cd 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -30,6 +30,9 @@ jobs: - name: Set up local.properties run: echo "${{ secrets.LOCAL_PROPERTIES }}" > local.properties + - name: Set up google-services.json + run: echo '${{ secrets.GOOGLE_SERVICES }}' | base64 -d > app/google-services.json + - name: Code style checks run: ./gradlew ktlintCheck detekt From e40753d0ab8919999401c2df44bc7b6107f41b92 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Sun, 25 Jan 2026 17:41:32 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[NDGL-17]=20refactor:=20BaseResponse=20ge?= =?UTF-8?q?tData()=20null=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - null 반환 시 Unit 캐스팅 대신 명확한 에러 메시지 출력 - code와 message 정보 포함하여 디버깅 용이성 향상 --- .../src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt index 115ceb15..6a9c45f2 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/model/BaseResponse.kt @@ -10,5 +10,5 @@ data class BaseResponse( ) fun BaseResponse.getData(): T { - return data ?: Unit as T + return data ?: error("Response data is null. code=$code, message=$message") } From dbe9bb2d624f4fef9c22d81a73c685a363ea8782 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Sun, 25 Jan 2026 17:41:50 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[NDGL-17]=20refactor:=20Authenticator=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EA=B0=B1=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - uuid null/empty 체크 추가 - mutex 내에서 토큰 갱신 및 저장을 원자적으로 수행 - 예외 발생 시 Timber 로깅 추가 - 에러 발생 시 null 반환으로 재시도 중단 --- .../core/authenticator/NDGLAuthenticator.kt | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt index a4805b8b..3c94320d 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt @@ -11,6 +11,7 @@ import okhttp3.Authenticator import okhttp3.Request import okhttp3.Response import okhttp3.Route +import timber.log.Timber import javax.inject.Inject import javax.inject.Provider @@ -40,20 +41,24 @@ class NDGLAuthenticator @Inject constructor( return null } - val uuid = runBlocking { localAuthDataSource.getUuid() } - val authResponse = runBlocking { mutex.withLock { - ndglApi.get().login(LoginRequest(uuid)) - } - }.getData() + try { + val uuid = localAuthDataSource.getUuid() + if (uuid.isNullOrEmpty()) { + return@withLock null + } - runBlocking { - localAuthDataSource.apply { - setAccessToken(authResponse.accessToken) - setUuid(authResponse.uuid) + val response = ndglApi.get().login(LoginRequest(uuid)).getData() + localAuthDataSource.setAccessToken(response.accessToken) + localAuthDataSource.setUuid(response.uuid) + response + } catch (e: Exception) { + Timber.e(e, "Failed to refresh token") + null + } } - } + } ?: return null val newRequest = originRequest.newBuilder() .header(RETRY_HEADER, (retryCount + 1).toString()) From e0edb7cdd2adc83f3d0a644812ff1299e3cbd531 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Sun, 25 Jan 2026 18:47:48 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[NDGL-17]=20chore:=20buildConfigField=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/core/build.gradle.kts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/data/core/build.gradle.kts b/data/core/build.gradle.kts index 89d2451f..6c93d156 100644 --- a/data/core/build.gradle.kts +++ b/data/core/build.gradle.kts @@ -14,11 +14,7 @@ android { load(rootProject.file("local.properties").bufferedReader()) } - buildConfigField( - "String", - "NDGL_BASE_URL", - localProperties["NDGL_BASE_URL"] as String, - ) + buildConfigField("String", "NDGL_BASE_URL", "\"${localProperties["NDGL_BASE_URL"]}\"") } buildFeatures { From daaecf170f399e81898ec765293280c9f9af6d49 Mon Sep 17 00:00:00 2001 From: mj010504 Date: Sun, 1 Feb 2026 19:57:01 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[NDGL-17]=20refactor:=20=EB=84=A4?= =?UTF-8?q?=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EA=B5=AC=EC=A1=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-logic/src/main/kotlin/NDGLDataPlugin.kt | 10 +++++ .../src/main/kotlin/NDGLFeaturePlugin.kt | 2 - data/auth/build.gradle.kts | 2 +- .../com/yapp/ndgl/data/auth/api/AuthApi.kt} | 11 +++-- .../com/yapp/ndgl/data/auth/di/AuthModule.kt | 19 ++++++++ .../ndgl/data/auth/di/AuthNetworkModule.kt | 34 +++++++++++++++ .../data/auth/local}/LocalAuthDataSource.kt | 4 +- .../data/auth}/local/di/DataStoreModule.kt | 2 +- .../data/auth}/local/util/DataStoreUtil.kt | 2 +- .../ndgl/data/auth/model}/AuthResponse.kt | 2 +- .../data/auth/model}/CreateUserRequest.kt | 2 +- .../ndgl/data/auth/model}/LoginRequest.kt | 2 +- .../ndgl/data/auth/token/TokenManagerImpl.kt | 43 +++++++++++++++++++ data/core/build.gradle.kts | 6 --- .../core/authenticator/NDGLAuthenticator.kt | 31 +++---------- .../yapp/ndgl/data/core/di/NetworkModule.kt | 37 +++++++++------- .../data/core/interceptor/NDGLInterceptor.kt | 6 +-- .../yapp/ndgl/data/core/token/TokenManager.kt | 9 ++++ 18 files changed, 157 insertions(+), 67 deletions(-) rename data/{core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt => auth/src/main/java/com/yapp/ndgl/data/auth/api/AuthApi.kt} (60%) create mode 100644 data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthModule.kt create mode 100644 data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthNetworkModule.kt rename data/{core/src/main/java/com/yapp/ndgl/data/core/local/datasource => auth/src/main/java/com/yapp/ndgl/data/auth/local}/LocalAuthDataSource.kt (93%) rename data/{core/src/main/java/com/yapp/ndgl/data/core => auth/src/main/java/com/yapp/ndgl/data/auth}/local/di/DataStoreModule.kt (95%) rename data/{core/src/main/java/com/yapp/ndgl/data/core => auth/src/main/java/com/yapp/ndgl/data/auth}/local/util/DataStoreUtil.kt (91%) rename data/{core/src/main/java/com/yapp/ndgl/data/core/model/auth => auth/src/main/java/com/yapp/ndgl/data/auth/model}/AuthResponse.kt (79%) rename data/{core/src/main/java/com/yapp/ndgl/data/core/model/auth => auth/src/main/java/com/yapp/ndgl/data/auth/model}/CreateUserRequest.kt (84%) rename data/{core/src/main/java/com/yapp/ndgl/data/core/model/auth => auth/src/main/java/com/yapp/ndgl/data/auth/model}/LoginRequest.kt (71%) create mode 100644 data/auth/src/main/java/com/yapp/ndgl/data/auth/token/TokenManagerImpl.kt create mode 100644 data/core/src/main/java/com/yapp/ndgl/data/core/token/TokenManager.kt diff --git a/build-logic/src/main/kotlin/NDGLDataPlugin.kt b/build-logic/src/main/kotlin/NDGLDataPlugin.kt index b7aed8b5..456b875f 100644 --- a/build-logic/src/main/kotlin/NDGLDataPlugin.kt +++ b/build-logic/src/main/kotlin/NDGLDataPlugin.kt @@ -4,11 +4,14 @@ import convention.configureKotlinAndroid import convention.configureTimber import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.kotlin.dsl.dependencies +import util.libs class NDGLDataPlugin : Plugin { override fun apply(target: Project): Unit = with(target) { with(pluginManager) { apply("com.android.library") + apply("org.jetbrains.kotlin.plugin.serialization") } configureKotlinAndroid() @@ -19,5 +22,12 @@ class NDGLDataPlugin : Plugin { if (path != ":data:core") { dependencies.add("implementation", project(":data:core")) } + + dependencies { + "implementation"(libs.findLibrary("retrofit").get()) + "implementation"(libs.findLibrary("retrofit-kotlinx-serialization-json").get()) + "implementation"(libs.findLibrary("kotlinx-serialization-json").get()) + "implementation"(libs.findLibrary("okhttp").get()) + } } } diff --git a/build-logic/src/main/kotlin/NDGLFeaturePlugin.kt b/build-logic/src/main/kotlin/NDGLFeaturePlugin.kt index da84f274..44515d93 100644 --- a/build-logic/src/main/kotlin/NDGLFeaturePlugin.kt +++ b/build-logic/src/main/kotlin/NDGLFeaturePlugin.kt @@ -1,6 +1,4 @@ -import convention.configureComposeAndroid import convention.configureCoroutineAndroid -import convention.configureFirebase import convention.configureHiltAndroid import org.gradle.api.Plugin import org.gradle.api.Project diff --git a/data/auth/build.gradle.kts b/data/auth/build.gradle.kts index fd26b5b8..1465a39c 100644 --- a/data/auth/build.gradle.kts +++ b/data/auth/build.gradle.kts @@ -7,5 +7,5 @@ android { } dependencies { - implementation(project(":data:core")) + implementation(libs.androidx.datastore) } diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/api/AuthApi.kt similarity index 60% rename from data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/api/AuthApi.kt index d4c2ec05..c5746900 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/api/NDGLApi.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/api/AuthApi.kt @@ -1,14 +1,13 @@ -package com.yapp.ndgl.data.core.api +package com.yapp.ndgl.data.auth.api +import com.yapp.ndgl.data.auth.model.AuthResponse +import com.yapp.ndgl.data.auth.model.CreateUserRequest +import com.yapp.ndgl.data.auth.model.LoginRequest import com.yapp.ndgl.data.core.model.BaseResponse -import com.yapp.ndgl.data.core.model.auth.AuthResponse -import com.yapp.ndgl.data.core.model.auth.CreateUserRequest -import com.yapp.ndgl.data.core.model.auth.LoginRequest import retrofit2.http.Body import retrofit2.http.POST -interface NDGLApi { - // Auth +interface AuthApi { @POST("/api/v1/auth/users") suspend fun createUser( @Body request: CreateUserRequest, diff --git a/data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthModule.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthModule.kt new file mode 100644 index 00000000..7d1af3ae --- /dev/null +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthModule.kt @@ -0,0 +1,19 @@ +package com.yapp.ndgl.data.auth.di + +import com.yapp.ndgl.data.auth.token.TokenManagerImpl +import com.yapp.ndgl.data.core.token.TokenManager +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class AuthModule { + @Binds + @Singleton + abstract fun bindTokenManager( + impl: TokenManagerImpl, + ): TokenManager +} diff --git a/data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthNetworkModule.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthNetworkModule.kt new file mode 100644 index 00000000..d23cf8f2 --- /dev/null +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/di/AuthNetworkModule.kt @@ -0,0 +1,34 @@ +package com.yapp.ndgl.data.auth.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.yapp.ndgl.data.auth.api.AuthApi +import com.yapp.ndgl.data.core.adapter.NDGLCallAdapterFactory +import com.yapp.ndgl.data.core.di.AuthClient +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AuthNetworkModule { + @Provides + @Singleton + fun provideAuthApi( + json: Json, + baseUrl: String, + @AuthClient okHttpClient: OkHttpClient, + callAdapterFactory: NDGLCallAdapterFactory, + ): AuthApi = Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) + .addCallAdapterFactory(callAdapterFactory) + .build() + .create(AuthApi::class.java) +} diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/local/LocalAuthDataSource.kt similarity index 93% rename from data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/local/LocalAuthDataSource.kt index 5e66a01f..b57b885c 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/local/datasource/LocalAuthDataSource.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/local/LocalAuthDataSource.kt @@ -1,10 +1,10 @@ -package com.yapp.ndgl.data.core.local.datasource +package com.yapp.ndgl.data.auth.local import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import com.yapp.ndgl.data.core.local.util.handleException +import com.yapp.ndgl.data.auth.local.util.handleException import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/local/di/DataStoreModule.kt similarity index 95% rename from data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/local/di/DataStoreModule.kt index 212c038c..e3283ff9 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/local/di/DataStoreModule.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/local/di/DataStoreModule.kt @@ -1,4 +1,4 @@ -package com.yapp.ndgl.data.core.local.di +package com.yapp.ndgl.data.auth.local.di import android.content.Context import androidx.datastore.core.DataStore diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/local/util/DataStoreUtil.kt similarity index 91% rename from data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/local/util/DataStoreUtil.kt index e9e40f5b..da2fce15 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/local/util/DataStoreUtil.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/local/util/DataStoreUtil.kt @@ -1,4 +1,4 @@ -package com.yapp.ndgl.data.core.local.util +package com.yapp.ndgl.data.auth.local.util import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.emptyPreferences diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/model/AuthResponse.kt similarity index 79% rename from data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/model/AuthResponse.kt index b0be40f9..83909386 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/AuthResponse.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/model/AuthResponse.kt @@ -1,4 +1,4 @@ -package com.yapp.ndgl.data.core.model.auth +package com.yapp.ndgl.data.auth.model import kotlinx.serialization.Serializable diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/model/CreateUserRequest.kt similarity index 84% rename from data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/model/CreateUserRequest.kt index b877eda1..50a8e52a 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/CreateUserRequest.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/model/CreateUserRequest.kt @@ -1,4 +1,4 @@ -package com.yapp.ndgl.data.core.model.auth +package com.yapp.ndgl.data.auth.model import kotlinx.serialization.Serializable diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/model/LoginRequest.kt similarity index 71% rename from data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt rename to data/auth/src/main/java/com/yapp/ndgl/data/auth/model/LoginRequest.kt index a83ff81c..86d07ffb 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/model/auth/LoginRequest.kt +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/model/LoginRequest.kt @@ -1,4 +1,4 @@ -package com.yapp.ndgl.data.core.model.auth +package com.yapp.ndgl.data.auth.model import kotlinx.serialization.Serializable diff --git a/data/auth/src/main/java/com/yapp/ndgl/data/auth/token/TokenManagerImpl.kt b/data/auth/src/main/java/com/yapp/ndgl/data/auth/token/TokenManagerImpl.kt new file mode 100644 index 00000000..faf7056e --- /dev/null +++ b/data/auth/src/main/java/com/yapp/ndgl/data/auth/token/TokenManagerImpl.kt @@ -0,0 +1,43 @@ +package com.yapp.ndgl.data.auth.token + +import com.yapp.ndgl.data.auth.api.AuthApi +import com.yapp.ndgl.data.auth.local.LocalAuthDataSource +import com.yapp.ndgl.data.auth.model.LoginRequest +import com.yapp.ndgl.data.core.model.getData +import com.yapp.ndgl.data.core.token.TokenManager +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TokenManagerImpl @Inject constructor( + private val localAuthDataSource: LocalAuthDataSource, + private val authApi: AuthApi, +) : TokenManager { + + override suspend fun getAccessToken(): String { + return localAuthDataSource.getAccessToken() + } + + override suspend fun getUuid(): String { + return localAuthDataSource.getUuid() + } + + override suspend fun setAccessToken(accessToken: String) { + localAuthDataSource.setAccessToken(accessToken) + } + + override suspend fun setUuid(uuid: String) { + localAuthDataSource.setUuid(uuid) + } + + override suspend fun refreshToken(): String { + val uuid = getUuid() + check(uuid.isNotEmpty()) { "UUID is empty" } + + val response = authApi.login(LoginRequest(uuid)).getData() + setAccessToken(response.accessToken) + setUuid(response.uuid) + + return response.accessToken + } +} diff --git a/data/core/build.gradle.kts b/data/core/build.gradle.kts index 6c93d156..633d9910 100644 --- a/data/core/build.gradle.kts +++ b/data/core/build.gradle.kts @@ -3,7 +3,6 @@ import kotlin.apply plugins { id("ndgl.data") - alias(libs.plugins.kotlin.serialization) } android { @@ -23,10 +22,5 @@ android { } dependencies { - implementation(libs.androidx.datastore) - implementation(libs.retrofit) - implementation(libs.retrofit.kotlinx.serialization.json) - implementation(libs.kotlinx.serialization.json) - implementation(libs.okhttp) implementation(libs.okhttp.logging.interceptor) } diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt index 3c94320d..e0573624 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/authenticator/NDGLAuthenticator.kt @@ -1,9 +1,6 @@ package com.yapp.ndgl.data.core.authenticator -import com.yapp.ndgl.data.core.api.NDGLApi -import com.yapp.ndgl.data.core.local.datasource.LocalAuthDataSource -import com.yapp.ndgl.data.core.model.auth.LoginRequest -import com.yapp.ndgl.data.core.model.getData +import com.yapp.ndgl.data.core.token.TokenManager import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -13,11 +10,9 @@ import okhttp3.Response import okhttp3.Route import timber.log.Timber import javax.inject.Inject -import javax.inject.Provider class NDGLAuthenticator @Inject constructor( - private val localAuthDataSource: LocalAuthDataSource, - private val ndglApi: Provider, + private val tokenManager: TokenManager, ) : Authenticator { private val mutex = Mutex() @@ -28,31 +23,15 @@ class NDGLAuthenticator @Inject constructor( return null } - if (originRequest.url.encodedPath.contains("/api/v1/auth/login")) { - runBlocking { - localAuthDataSource.clearSession() - } - - return null - } - val retryCount = originRequest.header(RETRY_HEADER)?.toIntOrNull() ?: 0 if (retryCount >= MAX_RETRY_COUNT) { return null } - val authResponse = runBlocking { + val newAccessToken = runBlocking { mutex.withLock { try { - val uuid = localAuthDataSource.getUuid() - if (uuid.isNullOrEmpty()) { - return@withLock null - } - - val response = ndglApi.get().login(LoginRequest(uuid)).getData() - localAuthDataSource.setAccessToken(response.accessToken) - localAuthDataSource.setUuid(response.uuid) - response + tokenManager.refreshToken() } catch (e: Exception) { Timber.e(e, "Failed to refresh token") null @@ -62,7 +41,7 @@ class NDGLAuthenticator @Inject constructor( val newRequest = originRequest.newBuilder() .header(RETRY_HEADER, (retryCount + 1).toString()) - .header("Authorization", "Bearer ${authResponse.accessToken}") + .header("Authorization", "Bearer $newAccessToken") .build() return newRequest diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt index 893fc746..0b9eca12 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/di/NetworkModule.kt @@ -1,9 +1,6 @@ package com.yapp.ndgl.data.core.di -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.yapp.ndgl.data.core.BuildConfig -import com.yapp.ndgl.data.core.adapter.NDGLCallAdapterFactory -import com.yapp.ndgl.data.core.api.NDGLApi import com.yapp.ndgl.data.core.authenticator.NDGLAuthenticator import com.yapp.ndgl.data.core.interceptor.NDGLInterceptor import dagger.Module @@ -11,10 +8,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit +import javax.inject.Qualifier import javax.inject.Singleton @Module @@ -26,6 +22,10 @@ object NetworkModule { ignoreUnknownKeys = true } + @Singleton + @Provides + fun provideBaseUrl(): String = BuildConfig.NDGL_BASE_URL + @Singleton @Provides fun provideDefaultOkHttpClient( @@ -45,17 +45,22 @@ object NetworkModule { return builder.build() } + @AuthClient @Singleton @Provides - fun provideNDGLApi( - json: Json, - okHttpClient: OkHttpClient, - callAdapterFactory: NDGLCallAdapterFactory, - ): NDGLApi = Retrofit.Builder() - .client(okHttpClient) - .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) - .addCallAdapterFactory(callAdapterFactory) - .baseUrl(BuildConfig.NDGL_BASE_URL) - .build() - .create(NDGLApi::class.java) + fun provideAuthOkHttpClient(): OkHttpClient { + val builder = OkHttpClient.Builder() + + if (BuildConfig.DEBUG) { + val loggingInterceptor = HttpLoggingInterceptor() + loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY + builder.addInterceptor(loggingInterceptor) + } + + return builder.build() + } } + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthClient diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt index 15f8e3c0..2b988a4e 100644 --- a/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/interceptor/NDGLInterceptor.kt @@ -1,6 +1,6 @@ package com.yapp.ndgl.data.core.interceptor -import com.yapp.ndgl.data.core.local.datasource.LocalAuthDataSource +import com.yapp.ndgl.data.core.token.TokenManager import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.Request @@ -8,7 +8,7 @@ import okhttp3.Response import javax.inject.Inject class NDGLInterceptor @Inject constructor( - private val localAuthDataSource: LocalAuthDataSource, + private val tokenManager: TokenManager, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originRequest = chain.request() @@ -17,7 +17,7 @@ class NDGLInterceptor @Inject constructor( if (isAccessTokenUsed(originRequest)) { requestBuilder.addHeader( "Authorization", - "Bearer ${runBlocking { localAuthDataSource.getAccessToken() }}", + "Bearer ${runBlocking { tokenManager.getAccessToken() }}", ) } diff --git a/data/core/src/main/java/com/yapp/ndgl/data/core/token/TokenManager.kt b/data/core/src/main/java/com/yapp/ndgl/data/core/token/TokenManager.kt new file mode 100644 index 00000000..f25d62cf --- /dev/null +++ b/data/core/src/main/java/com/yapp/ndgl/data/core/token/TokenManager.kt @@ -0,0 +1,9 @@ +package com.yapp.ndgl.data.core.token + +interface TokenManager { + suspend fun getAccessToken(): String + suspend fun getUuid(): String + suspend fun setAccessToken(accessToken: String) + suspend fun setUuid(uuid: String) + suspend fun refreshToken(): String +}