Skip to content

Commit

Permalink
Merge pull request #35 from YAPP-Github/feature/TNT-164
Browse files Browse the repository at this point in the history
[TNT-164] 네트워크 기본 세팅
  • Loading branch information
hoyahozz authored Jan 26, 2025
2 parents 58d69c1 + dc072c1 commit a6c1bb5
Show file tree
Hide file tree
Showing 22 changed files with 283 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/android-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ jobs:
distribution: zulu
cache: gradle

- name: add local.properties
run: |
echo RELEASE_BASE_API_URL=\"${{ secrets.RELEASE_BASE_API_URL }}\" >> ./local.properties
echo DEBUG_BASE_API_URL=\"${{ secrets.DEBUG_BASE_API_URL }}\" >> ./local.properties
- name: Grant execute permission for gradlew
run: chmod +x gradlew

Expand Down
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation(projects.data.network)
implementation(projects.data.storage)
implementation(projects.data.repository)
implementation(projects.data.session)

implementation(libs.androidx.activity.compose)
}
19 changes: 19 additions & 0 deletions data/network/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import co.kr.tnt.setNamespace
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties

plugins {
id("tnt.android.library")
Expand All @@ -12,6 +13,24 @@ android {
buildFeatures {
buildConfig = true
}

buildTypes {
debug {
buildConfigField(
"String",
"BASE_API_URL",
gradleLocalProperties(rootDir, providers).getProperty("DEBUG_BASE_API_URL"),
)
}

release {
buildConfigField(
"String",
"BASE_API_URL",
gradleLocalProperties(rootDir, providers).getProperty("RELEASE_BASE_API_URL"),
)
}
}
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package co.kr.data.network.authenticator

import co.kr.data.network.monitor.NetworkSessionMonitor
import okhttp3.Authenticator
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import javax.inject.Inject

// 구현체 주입 여부 재검토 필요.
internal class Authenticator @Inject constructor(
private val networkSessionMonitor: NetworkSessionMonitor,
) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
networkSessionMonitor.sendExpired()
return null
}
}
17 changes: 17 additions & 0 deletions data/network/src/main/java/co/kr/data/network/di/MonitorModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package co.kr.data.network.di

import co.kr.data.network.monitor.NetworkSessionMonitor
import co.kr.tnt.domain.monitor.SessionMonitor
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
abstract class MonitorModule {
@Binds
abstract fun bindsSessionMonitor(
monitor: NetworkSessionMonitor,
): SessionMonitor
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package co.kr.data.network.di

import co.kr.data.network.authenticator.Authenticator
import co.kr.data.network.interceptor.SessionInterceptor
import co.kr.data.network.service.TnTService
import co.kr.tnt.data.network.BuildConfig
import dagger.Module
Expand Down Expand Up @@ -28,7 +30,7 @@ internal object NetworkModule {
converterFactory: Converter.Factory,
): TnTService {
return Retrofit.Builder()
.baseUrl("https://TODO") // TODO
.baseUrl(BuildConfig.BASE_API_URL)
.addConverterFactory(converterFactory)
.client(okHttpClient).build()
.create(TnTService::class.java)
Expand All @@ -37,11 +39,15 @@ internal object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(
sessionInterceptor: SessionInterceptor,
loggingInterceptor: HttpLoggingInterceptor,
authenticator: Authenticator,
): OkHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS)
.readTimeout(TIME_OUT_SECONDS, TimeUnit.SECONDS)
.addInterceptor(sessionInterceptor)
.addInterceptor(loggingInterceptor)
.authenticator(authenticator)
.build()

@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package co.kr.data.network.interceptor

import co.kr.data.network.provider.SessionProvider
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject

internal class SessionInterceptor @Inject constructor(
private val sessionProvider: SessionProvider,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originRequest = chain.request()
val requestBuilder = originRequest.newBuilder()

requestBuilder.addHeader(
"AUTHORIZATION",
"SESSION-ID ${runBlocking { sessionProvider.getSessionId() }}",
)

return chain.proceed(requestBuilder.build())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.kr.data.network.model.base

import kotlinx.serialization.Serializable

@Serializable
data class BaseErrorResponse(
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package co.kr.data.network.model.exception

sealed class NetworkException(override val message: String) : Exception(message) {
data class BadRequestException(
override val message: String = "Bad request",
) : NetworkException(message)

data class UnauthorizedException(
override val message: String = "Unauthorized",
) : NetworkException(message)

data class ForbiddenException(
override val message: String = "Forbidden",
) : NetworkException(message)

data class NotFoundException(
override val message: String = "Not Found",
) : NetworkException(message)

data class ConflictException(
override val message: String = "Conflict",
) : NetworkException(message)

data class ServerException(
override val message: String = "Server Error",
) : NetworkException(message)

data class UnknownException(
override val message: String = "Unknown Error",
) : NetworkException(message)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.kr.data.network.monitor

import co.kr.tnt.domain.monitor.SessionMonitor
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class NetworkSessionMonitor @Inject constructor() : SessionMonitor {
private val _onExpired = Channel<Unit>(
capacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val onExpired: Flow<Unit>
get() = _onExpired.receiveAsFlow()

fun sendExpired() = _onExpired.trySend(Unit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package co.kr.data.network.provider

interface SessionProvider {
suspend fun getSessionId(): String
suspend fun setSessionId(sessionId: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package co.kr.data.network.util

import co.kr.data.network.model.base.BaseErrorResponse
import co.kr.data.network.model.exception.NetworkException
import kotlinx.serialization.json.Json
import retrofit2.HttpException
import retrofit2.Response

internal suspend inline fun <T> networkHandler(
crossinline call: suspend () -> T,
): T = try {
call()
} catch (httpException: HttpException) {
val errorResponse = parseErrorResponse(httpException.response())
val message = errorResponse.message

throw when (httpException.code()) {
400 -> NetworkException.BadRequestException(message)
401 -> NetworkException.UnauthorizedException(message)
403 -> NetworkException.ForbiddenException(message)
404 -> NetworkException.NotFoundException(message)
409 -> NetworkException.ConflictException(message)
in 500 until 600 -> NetworkException.ServerException(message)
else -> NetworkException.UnknownException(message)
}
}

private fun parseErrorResponse(response: Response<*>?): BaseErrorResponse {
return try {
requireNotNull(
response?.errorBody()?.string()?.let {
Json.decodeFromString<BaseErrorResponse>(it)
},
)
} catch (e: Exception) {
throw NetworkException.UnknownException("Failed to parse error message,\n${e.message}")
}
}
1 change: 1 addition & 0 deletions data/session/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
15 changes: 15 additions & 0 deletions data/session/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import co.kr.tnt.setNamespace

plugins {
id("tnt.android.library")
id("tnt.android.hilt")
}

android {
setNamespace("data.session")
}

dependencies {
implementation(projects.data.network)
implementation(projects.data.storage)
}
2 changes: 2 additions & 0 deletions data/session/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package co.kr.data.session

import co.kr.data.network.provider.SessionProvider
import co.kr.data.storage.source.SessionDataSource
import kotlinx.coroutines.flow.firstOrNull
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class SessionProviderImpl @Inject constructor(
private val sessionDataSource: SessionDataSource,
) : SessionProvider {
override suspend fun getSessionId(): String =
sessionDataSource.sessionId.firstOrNull() ?: ""

override suspend fun setSessionId(sessionId: String) =
sessionDataSource.updateSessionId(sessionId)
}
17 changes: 17 additions & 0 deletions data/session/src/main/java/co/kr/data/session/di/SessionModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package co.kr.data.session.di

import co.kr.data.network.provider.SessionProvider
import co.kr.data.session.SessionProviderImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@InstallIn(SingletonComponent::class)
@Module
internal abstract class SessionModule {
@Binds
abstract fun bindsSessionProvider(
provider: SessionProviderImpl,
): SessionProvider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.kr.tnt.domain.monitor

import kotlinx.coroutines.flow.Flow

interface SessionMonitor {
val onExpired: Flow<Unit>
}
7 changes: 6 additions & 1 deletion feature/main/src/main/java/co/kr/tnt/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.domain.monitor.SessionMonitor
import co.kr.tnt.main.ui.TnTApp
import co.kr.tnt.main.ui.rememberTnTAppState
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var sessionMonitor: SessionMonitor

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
val appState = rememberTnTAppState()
val appState = rememberTnTAppState(sessionMonitor = sessionMonitor)

TnTTheme {
TnTApp(appState)
Expand Down
13 changes: 13 additions & 0 deletions feature/main/src/main/java/co/kr/tnt/main/ui/TnTApp.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
package co.kr.tnt.main.ui

import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.flow.collectLatest

@Composable
fun TnTApp(
appState: TnTAppState,
) {
val context = LocalContext.current

LaunchedEffect(appState.sessionMonitor.onExpired) {
appState.sessionMonitor.onExpired.collectLatest {
// TODO navigate to login dialog
Toast.makeText(context, "세션이 만료되었습니다.", Toast.LENGTH_SHORT).show()
}
}

TnTNavHost(
appState = appState,
)
Expand Down
10 changes: 9 additions & 1 deletion feature/main/src/main/java/co/kr/tnt/main/ui/TnTAppState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@ import androidx.navigation.NavDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import co.kr.tnt.domain.monitor.SessionMonitor
import co.kr.tnt.navigation.Route

@Composable
fun rememberTnTAppState(
sessionMonitor: SessionMonitor,
navController: NavHostController = rememberNavController(),
): TnTAppState {
return remember { TnTAppState(navController) }
return remember {
TnTAppState(
sessionMonitor,
navController,
)
}
}

@Stable
@Suppress("UnusedPrivateProperty")
class TnTAppState(
val sessionMonitor: SessionMonitor,
val navController: NavHostController,
) {
private val currentDestination: NavDestination?
Expand Down
Loading

0 comments on commit a6c1bb5

Please sign in to comment.