Skip to content

Commit

Permalink
Merge pull request #24 from uswLectureEvaluation/refactor/suwiki-serv…
Browse files Browse the repository at this point in the history
…er-error-handling

Refactor/suwiki server error handling
  • Loading branch information
jinukeu authored Oct 23, 2023
2 parents ecf0def + 13c1de2 commit 243e1b6
Show file tree
Hide file tree
Showing 46 changed files with 636 additions and 162 deletions.
7 changes: 6 additions & 1 deletion app-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("suwiki.android.application")
id("suwiki.android.application.compose")
id("suwiki.android.hilt")
}

android {
Expand Down Expand Up @@ -33,6 +34,7 @@ dependencies {

implementation(projects.domain)
implementation(projects.di)
implementation(projects.domain.openmajor)

implementation(projects.local.openmajor)
implementation(projects.local.timetable.editor)
Expand All @@ -48,5 +50,8 @@ dependencies {
implementation(projects.remote.signup)
implementation(projects.remote.timetable.viewer)
implementation(projects.remote.user)
implementation(libs.junit)

implementation(projects.presentation)

implementation(libs.timber)
}
14 changes: 2 additions & 12 deletions app-compose/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application
android:allowBackup="true"
android:name=".SuwikiApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Uswtimetable">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Uswtimetable">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.kunize.uswtimetable

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber

@HiltAndroidApp
class SuwikiApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.suwiki.core.database

import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RenameTable
import androidx.room.RoomDatabase
Expand All @@ -10,9 +11,9 @@ import com.suwiki.core.database.model.OpenMajorEntity
@Database(
entities = [OpenMajorEntity::class],
version = 1,
// autoMigrations = [
// AutoMigration(from = 1, to = 2, spec = OpenMajorDatabase.RenameTableAutoMigration::class),
// ],
autoMigrations = [
AutoMigration(from = 1, to = 2, spec = OpenMajorDatabase.RenameTableAutoMigration::class),
],
exportSchema = true,
)
abstract class OpenMajorDatabase : RoomDatabase() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ class ForbiddenException(
override val message: String = "제한된 요청이에요.",
) : RuntimeException()

class NotFoundException(
override val message: String = "정보를 찾을 수 없어요.",
) : RuntimeException()

class NetworkException(
override val message: String = "네트워크 연결 상태 또는 수위키 서버에 문제가 있어요.",
) : RuntimeException()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.suwiki.core.model.exception

enum class SuwikiServerError(val exception: Exception) {
USER003(IncorrectEmailFormException()),
USER004(UserNotExistException()),
USER005(PasswordErrorException()),
USER006(UserPointLackException()),
USER007(NeedLoginException()),
USER008(RestrictedUserException()),
USER009(BlackListUserException()),
USER010(LoginIdOrEmailOverlapException()),
USER011(PasswordNotChangedException()),
USER013(UserNotFoundByEmailException()),
USER014(UserAlreadyBlackedListedException()),
USER015(UserIsBlackListedException()),
USER016(UserNotFoundByLoginIdException()),

CONFIRMATION_TOKEN001(EmailNotAuthedException()),
CONFIRMATION_TOKEN002(EmailValidatedException()),
CONFIRMATION_TOKEN003(EmailValidatedErrorRetryException()),
CONFIRMATION_TOKEN004(EmailAuthTokenAlreadyUsedException()),

POSTS001(PostWriteOverlapException()),

EXAM_POST001(ExamPostNotFoundException()),
EXAM_POST002(ExamPostAlreadyPurchaseException()),

EVALUATE_POST001(EvaluatePostNotFoundException()),

TOKEN001(TokenExpiredException()),
TOKEN002(TokenIsBrokenException()),

SECURITY001(UnAuthenticatedException()),
SECURITY003(LoginFailedException()),

MAIL001(SendMailFailedException()),
}

class IncorrectEmailFormException(
override val message: String = "이메일 형식이 잘못되었어요.",
) : RuntimeException()

class UserNotExistException(
override val message: String = "존재하지 않는 사용자에요.",
) : RuntimeException()

class PasswordErrorException(
override val message: String = "비밀번호를 확인해주세요.",
) : RuntimeException()

class UserPointLackException(
override val message: String = "포인트가 부족해요.",
) : RuntimeException()

class NeedLoginException(
override val message: String = "로그인이 필요해요.",
) : RuntimeException()

class RestrictedUserException(
override val message: String = "접근 권한이 없어요.",
) : RuntimeException()

class BlackListUserException(
override val message: String = "블랙리스트 대상이에요. 이용할 수 없어요.",
) : RuntimeException()

class LoginIdOrEmailOverlapException(
override val message: String = "이미 사용중인 아이디 혹은 이메일이에요.",
) : RuntimeException()

class PasswordNotChangedException(
override val message: String = "이전 비밀번호와 동일하게 변경할 수 없어요.",
) : RuntimeException()

class EmailNotAuthedException(
override val message: String = "이메일 인증이 완료되지 않았어요. 교내 포털 사이트에서 이메일 인증을 완료해주세요.",
) : RuntimeException()

class EmailValidatedException(
override val message: String = "이메일 인증에 실패했습니다.",
) : RuntimeException()

class EmailValidatedErrorRetryException(
override val message: String = "이메일 인증 만료기간이 지나거나, 예기치 못한 오류로 이메일 인증에 실패했습니다. 회원가입을 다시 진행해주세요",
) : RuntimeException()

class EmailAuthTokenAlreadyUsedException(
override val message: String = "이미 사용된 인증 토큰 입니다.",
) : RuntimeException()

class PostWriteOverlapException(
override val message: String = "이미 작성한 정보입니다.",
) : RuntimeException()

class ExamPostNotFoundException(
override val message: String = "해당 시험정보를 찾을 수 없습니다.",
) : RuntimeException()

class ExamPostAlreadyPurchaseException(
override val message: String = "이미 구매한 시험정보 입니다.",
) : RuntimeException()

class EvaluatePostNotFoundException(
override val message: String = "해당 강의평가를 찾을 수 없습니다.",
) : RuntimeException()

class UserAlreadyBlackedListedException(
override val message: String = "이미 블랙리스트인 사용자 입니다.",
) : RuntimeException()

class UserIsBlackListedException(
override val message: String = "신고 당한 횟수 3회 누적으로 블랙리스트 조치 되었습니다. 더 이상 서비스를 이용할 수 없습니다.",
) : RuntimeException()

class UserNotFoundByEmailException(
override val message: String = "해당 이메일에 대한 유저를 찾을 수 없습니다.",
) : RuntimeException()

class UserNotFoundByLoginIdException(
override val message: String = "해당 아이디에 대한 유저를 찾을 수 없습니다.",
) : RuntimeException()

class TokenExpiredException(
override val message: String = "토큰이 만료되었습니다 다시 로그인 해주세요",
) : RuntimeException()

class TokenIsBrokenException(
override val message: String = "토큰이 유효하지 않습니다.",
) : RuntimeException()

class UnAuthenticatedException(
override val message: String = "로그인이 필요해요.",
) : RuntimeException()

class LoginFailedException(
override val message: String = "아이디, 비밀번호를 확인해주세요.",
) : RuntimeException()

class SendMailFailedException(
override val message: String = "메일 전송에 실패했어요.",
) : RuntimeException()
20 changes: 20 additions & 0 deletions core/network/src/main/java/com/suwiki/core/network/di/ApiModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.suwiki.core.network.di

import com.suwiki.core.network.api.AuthApi
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {

@Singleton
@Provides
fun provideAuthApi(@NormalRetrofit retrofit: Retrofit): AuthApi {
return retrofit.create(AuthApi::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.suwiki.core.network.retrofit

import com.suwiki.core.model.exception.ForbiddenException
import com.suwiki.core.model.exception.NetworkException
import com.suwiki.core.model.exception.NotFoundException
import com.suwiki.core.model.exception.RequestFailException
import com.suwiki.core.model.exception.SuwikiServerError
import com.suwiki.core.model.exception.UnknownException

sealed interface ApiResult<out T> {
Expand All @@ -13,9 +15,9 @@ sealed interface ApiResult<out T> {
data class NetworkError(val throwable: Throwable) : Failure
data class UnknownApiError(val throwable: Throwable) : Failure

fun safeThrowable(httpErrorHandler: HttpErrorHandler): Throwable =
fun safeThrowable(): Throwable =
when (this) {
is HttpError -> httpErrorHandler.handleHttpError(this)
is HttpError -> handleHttpError(this)
is NetworkError -> throwable
is UnknownApiError -> throwable
}
Expand All @@ -28,15 +30,7 @@ sealed interface ApiResult<out T> {
get() = this is Failure

fun getOrThrow(customHttpErrorHandler: (Failure.HttpError.() -> Exception)? = null): T {
val httpErrorHandler = customHttpErrorHandler?.let {
object : HttpErrorHandler {
override fun handleHttpError(httpError: Failure.HttpError): Exception {
return it(httpError)
}
}
} ?: DefaultHttpErrorHandler

throwFailure(httpErrorHandler)
throwFailure()
return (this as Success).data
}

Expand All @@ -53,7 +47,7 @@ sealed interface ApiResult<out T> {

fun exceptionOrNull(): Throwable? =
when (this) {
is Failure -> safeThrowable(DefaultHttpErrorHandler)
is Failure -> safeThrowable()
else -> null
}

Expand All @@ -80,25 +74,30 @@ internal fun ApiResult<*>.throwOnSuccess() {
if (this is ApiResult.Success) throw IllegalStateException("Cannot be called under Success conditions.")
}

internal fun ApiResult<*>.throwFailure(httpErrorHandler: HttpErrorHandler) {
internal fun ApiResult<*>.throwFailure() {
if (this is ApiResult.Failure) {
throw safeThrowable(httpErrorHandler)
throw safeThrowable()
}
}

interface HttpErrorHandler {
fun handleHttpError(
httpError: ApiResult.Failure.HttpError,
): Exception
private fun handleHttpError(httpError: ApiResult.Failure.HttpError): Exception {
return getSuwikiErrorBody(httpError.body).getOrNull()?.run {
handleSuwikiError(this)
} ?: handleNonSuwikiError(httpError.code)
}

object DefaultHttpErrorHandler : HttpErrorHandler {
override fun handleHttpError(httpError: ApiResult.Failure.HttpError): Exception {
return when (httpError.code) {
400 -> RequestFailException()
403 -> ForbiddenException()
500, 501, 502, 503, 504, 505 -> NetworkException()
else -> UnknownException()
}
}
private fun handleSuwikiError(suwikiErrorResponse: SuwikiErrorResponse): Exception = runCatching {
SuwikiServerError.valueOf(suwikiErrorResponse.suwikiCode).exception
}.getOrNull() ?: handleNonSuwikiError(suwikiErrorResponse.httpStatusCode)

private fun handleNonSuwikiError(httpStatusCode: Int) = when (httpStatusCode) {
400 -> RequestFailException()
403 -> ForbiddenException()
404 -> NotFoundException()
500, 501, 502, 503, 504, 505 -> NetworkException()
else -> UnknownException()
}

private fun getSuwikiErrorBody(body: String) = runCatching {
KotlinSerializationUtil.json.decodeFromString<SuwikiErrorResponse>(body)
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,9 @@ private class ApiResultCall<R>(
callback.onResponse(this@ApiResultCall, Response.success(response.toApiResult()))
}

override fun onFailure(call: Call<R>, throwable: Throwable) {
val error = if (throwable is IOException) {
ApiResult.Failure.NetworkError(throwable)
} else {
ApiResult.Failure.UnknownApiError(throwable)
}
callback.onResponse(this@ApiResultCall, Response.success(error))
}

private fun Response<R>.toApiResult(): ApiResult<R> {
if (!isSuccessful) { // Http 응답 에러
val errorBody = errorBody()?.toString()
val errorBody = errorBody()?.string()
return ApiResult.Failure.HttpError(
code = code(),
message = message(),
Expand All @@ -59,6 +50,15 @@ private class ApiResultCall<R>(
)
}
}

override fun onFailure(call: Call<R>, throwable: Throwable) {
val error = if (throwable is IOException) {
ApiResult.Failure.NetworkError(throwable)
} else {
ApiResult.Failure.UnknownApiError(throwable)
}
callback.onResponse(this@ApiResultCall, Response.success(error))
}
},
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.suwiki.core.network.retrofit

import kotlinx.serialization.json.Json

object KotlinSerializationUtil {
val json = Json { ignoreUnknownKeys = true }
}
Loading

0 comments on commit 243e1b6

Please sign in to comment.