Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
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
51 changes: 51 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,51 @@
package com.eatssu.android.data.model

import java.io.IOException

sealed class ApiResult<T : Any> {
fun <R : Any> map(transform: (T) -> R): ApiResult<out R> = when (this) {
is Success -> Success(transform(data))
is Failure -> Failure(responseCode, message)
is NetworkError -> NetworkError(exception)
is UnknownError -> UnknownError(exception)
}

fun orElse(default: T): T = when (this) {
is Success -> data
else -> default
}

fun orElse(default: () -> T): T = when (this) {
is Success -> data
else -> default()
}

fun orNull(): T? = when (this) {
is Success -> data
else -> null
}

data class Success<T : Any>(val data: T) : ApiResult<T>()

data class Failure(
val responseCode: Int,
val message: String?
) : ApiResult<Nothing>()

data class NetworkError(
val exception: IOException
) : ApiResult<Nothing>()

data class UnknownError(
val exception: Throwable
) : ApiResult<Nothing>()

}

fun <TElement, TList : List<TElement>> ApiResult<TList>.orEmptyList(): List<TElement> =
when (this) {
is ApiResult.Success -> data
else -> emptyList()
}

fun ApiResult<Unit>.isSuccess(): Boolean = this is ApiResult.Success
Original file line number Diff line number Diff line change
@@ -1,43 +1,24 @@
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.toDomain
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.service.MealService
import com.eatssu.android.domain.repository.MealRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
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)

// 응답이 성공적이라면 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))
}
}
): List<List<String>> {
return mealService.getTodayMeal(date, restaurant, time).orEmptyList().toDomain()
}

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
Expand Up @@ -10,14 +10,12 @@ 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,7 @@
package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.response.toDomain
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.service.PartnershipService
import com.eatssu.android.data.service.UserService
import com.eatssu.android.domain.model.Partnership
Expand All @@ -13,24 +14,15 @@ class PartnershipRepositoryImpl @Inject constructor(
private val userService: UserService,
) : 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 { partnershipResponse -> partnershipResponse.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,13 @@ 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.model.isSuccess
import com.eatssu.android.data.service.ReviewService
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 +21,36 @@ 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))
}
): GetReviewListResponse? =
reviewService.getReviewList(menuType, mealId, menuId).orNull()

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,49 @@ package com.eatssu.android.data.repository

import com.eatssu.android.data.dto.request.ChangeNicknameRequest
import com.eatssu.android.data.dto.request.UserDepartmentRequest
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.dto.response.MyReviewResponse
import com.eatssu.android.data.dto.response.toDomain
import com.eatssu.android.data.model.isSuccess
import com.eatssu.android.data.model.orEmptyList
import com.eatssu.android.data.service.UserService
import com.eatssu.android.domain.model.College
import com.eatssu.android.domain.model.Department
import com.eatssu.android.domain.repository.UserRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject

class UserRepositoryImpl @Inject constructor(private val userService: UserService) :
UserRepository {

override suspend fun updateUserName(body: ChangeNicknameRequest) {
userService.changeNickname(body)
}

override suspend fun updateUserName(body: ChangeNicknameRequest): Boolean =
userService.changeNickname(body).isSuccess()

override suspend fun checkUserNameValidation(nickname: String): Boolean =
userService.checkNickname(nickname).orElse(false)
Comment on lines +22 to +26
Copy link
Member

Choose a reason for hiding this comment

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

isSuccess를 쓰는 상황과 .orElse(false)를 쓰는 상황이 정확히 나뉘는 기준이 있을까요?

Copy link
Member Author

@PeraSite PeraSite Oct 15, 2025

Choose a reason for hiding this comment

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

updateUserName는 Response 형태가 Unit이라 BaseResponse에서 성공했는지, HTTP Response Code가 어떻게 되는지만 확인하면 되는데,
checkNickname은 Response 형태가 Boolean이라서 BaseResponse에도 성공했고, HTTP Response도 200인데 result.data 타입이 false가 될 수 있는 Boolean이길래 이렇게 작성했습니다!

실제 서버 코드를 확인해본 것은 아니지만 닉네임이 중복인게(=result.data가 false인게) 오류는 아니니까 정상적으로 반환하는 것 같아요.
즉 네트워크 요청은 Success 상태지만 닉네임이 중복이라 값이 false인 경우를 처리하는 목적입니다!


override suspend fun checkUserNameValidation(nickname: String): Flow<BaseResponse<Boolean>> =
flow {
emit(userService.checkNickname(nickname))
}
override suspend fun getUserReviews(): MyReviewResponse? =
userService.getMyReviews().orNull()

override suspend fun getUserReviews(): Flow<BaseResponse<MyReviewResponse>> =
flow {
emit(userService.getMyReviews())
}
override suspend fun getUserNickName(): String =
userService.getMyInfo().map { it.nickname ?: "" }.orNull() ?: ""

override suspend fun getUserNickName() = userService.getMyInfo().result?.nickname ?: ""

override suspend fun signOut(): Boolean {
return userService.signOut().result ?: false
}
override suspend fun signOut(): Boolean =
userService.signOut().orElse(false)
Copy link
Member

Choose a reason for hiding this comment

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

여기도 .isSuccess() 랑 차이가 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

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

위 케이스와 마찬가지로 Success인데 result.data가 false인 케이스가 가능하게 API 명세가 Boolean을 반환해서 이렇게 대응했습니다!


override suspend fun getTotalColleges(): List<College> =
userService.getCollegeList().result?.map { it.toDomain() }.orEmpty()
userService.getCollegeList()
.map { list -> list.map { it.toDomain() } }
.orEmptyList()

override suspend fun getTotalDepartments(collegeId: Int): List<Department> =
userService.getDepartmentsByCollege(collegeId).result?.map { it.toDomain() }.orEmpty()
userService.getDepartmentsByCollege(collegeId)
.map { list -> list.map { it.toDomain() } }
.orEmptyList()

override suspend fun getUserCollegeDepartment(): Pair<College, Department> =
userService.getUserCollegeDepartment().result?.toDomain()
?: throw IllegalStateException("유저 학과 정보를 불러올 수 없습니다.")
override suspend fun getUserCollegeDepartment(): Pair<College, Department>? =
userService.getUserCollegeDepartment().map { it.toDomain() }.orNull()

override suspend fun setUserDepartment(departmentId: Int): BaseResponse<Void> {
return userService.setUserDepartment(
UserDepartmentRequest(departmentId)
)
override suspend fun setUserDepartment(departmentId: Int): Boolean {
return userService.setUserDepartment(UserDepartmentRequest(departmentId)).isSuccess()
}

}
Loading