Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
9096ecc
feat: ApiResult 추가
PeraSite Oct 9, 2025
a68d0bd
refactor: Repository 시그니처 정리
PeraSite Oct 9, 2025
9e2a51d
feat: RepositoryImpl ApiResult 사용
PeraSite Oct 9, 2025
05159c6
feat: Service ApiResult 사용
PeraSite Oct 9, 2025
1877369
feat: Presentation 레이어 ApiResult 적용
PeraSite Oct 9, 2025
a3362fe
feat: ApiResult 처리 CallAdapter 추가
PeraSite Oct 9, 2025
c09b185
fix: getFixMenu만 크래시가 발생하던 문제 수정
PeraSite Oct 9, 2025
63f4a0e
refactor: ArrayList를 List로 정리
PeraSite Oct 9, 2025
6cc8dec
chore: BaseResponseConverter 삭제
PeraSite Oct 9, 2025
dd60769
fix: 런타임 발생 오류 해결
PeraSite Oct 9, 2025
f587b8c
chore: 로그인 실패 줄바꿈 삭제
PeraSite Oct 9, 2025
d140199
fix: early return 추가
PeraSite Oct 10, 2025
ab61862
refactor: 로그아웃 로직 병합
PeraSite Oct 10, 2025
886200a
chore: List로 타입 통일화
PeraSite Oct 10, 2025
019859d
feat: NetworkError 대응 Activity 추가
PeraSite Oct 10, 2025
8281771
feat: HealthCheck 모듈 추가
PeraSite Oct 10, 2025
7701831
fix: ApiResult의 함수를 외부 확장 함수로 변경
PeraSite Oct 10, 2025
d09d4b7
feat: NetworkError 발생 시 ServerErrorActivity 이동 추가
PeraSite Oct 10, 2025
b77cbf5
refactor: IntroActivity 함수화
PeraSite Oct 10, 2025
2c91d5c
refactor: HealthCheck use-case가 boolean을 사용하게 수정, autoLogin 로직 리팩토링
PeraSite Oct 10, 2025
7da9ca8
refactor: nested it 이름 변경
PeraSite Oct 14, 2025
6412fd5
fix: /actuator/health 가 BaseResponse 형태를 따르지 않아 발생하던 문제 수정
PeraSite Oct 15, 2025
a16114d
docs: ApiResult의 각 data class 주석 추가
PeraSite Oct 15, 2025
31e0c99
docs: ApiResult 매핑 함수 주석 추가
PeraSite Oct 15, 2025
e58a3a9
chore: 기존 PartnershipRepositoryImpl 주석 복구
PeraSite Oct 15, 2025
f64d784
refactor: ApiResult.map 외부 확장 함수로 분리
PeraSite Oct 15, 2025
ae15754
refactor: true 반환 one-liner로 변경
PeraSite Oct 15, 2025
b221c36
refactor: getUserNickName ApiResult 매핑 형태 개선
PeraSite Oct 15, 2025
04055fc
refactor: 불필요한 orNull 및 emptyList() 처리 개선
PeraSite Oct 15, 2025
cb24faf
refactor: ApiResult가 UI 레이어로 넘어가지 않게 loginWithKakao 수정
PeraSite Oct 17, 2025
855d229
refactor: 미사용 Response 객체 삭제
PeraSite Oct 17, 2025
976744f
refactor: 하드 코딩된 부분을 Restraunt enum 사용하게 수정
PeraSite Oct 17, 2025
fb4b49c
refactor: 안쓰는 함수 제거
PeraSite Oct 17, 2025
55cadf0
refactor: 어떤 식당 메뉴 가져올 것인지 한번에 호출하게 변경
PeraSite Oct 17, 2025
1689f2e
refactor: 불필요한 UiState.Success의 nullable 제거
PeraSite Oct 17, 2025
f214504
fix: UiState.Success 변경 대응
PeraSite Oct 17, 2025
0ec4086
feat: Health Check 관련 코드 삭제
PeraSite Oct 17, 2025
f61979e
feat: NetworkErrorEventBus로 네트워크 오류 발생 시 ServerErrorActivity로 이동
PeraSite Oct 17, 2025
1d40b3c
refactor: Indent 적게 수정
PeraSite Oct 17, 2025
d08ea51
refactor: Deprecate된 arguments.getSerializable 개선
PeraSite Oct 17, 2025
bc9208d
chore: 불필요한 out 삭제
PeraSite Oct 17, 2025
60ffc27
chore: 불필요한 context 삭제
PeraSite Oct 17, 2025
d12a307
fix: Deprecate된 fragmentManager 접근 수정
PeraSite Oct 17, 2025
be68944
docs: 누락된 주석 추가
PeraSite Oct 20, 2025
1bdf871
refactor: BaseActivity, IntroActivity에서 공통적으로 NetworkErrorEventBus에 구…
PeraSite Oct 20, 2025
73c652a
Merge branch 'develop' into refactor/error-handling
PeraSite Oct 20, 2025
dd79dde
feat: HealthCheck 관련 Service, Repository, UseCase 추가
PeraSite Oct 20, 2025
4503516
feat: IntroViewModel에서 최초 실행 시 health check하게끔 수정
PeraSite Oct 20, 2025
265a86b
refactor: MenuViewModel 클린 아키텍처 적용
PeraSite Oct 20, 2025
3c525bd
chore: 오타 수정
PeraSite Oct 20, 2025
f206d82
refactor: MapViewModel runCatching 리팩토링
PeraSite Oct 21, 2025
fcb7647
refactor: UserInfoViewModel try-catch 리팩토링
PeraSite Oct 21, 2025
2c86c4f
refactor: MyPageViewModel runCatching 리팩토링
PeraSite Oct 21, 2025
7397c53
refactor: SignOutViewModel try-catch 리팩토링
PeraSite Oct 21, 2025
9ff0cb8
refactor: MainViewModel runCatching 리팩토링
PeraSite Oct 21, 2025
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 @@ -186,6 +186,9 @@
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".presentation.error.ServerErrorActivity"
android:exported="false" />
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ data class GetMealResponse(
@SerializedName("mealId") var mealId: Long? = null,
@SerializedName("price") var price: Int? = null,
@SerializedName("rating") var rating: Double? = null,
@SerializedName("briefMenus") var briefMenus: ArrayList<MenusInformationList> = arrayListOf(),
@SerializedName("briefMenus") var briefMenus: List<MenusInformationList> = emptyList(),
)

data class MenusInformationList(
Expand All @@ -18,7 +18,7 @@ data class MenusInformationList(

)

fun ArrayList<GetMealResponse>.mapTodayMenuResponseToMenu(): List<Menu> {
fun List<GetMealResponse>.mapTodayMenuResponseToMenu(): List<Menu> {
val menuList = mutableListOf<Menu>()

this.forEach { mealResponse ->
Expand All @@ -37,7 +37,7 @@ fun ArrayList<GetMealResponse>.mapTodayMenuResponseToMenu(): List<Menu> {
}


fun ArrayList<GetMealResponse>.toDomain(): List<List<String>> {
fun List<GetMealResponse>.toDomain(): List<List<String>> {
return this.map { meal ->
meal.briefMenus.mapNotNull { it.name }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import timber.log.Timber

data class GetFixedMenuResponse(

@SerializedName("categoryMenuListCollection") var categoryMenuListCollection: ArrayList<CategoryMenuListCollection> = arrayListOf(),
@SerializedName("categoryMenuListCollection") var categoryMenuListCollection: List<CategoryMenuListCollection> = emptyList(),

)

data class CategoryMenuListCollection(

@SerializedName("category") var category: String? = null,
@SerializedName("menus") var menus: ArrayList<MenuInformationList> = arrayListOf(),
@SerializedName("menus") var menus: List<MenuInformationList> = emptyList(),

)

Expand Down
58 changes: 58 additions & 0 deletions app/src/main/java/com/eatssu/android/data/model/ApiResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.eatssu.android.data.model

import com.eatssu.android.data.model.ApiResult.Failure
import com.eatssu.android.data.model.ApiResult.NetworkError
import com.eatssu.android.data.model.ApiResult.Success
import com.eatssu.android.data.model.ApiResult.UnknownError
import java.io.IOException

sealed interface ApiResult<out T> {
// API가 성공했을 때
data class Success<T>(val data: T) : ApiResult<T>

// 서버에서 에러 반환
data class Failure(
val responseCode: Int,
val message: String?
) : ApiResult<Nothing>

// 소켓 연결 끊김, 타임아웃 등 네트워크 예외인 IOException 처리
data class NetworkError(
val exception: IOException
) : ApiResult<Nothing>

// IOException을 제외한 예외 처리
data class UnknownError(
val exception: Throwable
) : ApiResult<Nothing>
}

// ApiResult가 성공인지 아닌지 여부 확인
fun ApiResult<Unit>.isSuccess(): Boolean = this is Success
Copy link
Member

Choose a reason for hiding this comment

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

요게.. 살짝 위험해보입니다
계속해서 개발자가 바뀌는 저희 팀 특성상 일관성을 유지하는것이 좀 어려운 과제라고 생각해요

저희 팀은 base response를 쓰고 있죠?
응답 코드도 두개고, isSuccess도 두개에요

근데 간혹가다가, 200 속에 400을 보내는 개발자가 있을 수 있습니다

그래서 살짝 위험한 코드가 아닌지..! 조심스레 의견을 남깁니다

image

Copy link
Member Author

@PeraSite PeraSite Oct 17, 2025

Choose a reason for hiding this comment

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

ㅋㅋㅋㅋ 올려주신 짤처럼 저도 HTTP Response Code만 신뢰할 수 없으니, BaseResponse라는 래퍼 정보를 기반으로 오류 처리하려고 고민을 많이 했어요.
그래서 ApiResultCall.kt의 toApiResult 함수를 보시면

  1. HTTP Response Code가 2xx, 3xx대면 Fail
  2. BaseResponse 형태가 아니면 Fail
  3. BaseResponse.isSuccess가 false면 Fail
  4. BaseResponse.data가 null이면 Fail

이렇게 BaseResponse까지 검증해서 처리하고 있어요!
그래서 ApiResult is Success라면 HTTP 반환 값뿐만 아니라 백엔드에서도 정말 진짜 성공한거가 맞습니다!


// 성공한 경우 데이터 반환, 실패한 경우 빈 리스트 반환
fun <TElement, TList : List<TElement>> ApiResult<TList>.orEmptyList(): List<TElement> =
when (this) {
is Success -> data
else -> emptyList()
}

// 성공한 경우 데이터 반환, 실패한 경우 기본값 반환
fun <T> ApiResult<T>.orElse(default: T): T = when (this) {
is Success -> data
else -> default
}

// 성공한 경우 데이터 반환, 실패한 경우 null 반환
fun <T> ApiResult<T>.orNull(): T? = when (this) {
is Success -> data
else -> null
}

// ApiResult가 Success일 때 데이터를 변환
fun <T, R> ApiResult<T>.map(transform: (T) -> R): ApiResult<R> = when (this) {
is Success<T> -> Success(transform(data))
is Failure -> Failure(responseCode, message)
is NetworkError -> NetworkError(exception)
is UnknownError -> UnknownError(exception)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.eatssu.android.data.repository

import com.eatssu.android.data.model.isSuccess
import com.eatssu.android.data.service.HealthCheckService
import com.eatssu.android.domain.repository.HealthCheckRepository
import javax.inject.Inject

class HealthCheckRepositoryImpl @Inject constructor(
private val healthCheckService: HealthCheckService
) : HealthCheckRepository {
override suspend fun checkHealth(): Boolean {
return healthCheckService.checkHealth().isSuccess()
}
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.dto.response.MenuOfMealResponse
import com.eatssu.android.data.dto.response.MenusInformation
import com.eatssu.android.data.dto.response.mapTodayMenuResponseToMenu
import com.eatssu.android.data.dto.response.toDomain
import com.eatssu.android.data.model.map
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.service.MealService
import com.eatssu.android.domain.model.Menu
import com.eatssu.android.domain.repository.MealRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import com.eatssu.common.enums.Restaurant
import com.eatssu.common.enums.Time
import javax.inject.Inject

class MealRepositoryImpl @Inject constructor(
private val mealService: MealService,
) : MealRepository {

override suspend fun getTodayMeal( //todo 분기처리 어떻게 할지?
override suspend fun getTodayMeal(
date: String,
restaurant: String,
time: String
): Flow<List<List<String>>> {
return flow {
try {
val response = mealService.getTodayMeal2(date, restaurant, time)
): List<List<String>> {
return mealService.getTodayMeal(date, restaurant, time).orEmptyList().toDomain()
}

// 응답이 성공적이라면 Result.success()로 감싸서 Flow로 반환
if (response.isSuccess == true) {
response.result?.let { emit(it.toDomain()) } // 성공시 데이터를 반환
} else {
// 실패한 경우에는 Result.failure()로 실패 정보 반환
emit(emptyList())
}
} catch (e: Exception) {
// 네트워크 오류 또는 예외가 발생한 경우에는 Result.failure()로 반환
// emit(ApiResult.Failure(e))
}
}
override suspend fun getTodayMenuList(
date: String,
restaurant: Restaurant,
time: Time
): List<Menu> {
return mealService.getTodayMeal(date, restaurant.toString(), time.toString())
.map { it.mapTodayMenuResponseToMenu() }
.orEmptyList()
}

override suspend fun getMenuInfoByMealId(mealId: Long): Flow<BaseResponse<MenuOfMealResponse>> =
flow {
emit(mealService.getMenuInfoByMealId(mealId))
}
override suspend fun getMenuInfoByMealId(mealId: Long): List<MenusInformation> =
mealService.getMenuInfoByMealId(mealId).map { it.briefMenus }.orEmptyList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.response.mapFixedMenuResponseToMenu
import com.eatssu.android.data.model.map
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.service.MenuService
import com.eatssu.android.domain.model.Menu
import com.eatssu.android.domain.repository.MenuRepository
import com.eatssu.common.enums.Restaurant
import javax.inject.Inject

class MenuRepositoryImpl @Inject constructor(
private val menuService: MenuService
) : MenuRepository {
override suspend fun getFixedMenuList(restaurant: Restaurant): List<Menu> {
return menuService.getFixMenu(restaurant.toString())
.map { it.mapFixedMenuResponseToMenu() }
.orEmptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ package com.eatssu.android.data.repository
import com.eatssu.android.data.dto.request.CheckValidTokenRequest
import com.eatssu.android.data.dto.request.LoginWithKakaoRequest
import com.eatssu.android.data.dto.response.toDomain
import com.eatssu.android.data.model.map
import com.eatssu.android.data.model.orElse
import com.eatssu.android.data.model.orNull
import com.eatssu.android.data.service.OauthService
import com.eatssu.android.domain.model.Token
import com.eatssu.android.domain.repository.OauthRepository
import javax.inject.Inject

class OauthRepositoryImpl @Inject constructor(private val oauthService: OauthService) :
OauthRepository {
override suspend fun reissueToken(refreshToken: String): Token =
oauthService.getNewToken(refreshToken).result?.toDomain()
?: throw IllegalStateException("Failed to get a new token.")
override suspend fun reissueToken(refreshToken: String): Token? =
oauthService.getNewToken(refreshToken).map { it.toDomain() }.orNull()

override suspend fun login(body: LoginWithKakaoRequest): Token =
oauthService.loginWithKakao(body).result?.toDomain()
?: throw IllegalStateException("Failed to login.")
override suspend fun login(body: LoginWithKakaoRequest): Token? =
oauthService.loginWithKakao(body).map { it.toDomain() }.orNull()

override suspend fun checkValidToken(body: CheckValidTokenRequest): Boolean =
oauthService.checkValidToken(body).result ?: false
oauthService.checkValidToken(body).orElse(false)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.response.toDomain
import com.eatssu.android.data.model.map
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.model.orNull
import com.eatssu.android.data.service.PartnershipService
import com.eatssu.android.data.service.UserService
import com.eatssu.android.domain.model.Partnership
Expand All @@ -14,23 +17,17 @@ class PartnershipRepositoryImpl @Inject constructor(
) : PartnershipRepository {

// 유저의 학과 상관없이 모든 제휴 정보 조회
override suspend fun getAllPartnerships(): List<Partnership> {
return partnershipService.getAllPartnerships()
.result
?.map { it.toDomain() } ?: emptyList()
}
override suspend fun getAllPartnerships(): List<Partnership> =
partnershipService.getAllPartnerships()
.map { list -> list.map { it.toDomain() } }
.orEmptyList()

// 특정 식당 클릭 시 제휴 정보 조회
override suspend fun getPartnershipById(partnershipId: Int): PartnershipRestaurant? {
return partnershipService.getPartnershipById(partnershipId)
.result
?.toDomain()
}
override suspend fun getPartnershipById(partnershipId: Int): PartnershipRestaurant? =
partnershipService.getPartnershipById(partnershipId).map { it.toDomain() }.orNull()

// 유저의 학과에 해당하는 제휴 정보 조회
override suspend fun getUserCollegePartnerships(): List<Partnership> {
return userService.getUserDepartmentPartnerships()
.result
?.map { it.toDomain() } ?: emptyList()
}
override suspend fun getUserCollegePartnerships(): List<Partnership> =
userService.getUserDepartmentPartnerships().map { list -> list.map { it.toDomain() } }
.orEmptyList()
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.request.ReportRequest
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.model.isSuccess
import com.eatssu.android.data.service.ReportService
import com.eatssu.android.domain.repository.ReportRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

class ReportRepositoryImpl @Inject constructor(private val reportService: ReportService) :
ReportRepository {

override suspend fun reportReview(body: ReportRequest): Flow<BaseResponse<Void>> =
flow {
emit(reportService.reportReview(body))
}
override suspend fun reportReview(body: ReportRequest): Boolean =
reportService.reportReview(body).isSuccess()

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.request.ModifyReviewRequest
import com.eatssu.android.data.dto.request.WriteReviewRequest
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.dto.response.GetMealReviewInfoResponse
import com.eatssu.android.data.dto.response.GetMenuReviewInfoResponse
import com.eatssu.android.data.dto.response.GetReviewListResponse
import com.eatssu.android.data.dto.response.ImageResponse
import com.eatssu.android.data.dto.response.toReviewList
import com.eatssu.android.data.model.isSuccess
import com.eatssu.android.data.model.map
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.model.orNull
import com.eatssu.android.data.service.ReviewService
import com.eatssu.android.domain.model.Review
import com.eatssu.android.domain.repository.ReviewRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
Expand All @@ -23,46 +25,37 @@ class ReviewRepositoryImpl @Inject constructor(private val reviewService: Review
override suspend fun writeReview(
menuId: Long,
body: WriteReviewRequest,
): Flow<BaseResponse<Void>> =
flow {
emit(reviewService.writeReview(menuId, body))
}
): Boolean =
reviewService.writeReview(menuId, body).isSuccess()

override suspend fun deleteReview(reviewId: Long): Flow<BaseResponse<Void>> =
flow {
emit(reviewService.deleteReview(reviewId))
}

override suspend fun deleteReview(reviewId: Long): Boolean =
reviewService.deleteReview(reviewId).isSuccess()

override suspend fun modifyReview(
reviewId: Long,
body: ModifyReviewRequest,
): Flow<BaseResponse<Void>> =
flow {
emit(reviewService.modifyReview(reviewId, body))
}
): Boolean =
reviewService.modifyReview(reviewId, body).isSuccess()

override suspend fun getReviewList(
menuType: String,
mealId: Long?,
menuId: Long?,
): Flow<BaseResponse<GetReviewListResponse>> = flow {
emit(reviewService.getReviewList(menuType, mealId, menuId))
}
): List<Review> =
reviewService.getReviewList(menuType, mealId, menuId).map { it.toReviewList() }
.orEmptyList()

override suspend fun getMenuReviewInfo(menuId: Long): GetMenuReviewInfoResponse? =
reviewService.getMenuReviewInfo(menuId).orNull()

override suspend fun getMenuReviewInfo(menuId: Long): Flow<BaseResponse<GetMenuReviewInfoResponse>> =
flow {
emit(reviewService.getMenuReviewInfo(menuId))
}
override suspend fun getMealReviewInfo(mealId: Long): GetMealReviewInfoResponse? =
reviewService.getMealReviewInfo(mealId).orNull()

override suspend fun getMealReviewInfo(mealId: Long): Flow<BaseResponse<GetMealReviewInfoResponse>> =
flow {
emit(reviewService.getMealReviewInfo(mealId))
}
override suspend fun getImageString(file: File): Flow<BaseResponse<ImageResponse>> = flow {
override suspend fun getImageString(file: File): ImageResponse? {
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
val multipart = MultipartBody.Part.createFormData("image", file.name, requestFile)
val response = reviewService.uploadImage(multipart)
emit(response)
}

return reviewService.uploadImage(multipart).orNull()
}
}
Loading