Skip to content
Closed
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
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@
<activity
android:name=".presentation.mypage.terms.WebViewActivity"
android:exported="false" />
<activity
android:name=".presentation.error.ErrorActivity"
android:exported="false" />
<activity
android:name=".presentation.login.IntroActivity"
android:exported="true">
Expand Down
21 changes: 19 additions & 2 deletions app/src/main/java/com/eatssu/android/di/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.eatssu.android.di


import android.content.Context
import com.eatssu.android.BuildConfig
import com.eatssu.android.BuildConfig.BASE_URL
import com.eatssu.android.di.network.NetworkErrorInterceptor
import com.eatssu.android.di.network.TokenAuthenticator
import com.eatssu.android.di.network.TokenInterceptor
import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase
Expand All @@ -13,6 +15,7 @@ import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
Expand Down Expand Up @@ -55,19 +58,22 @@ object NetworkModule {
@Provides
fun provideAuthOkHttpClient(
tokenInterceptor: TokenInterceptor,
tokenAuthenticator: TokenAuthenticator
tokenAuthenticator: TokenAuthenticator,
networkErrorInterceptor: NetworkErrorInterceptor
) = if (BuildConfig.DEBUG) {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

OkHttpClient.Builder()
.addInterceptor(networkErrorInterceptor)
.addInterceptor(loggingInterceptor)
.addInterceptor(tokenInterceptor)
.authenticator(tokenAuthenticator)
.build()
} else {
// 프로덕션 환경에서는 로깅 인터셉터를 추가하지 않음
OkHttpClient.Builder()
.addInterceptor(networkErrorInterceptor)
.addInterceptor(tokenInterceptor)
.authenticator(tokenAuthenticator)
.build()
Comment on lines 59 to 79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

현재 provideAuthOkHttpClient 함수는 BuildConfig.DEBUG 값에 따라 두 개의 거의 동일한 OkHttpClient.Builder 블록을 가지고 있습니다. 로깅 인터셉터 추가 여부만 다른데, 이로 인해 코드 중복이 발생합니다. 아래와 같이 리팩토링하여 중복을 제거하고 가독성을 높일 수 있습니다.

    fun provideAuthOkHttpClient(
        tokenInterceptor: TokenInterceptor,
        tokenAuthenticator: TokenAuthenticator,
        networkErrorInterceptor: NetworkErrorInterceptor
    ): OkHttpClient {
        val builder = OkHttpClient.Builder()
            .addInterceptor(networkErrorInterceptor)
            .addInterceptor(tokenInterceptor)
            .authenticator(tokenAuthenticator)

        if (BuildConfig.DEBUG) {
            val loggingInterceptor = HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            }
            builder.addInterceptor(loggingInterceptor)
        }

        return builder.build()
    }

Expand All @@ -77,8 +83,11 @@ object NetworkModule {
@Singleton
@Provides
@NoToken
fun provideNoAuthOkHttpClient(): OkHttpClient {
fun provideNoAuthOkHttpClient(
networkErrorInterceptor: NetworkErrorInterceptor
): OkHttpClient {
val builder = OkHttpClient.Builder()
.addInterceptor(networkErrorInterceptor)
if (BuildConfig.DEBUG) {
builder.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
Expand Down Expand Up @@ -108,6 +117,14 @@ object NetworkModule {
.build()
}

@Provides
@Singleton
fun provideNetworkErrorInterceptor(
@ApplicationContext context: Context
): NetworkErrorInterceptor {
return NetworkErrorInterceptor(context)
}

@Provides
@Singleton
fun provideTokenAuthenticator(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.eatssu.android.di.network

import android.content.Context
import android.content.Intent
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.presentation.error.ErrorActivity
import com.google.gson.Gson
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.IOException
import javax.inject.Inject


/**
* 네트워크 오류를 처리하는 인터셉터
* IOException(SocketTimeoutException, UnknownHostException) 발생 시 AlertDialog를 띄우는 ErrorActivity로 이동
*/
class NetworkErrorInterceptor @Inject constructor(
private val context: Context,
) : Interceptor {

companion object {
private val gson = Gson()
}

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()

try {
return chain.proceed(request)
} catch (e: IOException) {
val intent = Intent(context, ErrorActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Intent.FLAG_ACTIVITY_CLEAR_TASK 플래그는 현재 액티비티 스택을 모두 지우고 새로운 태스크를 시작합니다. 일시적인 네트워크 오류(예: 터널 통과)가 발생했을 때 사용자가 보고 있던 화면이 모두 사라지고 오류 화면만 남게 되어 사용자 경험을 크게 해칠 수 있습니다. 이 플래그를 제거하여 사용자가 오류 확인 후 이전 화면으로 돌아갈 수 있도록 하는 것이 좋습니다. FLAG_ACTIVITY_NEW_TASK만으로도 비-액티비티 컨텍스트에서 액티비티를 시작하는 데 충분합니다.

Suggested change
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

putExtra("message", "서버 통신에 실패했습니다. 잠시 후 다시 시도해 주세요.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

에러 메시지와 인텐트 extra의 키("message")가 하드코딩되어 있습니다. 유지보수성과 다국어 지원을 위해 문자열은 strings.xml 리소스에 정의하고, 인텐트 키는 ErrorActivity의 companion object에 상수로 정의하여 사용하는 것을 권장합니다.

예시:

// ErrorActivity.kt
companion object {
    const val EXTRA_MESSAGE = "error_message"
}

// strings.xml
<string name="network_error">서버 통신에 실패했습니다. 잠시 후 다시 시도해 주세요.</string>

// NetworkErrorInterceptor.kt
putExtra(ErrorActivity.EXTRA_MESSAGE, context.getString(R.string.network_error))

}
context.startActivity(intent)

val baseResponse = BaseResponse<Void>(
isSuccess = false,
code = 500, // 서버 처리 오류인지 통신 불가인지 구분
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

네트워크 오류를 나타내기 위해 code500으로 하드코딩하고 있습니다. 이 값은 IntroViewModel에서도 사용되므로, 두 파일 간의 의존성이 생깁니다. 이 값을 NetworkErrorInterceptorcompanion object에 상수로 정의하여 여러 곳에서 참조하도록 하면 코드의 일관성과 유지보수성을 높일 수 있습니다.

예시:

// NetworkErrorInterceptor.kt
companion object {
    const val NETWORK_ERROR_CODE = 500
    private val gson = Gson()
}

// IntroViewModel.kt
... else if (it.code != NetworkErrorInterceptor.NETWORK_ERROR_CODE) { ...

message = "서버 통신 실패",
)
val json = gson.toJson(baseResponse)
val responseBody = json.toResponseBody("application/json".toMediaTypeOrNull())

return Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(200) // HTTP 응답 코드는 200으로 해야 Retrofit에서 에러로 처리하지 않음
.message("서버 통신 실패")
.body(responseBody)
.build()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.eatssu.android.presentation.error

import android.app.AlertDialog
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.eatssu.android.databinding.ActivityErrorBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class ErrorActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityErrorBinding.inflate(layoutInflater)
setContentView(binding.root)

showDialog()
}

private fun showDialog() {
val message = intent.getStringExtra("message") ?: "알 수 없는 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."

AlertDialog.Builder(this)
.setTitle("알림")
.setMessage(message)
.setCancelable(false)
.setPositiveButton("확인") { _, _ -> finish() }
.setOnDismissListener { finish() }
.show()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class IntroViewModel @Inject constructor(
.collect {
if (it.result == true) { //토큰이 있고 유효함
_uiState.value = UiState.Success(IntroState.ValidToken)
} else { //토큰이 있어도 유효하지 않음
} else if (it.code != 500) { // 토큰이 있어도 유효하지 않음
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

매직 넘버 500을 사용하여 네트워크 오류 케이스를 확인하고 있습니다. 이 값은 NetworkErrorInterceptor에서 설정한 값과 동일해야 합니다. 이러한 값은 한 곳(예: NetworkErrorInterceptorcompanion object)에 상수로 정의하고 여러 곳에서 참조하도록 하여, 코드의 가독성을 높이고 잠재적인 버그를 방지하는 것이 좋습니다.

_uiState.value = UiState.Error
_uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다"))
}
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/res/layout/activity_error.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primary"
tools:context=".presentation.error.ErrorActivity">

<ImageView
android:layout_width="250dp"
android:layout_height="wrap_content"
android:scaleType="centerInside"
android:layout_margin="65dp"
android:src="@drawable/img_new_logo_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>