diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 50f221b92..8f1a8100e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -186,6 +186,9 @@ android:name="android.app.lib_name" android:value="" /> + diff --git a/app/src/main/java/com/eatssu/android/data/dto/response/MealResponse.kt b/app/src/main/java/com/eatssu/android/data/dto/response/MealResponse.kt index a0f50e053..6b18c17c0 100644 --- a/app/src/main/java/com/eatssu/android/data/dto/response/MealResponse.kt +++ b/app/src/main/java/com/eatssu/android/data/dto/response/MealResponse.kt @@ -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 = arrayListOf(), + @SerializedName("briefMenus") var briefMenus: List = emptyList(), ) data class MenusInformationList( @@ -18,7 +18,7 @@ data class MenusInformationList( ) -fun ArrayList.mapTodayMenuResponseToMenu(): List { +fun List.mapTodayMenuResponseToMenu(): List { val menuList = mutableListOf() this.forEach { mealResponse -> @@ -37,7 +37,7 @@ fun ArrayList.mapTodayMenuResponseToMenu(): List { } -fun ArrayList.toDomain(): List> { +fun List.toDomain(): List> { return this.map { meal -> meal.briefMenus.mapNotNull { it.name } } diff --git a/app/src/main/java/com/eatssu/android/data/dto/response/MenuResponse.kt b/app/src/main/java/com/eatssu/android/data/dto/response/MenuResponse.kt index 1118e758c..4c882f709 100644 --- a/app/src/main/java/com/eatssu/android/data/dto/response/MenuResponse.kt +++ b/app/src/main/java/com/eatssu/android/data/dto/response/MenuResponse.kt @@ -7,14 +7,14 @@ import timber.log.Timber data class GetFixedMenuResponse( - @SerializedName("categoryMenuListCollection") var categoryMenuListCollection: ArrayList = arrayListOf(), + @SerializedName("categoryMenuListCollection") var categoryMenuListCollection: List = emptyList(), ) data class CategoryMenuListCollection( @SerializedName("category") var category: String? = null, - @SerializedName("menus") var menus: ArrayList = arrayListOf(), + @SerializedName("menus") var menus: List = emptyList(), ) diff --git a/app/src/main/java/com/eatssu/android/data/model/ApiResult.kt b/app/src/main/java/com/eatssu/android/data/model/ApiResult.kt new file mode 100644 index 000000000..fdda33b79 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/model/ApiResult.kt @@ -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 { + // API가 성공했을 때 + data class Success(val data: T) : ApiResult + + // 서버에서 에러 반환 + data class Failure( + val responseCode: Int, + val message: String? + ) : ApiResult + + // 소켓 연결 끊김, 타임아웃 등 네트워크 예외인 IOException 처리 + data class NetworkError( + val exception: IOException + ) : ApiResult + + // IOException을 제외한 예외 처리 + data class UnknownError( + val exception: Throwable + ) : ApiResult +} + +// ApiResult가 성공인지 아닌지 여부 확인 +fun ApiResult.isSuccess(): Boolean = this is Success + +// 성공한 경우 데이터 반환, 실패한 경우 빈 리스트 반환 +fun > ApiResult.orEmptyList(): List = + when (this) { + is Success -> data + else -> emptyList() + } + +// 성공한 경우 데이터 반환, 실패한 경우 기본값 반환 +fun ApiResult.orElse(default: T): T = when (this) { + is Success -> data + else -> default +} + +// 성공한 경우 데이터 반환, 실패한 경우 null 반환 +fun ApiResult.orNull(): T? = when (this) { + is Success -> data + else -> null +} + +// ApiResult가 Success일 때 데이터를 변환 +fun ApiResult.map(transform: (T) -> R): ApiResult = when (this) { + is Success -> Success(transform(data)) + is Failure -> Failure(responseCode, message) + is NetworkError -> NetworkError(exception) + is UnknownError -> UnknownError(exception) +} \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/repository/HealthCheckRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/HealthCheckRepositoryImpl.kt new file mode 100644 index 000000000..06ff6c7d5 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/repository/HealthCheckRepositoryImpl.kt @@ -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() + } +} diff --git a/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt index 9202e2e0b..a514316f9 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/MealRepositoryImpl.kt @@ -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>> { - return flow { - try { - val response = mealService.getTodayMeal2(date, restaurant, time) + ): List> { + 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 { + return mealService.getTodayMeal(date, restaurant.toString(), time.toString()) + .map { it.mapTodayMenuResponseToMenu() } + .orEmptyList() } - override suspend fun getMenuInfoByMealId(mealId: Long): Flow> = - flow { - emit(mealService.getMenuInfoByMealId(mealId)) - } + override suspend fun getMenuInfoByMealId(mealId: Long): List = + mealService.getMenuInfoByMealId(mealId).map { it.briefMenus }.orEmptyList() } diff --git a/app/src/main/java/com/eatssu/android/data/repository/MenuRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/MenuRepositoryImpl.kt new file mode 100644 index 000000000..6c9072436 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/repository/MenuRepositoryImpl.kt @@ -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 { + return menuService.getFixMenu(restaurant.toString()) + .map { it.mapFixedMenuResponseToMenu() } + .orEmptyList() + } +} diff --git a/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt index e41581aa8..e6bd3c6e5 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt @@ -3,6 +3,9 @@ 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 @@ -10,14 +13,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) } diff --git a/app/src/main/java/com/eatssu/android/data/repository/PartnershipRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/PartnershipRepositoryImpl.kt index dd1eac12e..6411e4717 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/PartnershipRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/PartnershipRepositoryImpl.kt @@ -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 @@ -14,23 +17,17 @@ class PartnershipRepositoryImpl @Inject constructor( ) : PartnershipRepository { // 유저의 학과 상관없이 모든 제휴 정보 조회 - override suspend fun getAllPartnerships(): List { - return partnershipService.getAllPartnerships() - .result - ?.map { it.toDomain() } ?: emptyList() - } + override suspend fun getAllPartnerships(): List = + 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 { - return userService.getUserDepartmentPartnerships() - .result - ?.map { it.toDomain() } ?: emptyList() - } + override suspend fun getUserCollegePartnerships(): List = + userService.getUserDepartmentPartnerships().map { list -> list.map { it.toDomain() } } + .orEmptyList() } diff --git a/app/src/main/java/com/eatssu/android/data/repository/ReportRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/ReportRepositoryImpl.kt index a6fb009fd..8db91263f 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/ReportRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/ReportRepositoryImpl.kt @@ -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> = - flow { - emit(reportService.reportReview(body)) - } + override suspend fun reportReview(body: ReportRequest): Boolean = + reportService.reportReview(body).isSuccess() } diff --git a/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt index 3d1b7a1d5..22bfe5bd5 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/ReviewRepositoryImpl.kt @@ -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 @@ -23,46 +25,37 @@ class ReviewRepositoryImpl @Inject constructor(private val reviewService: Review override suspend fun writeReview( menuId: Long, body: WriteReviewRequest, - ): Flow> = - flow { - emit(reviewService.writeReview(menuId, body)) - } + ): Boolean = + reviewService.writeReview(menuId, body).isSuccess() - override suspend fun deleteReview(reviewId: Long): Flow> = - 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> = - flow { - emit(reviewService.modifyReview(reviewId, body)) - } + ): Boolean = + reviewService.modifyReview(reviewId, body).isSuccess() override suspend fun getReviewList( menuType: String, mealId: Long?, menuId: Long?, - ): Flow> = flow { - emit(reviewService.getReviewList(menuType, mealId, menuId)) - } + ): List = + 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> = - flow { - emit(reviewService.getMenuReviewInfo(menuId)) - } + override suspend fun getMealReviewInfo(mealId: Long): GetMealReviewInfoResponse? = + reviewService.getMealReviewInfo(mealId).orNull() - override suspend fun getMealReviewInfo(mealId: Long): Flow> = - flow { - emit(reviewService.getMealReviewInfo(mealId)) - } - override suspend fun getImageString(file: File): Flow> = 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() + } } diff --git a/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt index 12c41ba66..5da2675a4 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/UserRepositoryImpl.kt @@ -2,56 +2,53 @@ 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.dto.response.toReviewList +import com.eatssu.android.data.model.isSuccess +import com.eatssu.android.data.model.map +import com.eatssu.android.data.model.orElse +import com.eatssu.android.data.model.orEmptyList +import com.eatssu.android.data.model.orNull 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.model.Review 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) - override suspend fun checkUserNameValidation(nickname: String): Flow> = - flow { - emit(userService.checkNickname(nickname)) - } + override suspend fun getUserReviews(): List = + userService.getMyReviews().map { it.toReviewList() }.orEmptyList() - override suspend fun getUserReviews(): Flow> = - 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) override suspend fun getTotalColleges(): List = - userService.getCollegeList().result?.map { it.toDomain() }.orEmpty() + userService.getCollegeList() + .map { list -> list.map { it.toDomain() } } + .orEmptyList() override suspend fun getTotalDepartments(collegeId: Int): List = - userService.getDepartmentsByCollege(collegeId).result?.map { it.toDomain() }.orEmpty() + userService.getDepartmentsByCollege(collegeId) + .map { list -> list.map { it.toDomain() } } + .orEmptyList() - override suspend fun getUserCollegeDepartment(): Pair = - userService.getUserCollegeDepartment().result?.toDomain() - ?: throw IllegalStateException("유저 학과 정보를 불러올 수 없습니다.") + override suspend fun getUserCollegeDepartment(): Pair? = + userService.getUserCollegeDepartment().map { it.toDomain() }.orNull() - override suspend fun setUserDepartment(departmentId: Int): BaseResponse { - return userService.setUserDepartment( - UserDepartmentRequest(departmentId) - ) + override suspend fun setUserDepartment(departmentId: Int): Boolean { + return userService.setUserDepartment(UserDepartmentRequest(departmentId)).isSuccess() } } diff --git a/app/src/main/java/com/eatssu/android/data/service/HealthCheckService.kt b/app/src/main/java/com/eatssu/android/data/service/HealthCheckService.kt new file mode 100644 index 000000000..d1599dec2 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/service/HealthCheckService.kt @@ -0,0 +1,14 @@ +package com.eatssu.android.data.service + +import com.eatssu.android.data.model.ApiResult +import retrofit2.http.GET + +interface HealthCheckService { + /** + * 서버와 정상적으로 통신할 수 있는지 확인합니다. + * 실제 서버의 상태(healthy)를 체크하는 목적이 아니라, 네트워크 연결이 가능한지 확인하는 용도입니다. + * 반환 타입이 Unit인 이유는 응답 본문의 내용이 중요하지 않고, 통신 성공 여부만 판단하기 때문입니다. + */ + @GET("actuator/health") + suspend fun checkHealth(): ApiResult +} diff --git a/app/src/main/java/com/eatssu/android/data/service/MealService.kt b/app/src/main/java/com/eatssu/android/data/service/MealService.kt index fabe0a657..1e3f33be0 100644 --- a/app/src/main/java/com/eatssu/android/data/service/MealService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/MealService.kt @@ -1,38 +1,23 @@ package com.eatssu.android.data.service -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.GetMealResponse import com.eatssu.android.data.dto.response.MenuOfMealResponse -import retrofit2.Call +import com.eatssu.android.data.model.ApiResult import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query interface MealService { - /** - * 변동메뉴 식단 리스트 조회 By 식당 - */ @GET("meals") - fun getTodayMeal( + suspend fun getTodayMeal( @Query("date") date: String, @Query("restaurant") restaurant: String, @Query("time") time: String, - ): Call>> + ): ApiResult> - // todo 위에 함수를 call 없애서 하나로 합치길 바람 ㅜㅜ 위젯 때문에 급하게 복사본을 만듦 - @GET("meals") - suspend fun getTodayMeal2( - @Query("date") date: String, - @Query("restaurant") restaurant: String, - @Query("time") time: String, - ): BaseResponse> - - /** - * 메뉴 정보 리스트 조회 - */ @GET("meals/{mealId}/menus-info") suspend fun getMenuInfoByMealId( @Path("mealId") mealId: Long, - ): BaseResponse + ): ApiResult } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/service/MenuService.kt b/app/src/main/java/com/eatssu/android/data/service/MenuService.kt index 5e17695eb..8c44ba4b6 100644 --- a/app/src/main/java/com/eatssu/android/data/service/MenuService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/MenuService.kt @@ -1,8 +1,7 @@ package com.eatssu.android.data.service -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.GetFixedMenuResponse -import retrofit2.Call +import com.eatssu.android.data.model.ApiResult import retrofit2.http.GET import retrofit2.http.Query @@ -12,8 +11,8 @@ interface MenuService { * 고정 메뉴 리스트 조회 */ @GET("menus") - fun getFixMenu( + suspend fun getFixMenu( @Query("restaurant") restaurant: String, - ): Call> + ): ApiResult } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/service/OauthService.kt b/app/src/main/java/com/eatssu/android/data/service/OauthService.kt index 0bc47e3b9..ab3f8c1f6 100644 --- a/app/src/main/java/com/eatssu/android/data/service/OauthService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/OauthService.kt @@ -2,8 +2,8 @@ package com.eatssu.android.data.service import com.eatssu.android.data.dto.request.CheckValidTokenRequest import com.eatssu.android.data.dto.request.LoginWithKakaoRequest -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.TokenResponse +import com.eatssu.android.data.model.ApiResult import retrofit2.http.Body import retrofit2.http.Header import retrofit2.http.POST @@ -13,15 +13,15 @@ interface OauthService { //여기는 토큰이 없는 레트로핏을 끼웁니 suspend fun getNewToken( @Header("Authorization") refreshToken: String?, ) //얘는 SP에 있는거 헤더에 넣어주면 됩니다. - : BaseResponse + : ApiResult @POST("oauths/kakao") suspend fun loginWithKakao( @Body request: LoginWithKakaoRequest, - ): BaseResponse + ): ApiResult @POST("oauths/valid/token") suspend fun checkValidToken( @Body request: CheckValidTokenRequest, - ): BaseResponse + ): ApiResult } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/service/PartnershipService.kt b/app/src/main/java/com/eatssu/android/data/service/PartnershipService.kt index 1da945f0a..c25101d32 100644 --- a/app/src/main/java/com/eatssu/android/data/service/PartnershipService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/PartnershipService.kt @@ -1,22 +1,22 @@ package com.eatssu.android.data.service -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.PartnershipResponse import com.eatssu.android.data.dto.response.PartnershipRestaurantResponse +import com.eatssu.android.data.model.ApiResult import retrofit2.http.GET interface PartnershipService{ // 전체 제휴 조회 @GET("partnerships") - suspend fun getAllPartnerships(): BaseResponse> + suspend fun getAllPartnerships(): ApiResult> // 개별 제휴 조회 @GET("partnerships/{partnershipId}") - suspend fun getPartnershipById(partnershipId: Int): BaseResponse + suspend fun getPartnershipById(partnershipId: Int): ApiResult // TODO 제휴 찜/등록하기/ 취소하기 @GET("partnerships/{partnershipId}/like") - suspend fun likePartnership(partnershipId: Int): BaseResponse + suspend fun likePartnership(partnershipId: Int): ApiResult } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/service/ReportService.kt b/app/src/main/java/com/eatssu/android/data/service/ReportService.kt index c8309c1f6..6f9c4fc0e 100644 --- a/app/src/main/java/com/eatssu/android/data/service/ReportService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/ReportService.kt @@ -1,7 +1,7 @@ package com.eatssu.android.data.service import com.eatssu.android.data.dto.request.ReportRequest -import com.eatssu.android.data.dto.response.BaseResponse +import com.eatssu.android.data.model.ApiResult import retrofit2.http.Body import retrofit2.http.POST @@ -9,5 +9,5 @@ interface ReportService { @POST("reports") //리뷰 신고하기 suspend fun reportReview( @Body request: ReportRequest, - ): BaseResponse + ): ApiResult } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/service/ReviewService.kt b/app/src/main/java/com/eatssu/android/data/service/ReviewService.kt index d33bb2204..352b1be55 100644 --- a/app/src/main/java/com/eatssu/android/data/service/ReviewService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/ReviewService.kt @@ -3,11 +3,11 @@ package com.eatssu.android.data.service 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.ApiResult import okhttp3.MultipartBody import retrofit2.http.Body import retrofit2.http.DELETE @@ -25,18 +25,18 @@ interface ReviewService { suspend fun writeReview( @Path("menuId") menuId: Long, @Body request: WriteReviewRequest, - ): BaseResponse + ): ApiResult @DELETE("/reviews/{reviewId}") //리뷰 삭제 suspend fun deleteReview( @Path("reviewId") reviewId: Long, - ): BaseResponse + ): ApiResult @PATCH("/reviews/{reviewId}") //리뷰 수정(글 수정) suspend fun modifyReview( @Path("reviewId") reviewId: Long, @Body request: ModifyReviewRequest, - ): BaseResponse + ): ApiResult //Todo paging 라이브러리 써보기 @GET("/reviews") //리뷰 리스트 조회 @@ -48,22 +48,22 @@ interface ReviewService { @Query("page") page: Int? = 0, @Query("size") size: Int? = 20, @Query("sort") sort: List? = arrayListOf("date", "DESC"), - ): BaseResponse + ): ApiResult @GET("/reviews/menus/{menuId}") //고정 메뉴 리뷰 정보 조회(메뉴명, 평점 등등) suspend fun getMenuReviewInfo( @Path("menuId") menuId: Long, - ): BaseResponse + ): ApiResult @GET("/reviews/meals/{mealId}") //식단(변동 메뉴) 리뷰 정보 조회(메뉴명, 평점 등등) suspend fun getMealReviewInfo( @Path("mealId") mealId: Long, - ): BaseResponse + ): ApiResult @Multipart @POST("/reviews/upload/image") //리뷰 이미지 업로드 suspend fun uploadImage( @Part image: MultipartBody.Part, - ): BaseResponse + ): ApiResult } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/service/UserService.kt b/app/src/main/java/com/eatssu/android/data/service/UserService.kt index 20ea1f995..e61baee79 100644 --- a/app/src/main/java/com/eatssu/android/data/service/UserService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/UserService.kt @@ -2,13 +2,13 @@ package com.eatssu.android.data.service 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.CollegeResponse import com.eatssu.android.data.dto.response.DepartmentResponse import com.eatssu.android.data.dto.response.MyNickNameResponse import com.eatssu.android.data.dto.response.MyReviewResponse import com.eatssu.android.data.dto.response.PartnershipResponse import com.eatssu.android.data.dto.response.UserCollegeDepartmentResponse +import com.eatssu.android.data.model.ApiResult import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET @@ -21,39 +21,39 @@ interface UserService { @PATCH("users/nickname") //닉네임 수정 suspend fun changeNickname( @Body request: ChangeNicknameRequest, - ): BaseResponse + ): ApiResult @GET("users/validate/nickname") //닉네임 중복 체크 suspend fun checkNickname( @Query("nickname") nickname: String, - ): BaseResponse + ): ApiResult @GET("users/reviews") //내가 쓴 리뷰 모아보기 - suspend fun getMyReviews(): BaseResponse + suspend fun getMyReviews(): ApiResult @GET("users/mypage") //내 정보 모아보기 - suspend fun getMyInfo(): BaseResponse + suspend fun getMyInfo(): ApiResult @DELETE("users") //유저 탈퇴 - suspend fun signOut(): BaseResponse + suspend fun signOut(): ApiResult @GET("users/lookup/colleges") // 교내 모든 단과대 조회 - suspend fun getCollegeList(): BaseResponse> + suspend fun getCollegeList(): ApiResult> @GET("users/lookup/departments") // 단과대에 따른 학과 조회 suspend fun getDepartmentsByCollege( @Query("collegeId") collegeId: Int, - ): BaseResponse> + ): ApiResult> @GET("users/department") // 유저의 단과대, 학과 조회 - suspend fun getUserCollegeDepartment(): BaseResponse + suspend fun getUserCollegeDepartment(): ApiResult @POST("users/department") // 유저의 학과 설정 suspend fun setUserDepartment( @Body departmentId: UserDepartmentRequest, - ): BaseResponse + ): ApiResult @GET("users/department/partnerships") // 유저 학과의 제휴 조회 - suspend fun getUserDepartmentPartnerships(): BaseResponse> + suspend fun getUserDepartmentPartnerships(): ApiResult> } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/DataModule.kt b/app/src/main/java/com/eatssu/android/di/DataModule.kt index e60a25fb9..83de32fe5 100644 --- a/app/src/main/java/com/eatssu/android/di/DataModule.kt +++ b/app/src/main/java/com/eatssu/android/di/DataModule.kt @@ -1,17 +1,21 @@ package com.eatssu.android.di +import com.eatssu.android.data.repository.HealthCheckRepositoryImpl import com.eatssu.android.data.repository.MealRepositoryImpl +import com.eatssu.android.data.repository.MenuRepositoryImpl import com.eatssu.android.data.repository.OauthRepositoryImpl import com.eatssu.android.data.repository.PartnershipRepositoryImpl -import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.data.repository.ReportRepositoryImpl import com.eatssu.android.data.repository.UserRepositoryImpl +import com.eatssu.android.domain.repository.HealthCheckRepository import com.eatssu.android.domain.repository.MealRepository +import com.eatssu.android.domain.repository.MenuRepository import com.eatssu.android.domain.repository.OauthRepository +import com.eatssu.android.domain.repository.PartnershipRepository +import com.eatssu.android.domain.repository.ReportRepository import com.eatssu.android.domain.repository.ReviewRepository import com.eatssu.android.domain.repository.UserRepository -import com.eatssu.android.domain.repository.PartnershipRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -51,4 +55,15 @@ abstract class DataModule { internal abstract fun bindsPartnershipRepository( partnershipRepositoryImpl: PartnershipRepositoryImpl, ): PartnershipRepository + + @Binds + internal abstract fun bindsHealthCheckRepository( + healthCheckRepositoryImpl: HealthCheckRepositoryImpl, + ): HealthCheckRepository + + @Binds + internal abstract fun bindsMenuRepository( + menuRepositoryImpl: MenuRepositoryImpl, + ): MenuRepository + } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/NetworkModule.kt b/app/src/main/java/com/eatssu/android/di/NetworkModule.kt index 683a1b5cd..12432a2eb 100644 --- a/app/src/main/java/com/eatssu/android/di/NetworkModule.kt +++ b/app/src/main/java/com/eatssu/android/di/NetworkModule.kt @@ -3,6 +3,7 @@ package com.eatssu.android.di import com.eatssu.android.BuildConfig import com.eatssu.android.BuildConfig.BASE_URL +import com.eatssu.android.di.network.ApiResultCallAdapterFactory import com.eatssu.android.di.network.TokenAuthenticator import com.eatssu.android.di.network.TokenInterceptor import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase @@ -17,6 +18,7 @@ import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import okhttp3.ResponseBody import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.CallAdapter import retrofit2.Converter import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -87,11 +89,19 @@ object NetworkModule { return builder.build() } + @Singleton + @Provides + fun provideCallAdapterFactory(): CallAdapter.Factory = ApiResultCallAdapterFactory() + // 토큰이 필요한 retrofit @Singleton @Provides - fun provideAuthRetrofit(okHttpClient: OkHttpClient): Retrofit { + fun provideAuthRetrofit( + okHttpClient: OkHttpClient, + callAdapterFactory: CallAdapter.Factory, + ): Retrofit { return Retrofit.Builder().client(okHttpClient).baseUrl(BASE_URL) + .addCallAdapterFactory(callAdapterFactory) .addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(NullOnEmptyConverterFactory()) .build() @@ -101,8 +111,12 @@ object NetworkModule { @Singleton @Provides @NoToken - fun provideNoAuthRetrofit(@NoToken okHttpClient: OkHttpClient): Retrofit { + fun provideNoAuthRetrofit( + @NoToken okHttpClient: OkHttpClient, + callAdapterFactory: CallAdapter.Factory, + ): Retrofit { return Retrofit.Builder().client(okHttpClient).baseUrl(BASE_URL) + .addCallAdapterFactory(callAdapterFactory) .addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(NullOnEmptyConverterFactory()) .build() diff --git a/app/src/main/java/com/eatssu/android/di/ServiceModule.kt b/app/src/main/java/com/eatssu/android/di/ServiceModule.kt index ef0d223b6..cf054f515 100644 --- a/app/src/main/java/com/eatssu/android/di/ServiceModule.kt +++ b/app/src/main/java/com/eatssu/android/di/ServiceModule.kt @@ -1,5 +1,6 @@ package com.eatssu.android.di +import com.eatssu.android.data.service.HealthCheckService import com.eatssu.android.data.service.MealService import com.eatssu.android.data.service.MenuService import com.eatssu.android.data.service.OauthService @@ -22,6 +23,7 @@ object ServiceModule { fun provideOauthService(@NoToken noTokenRetrofit: Retrofit): OauthService { return noTokenRetrofit.create(OauthService::class.java) } + @Provides @Singleton fun provideUserService(retrofit: Retrofit): UserService { @@ -57,4 +59,11 @@ object ServiceModule { fun providePartnershipService(retrofit: Retrofit): PartnershipService { return retrofit.create(PartnershipService::class.java) } + + @Provides + @Singleton + fun provideHealthCheckService(@NoToken noTokenRetrofit: Retrofit): HealthCheckService { + return noTokenRetrofit.create(HealthCheckService::class.java) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/network/ApiResultCall.kt b/app/src/main/java/com/eatssu/android/di/network/ApiResultCall.kt new file mode 100644 index 000000000..158505779 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/di/network/ApiResultCall.kt @@ -0,0 +1,116 @@ +package com.eatssu.android.di.network + +import com.eatssu.android.data.dto.response.BaseResponse +import com.eatssu.android.data.model.ApiResult +import com.eatssu.android.presentation.base.NetworkErrorEventBus +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import timber.log.Timber +import java.io.IOException +import java.lang.reflect.Type + +@Suppress("UNCHECKED_CAST") +class ApiResultCall( + private val call: Call>, + private val responseType: Type +) : Call> { + + override fun enqueue(callback: Callback>) { + call.enqueue(object : Callback> { + override fun onResponse( + call: Call>, + response: Response> + ) { + callback.onResponse( + this@ApiResultCall, + Response.success(response.toApiResult()) as Response> + ) + } + + override fun onFailure(call: Call>, error: Throwable) { + Timber.e(error, "ApiResultCall - onFailure called") + val response = when (error) { + is IOException -> { + // NetworkError 발생 시 전역 이벤트 발생 + NetworkErrorEventBus.notifyNetworkError() + ApiResult.NetworkError(error) + } + else -> ApiResult.UnknownError(error) + } + callback.onResponse( + this@ApiResultCall, + Response.success(response) as Response> + ) + } + }) + } + + private fun Response>.toApiResult(): ApiResult { + // HTTP Response code가 200 ~ 300대가 아닌 경우 (ex. 400, 500) + if (!isSuccessful) { + val responseCode = code() + val errorMessage = errorBody()?.string() + + Timber.d("ApiResultCall - HTTP Response is not successful: $responseCode - $errorMessage") + return ApiResult.Failure( + responseCode, + errorMessage + ) + } + + val body = body() + + // 오류가 발생해도 프로토콜상 Body는 존재해야 함 + if (body == null) { + Timber.d("ApiResultCall - Response body is null") + return ApiResult.UnknownError( + IllegalStateException("Response Body가 존재하지 않습니다.") + ) + } + + if (body.isSuccess == false) { + Timber.d("ApiResultCall - API indicates failure: ${body.code} - ${body.message}") + return ApiResult.Failure( + body.code ?: -1, + body.message + ) + } + + if (responseType == Unit::class.java) { + Timber.d("ApiResultCall - Response type is Unit, returning Success with Unit") + return ApiResult.Success(Unit as T) + } + + val result = body.result + if (result == null) { + Timber.d("ApiResultCall - Result is null in successful response") + return ApiResult.UnknownError( + IllegalStateException("Result가 존재하지 않습니다.") + ) + } + + Timber.d("ApiResultCall - Successful API call, returning Success with $result") + return ApiResult.Success(result) + } + + + override fun isExecuted(): Boolean = call.isExecuted + + override fun clone(): Call> = ApiResultCall(call.clone(), responseType) + + override fun isCanceled(): Boolean = call.isCanceled + + override fun cancel() = call.cancel() + + override fun execute(): Response> { + throw UnsupportedOperationException("EatssuCall doesn't support execute") + } + + override fun request(): Request = call.request() + + override fun timeout(): Timeout = call.timeout() +} + diff --git a/app/src/main/java/com/eatssu/android/di/network/ApiResultCallAdapter.kt b/app/src/main/java/com/eatssu/android/di/network/ApiResultCallAdapter.kt new file mode 100644 index 000000000..27efc609c --- /dev/null +++ b/app/src/main/java/com/eatssu/android/di/network/ApiResultCallAdapter.kt @@ -0,0 +1,20 @@ +package com.eatssu.android.di.network + +import com.eatssu.android.data.dto.response.BaseResponse +import com.eatssu.android.data.model.ApiResult +import retrofit2.Call +import retrofit2.CallAdapter +import java.lang.reflect.Type + +class ApiResultCallAdapter( + private val baseResponseType: Type, + private val dataType: Type +) : CallAdapter, Call>> { + + override fun responseType(): Type = baseResponseType + + override fun adapt(call: Call>): Call> { + return ApiResultCall(call, dataType) + } +} + diff --git a/app/src/main/java/com/eatssu/android/di/network/ApiResultCallAdapterFactory.kt b/app/src/main/java/com/eatssu/android/di/network/ApiResultCallAdapterFactory.kt new file mode 100644 index 000000000..a521abfe7 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/di/network/ApiResultCallAdapterFactory.kt @@ -0,0 +1,46 @@ +package com.eatssu.android.di.network + +import com.eatssu.android.data.dto.response.BaseResponse +import com.eatssu.android.data.model.ApiResult +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ApiResultCallAdapterFactory : CallAdapter.Factory() { + + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + if (getRawType(returnType) == ApiResult::class.java) { + throw IllegalStateException("함수가 suspend로 선언 되어 있지 않습니다: ${annotations.joinToString { it.toString() }}") + } + + if (getRawType(returnType) != Call::class.java) return null + check(returnType is ParameterizedType) { + "Return 타입은 ApiResult 형태여야 합니다." + } + + val responseType = getParameterUpperBound(0, returnType) + if (getRawType(responseType) != ApiResult::class.java) return null + check(responseType is ParameterizedType) { + "Return 타입은 ApiResult 형태여야 합니다." + } + + val successType = getParameterUpperBound(0, responseType) + return createCallAdapter(successType) + } + + fun createCallAdapter(successType: Type): ApiResultCallAdapter { + val baseResponseType = object : ParameterizedType { + override fun getRawType(): Type = BaseResponse::class.java + override fun getActualTypeArguments(): Array = arrayOf(successType) + override fun getOwnerType(): Type? = null + } + return ApiResultCallAdapter(baseResponseType, successType) + } +} + diff --git a/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt b/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt index a1247df8f..12a5c0995 100644 --- a/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt +++ b/app/src/main/java/com/eatssu/android/di/network/TokenAuthenticator.kt @@ -1,6 +1,5 @@ package com.eatssu.android.di.network -import com.eatssu.android.domain.model.Token import com.eatssu.android.domain.model.TokenStateManager import com.eatssu.android.domain.usecase.auth.GetRefreshTokenUseCase import com.eatssu.android.domain.usecase.auth.LogoutUseCase @@ -41,41 +40,32 @@ class TokenAuthenticator @Inject constructor( return null } - val expiredRefreshToken = runBlocking { getRefreshTokenUseCase() } - return runBlocking { - try { - Timber.d("TokenAuthenticator → refreshToken으로 재발급 시도") - - val newToken: Token = reissueTokenUseCase(expiredRefreshToken) - val newAccessToken = newToken.accessToken - val newRefreshToken = newToken.refreshToken + Timber.d("TokenAuthenticator → refreshToken으로 재발급 시도") - if (newAccessToken.isNotEmpty() && newRefreshToken.isNotEmpty()) { - Timber.d("TokenAuthenticator → 새 토큰 발급 성공") - setAccessTokenUseCase(newAccessToken) - setRefreshTokenUseCase(newRefreshToken) - } else { - // 잘못된 토큰을 받은 경우 - Timber.e("TokenAuthenticator → 새 토큰 발급 실패") - logoutUseCase() // 로그아웃 처리 - TokenStateManager.setTokenError() - } + val expiredRefreshToken = getRefreshTokenUseCase() + val newToken = reissueTokenUseCase(expiredRefreshToken) - Timber.d("TokenAuthenticator → 새 토큰 저장 및 기존 API 재요청") + val newAccessToken = newToken?.accessToken + val newRefreshToken = newToken?.refreshToken - response.request.newBuilder() - .header("Authorization", "Bearer ${newAccessToken}") - .build() + if (newAccessToken.isNullOrEmpty() || + newRefreshToken.isNullOrEmpty() + ) { + Timber.e("TokenAuthenticator → 새 토큰 발급 실패") + logoutUseCase() // 로그아웃 처리 + TokenStateManager.setTokenError() + return@runBlocking null + } - } catch (e: Exception) { - // refreshToken이 만료된 경우 - Timber.e(e, "refreshToken이 만료") - logoutUseCase() - TokenStateManager.setTokenExpired() + Timber.d("TokenAuthenticator → 새 토큰 발급 성공") + setAccessTokenUseCase(newAccessToken) + setRefreshTokenUseCase(newRefreshToken) - null - } + Timber.d("TokenAuthenticator → 새 토큰 저장 및 기존 API 재요청") + response.request.newBuilder() + .header("Authorization", "Bearer $newAccessToken") + .build() } } diff --git a/app/src/main/java/com/eatssu/android/domain/repository/HealthCheckRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/HealthCheckRepository.kt new file mode 100644 index 000000000..0009cdadc --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/repository/HealthCheckRepository.kt @@ -0,0 +1,5 @@ +package com.eatssu.android.domain.repository + +interface HealthCheckRepository { + suspend fun checkHealth(): Boolean +} diff --git a/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt index 1aac5a02d..60764dc69 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/MealRepository.kt @@ -1,8 +1,9 @@ package com.eatssu.android.domain.repository -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.MenuOfMealResponse -import kotlinx.coroutines.flow.Flow +import com.eatssu.android.data.dto.response.MenusInformation +import com.eatssu.android.domain.model.Menu +import com.eatssu.common.enums.Restaurant +import com.eatssu.common.enums.Time interface MealRepository { @@ -13,13 +14,21 @@ interface MealRepository { date: String, restaurant: String, time: String, - ): Flow>> + ): List> + /** + * 오늘의 식단을 Menu 리스트로 가져오는 api + */ + suspend fun getTodayMenuList( + date: String, + restaurant: Restaurant, + time: Time, + ): List /** * MealId를 이용해서 Menu를 찾기 api */ suspend fun getMenuInfoByMealId( mealId: Long, - ): Flow> + ): List } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/repository/MenuRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/MenuRepository.kt new file mode 100644 index 000000000..9c67301bf --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/repository/MenuRepository.kt @@ -0,0 +1,11 @@ +package com.eatssu.android.domain.repository + +import com.eatssu.android.domain.model.Menu +import com.eatssu.common.enums.Restaurant + +interface MenuRepository { + /** + * 고정 메뉴 리스트 조회 + */ + suspend fun getFixedMenuList(restaurant: Restaurant): List +} diff --git a/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt index ae5766c1d..9b99426a4 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt @@ -7,9 +7,9 @@ import com.eatssu.android.domain.model.Token interface OauthRepository { suspend fun reissueToken( refreshToken: String, - ): Token + ): Token? - suspend fun login(body: LoginWithKakaoRequest): Token + suspend fun login(body: LoginWithKakaoRequest): Token? suspend fun checkValidToken(body: CheckValidTokenRequest): Boolean } diff --git a/app/src/main/java/com/eatssu/android/domain/repository/ReportRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/ReportRepository.kt index e1f247b14..da7ea591e 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/ReportRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/ReportRepository.kt @@ -1,13 +1,10 @@ package com.eatssu.android.domain.repository import com.eatssu.android.data.dto.request.ReportRequest -import com.eatssu.android.data.dto.response.BaseResponse -import kotlinx.coroutines.flow.Flow interface ReportRepository { suspend fun reportReview( body: ReportRequest, - ): Flow> - + ): Boolean } diff --git a/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt index 93289d82c..56f5d1393 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/ReviewRepository.kt @@ -2,12 +2,10 @@ package com.eatssu.android.domain.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 kotlinx.coroutines.flow.Flow +import com.eatssu.android.domain.model.Review import java.io.File interface ReviewRepository { @@ -15,33 +13,33 @@ interface ReviewRepository { suspend fun writeReview( menuId: Long, body: WriteReviewRequest, - ): Flow> + ): Boolean suspend fun deleteReview( reviewId: Long, - ): Flow> + ): Boolean suspend fun modifyReview( reviewId: Long, body: ModifyReviewRequest, - ): Flow> + ): Boolean suspend fun getReviewList( menuType: String, mealId: Long?, menuId: Long?, - ): Flow> + ): List suspend fun getMenuReviewInfo( menuId: Long, - ): Flow> + ): GetMenuReviewInfoResponse? suspend fun getMealReviewInfo( mealId: Long, - ): Flow> + ): GetMealReviewInfoResponse? suspend fun getImageString( file: File - ): Flow> + ): ImageResponse? } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt index fcfac9800..26dfbccf2 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/UserRepository.kt @@ -1,23 +1,21 @@ package com.eatssu.android.domain.repository import com.eatssu.android.data.dto.request.ChangeNicknameRequest -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.MyReviewResponse import com.eatssu.android.domain.model.College import com.eatssu.android.domain.model.Department -import kotlinx.coroutines.flow.Flow +import com.eatssu.android.domain.model.Review interface UserRepository { suspend fun updateUserName( body: ChangeNicknameRequest, - ) + ): Boolean suspend fun checkUserNameValidation( nickname: String, - ): Flow> + ): Boolean - suspend fun getUserReviews(): Flow> + suspend fun getUserReviews(): List suspend fun getUserNickName(): String suspend fun signOut(): Boolean @@ -28,11 +26,11 @@ interface UserRepository { suspend fun getTotalDepartments(collegeId: Int): List // 유저의 학과 정보 조회 - suspend fun getUserCollegeDepartment(): Pair + suspend fun getUserCollegeDepartment(): Pair? // 유저의 학과 설정 suspend fun setUserDepartment( departmentId: Int, - ): BaseResponse + ): Boolean } diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/auth/LoginUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/auth/LoginUseCase.kt index 37854e01d..8640d4220 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/auth/LoginUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/auth/LoginUseCase.kt @@ -8,6 +8,6 @@ import javax.inject.Inject class LoginUseCase @Inject constructor( private val oauthRepository: OauthRepository, ) { - suspend operator fun invoke(body: LoginWithKakaoRequest): Token = + suspend operator fun invoke(body: LoginWithKakaoRequest): Token? = oauthRepository.login(body) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/auth/ReissueTokenUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/auth/ReissueTokenUseCase.kt index 2144cadc8..887aecd55 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/auth/ReissueTokenUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/auth/ReissueTokenUseCase.kt @@ -7,6 +7,6 @@ import javax.inject.Inject class ReissueTokenUseCase @Inject constructor( private val oauthRepository: OauthRepository, ) { - suspend operator fun invoke(refreshToken: String): Token = + suspend operator fun invoke(refreshToken: String): Token? = oauthRepository.reissueToken(refreshToken) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/health/HealthCheckUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/health/HealthCheckUseCase.kt new file mode 100644 index 000000000..cbfad2c10 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/usecase/health/HealthCheckUseCase.kt @@ -0,0 +1,11 @@ +package com.eatssu.android.domain.usecase.health + +import com.eatssu.android.domain.repository.HealthCheckRepository +import javax.inject.Inject + +class HealthCheckUseCase @Inject constructor( + private val healthCheckRepository: HealthCheckRepository +) { + suspend operator fun invoke(): Boolean = + healthCheckRepository.checkHealth() +} diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuListUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuListUseCase.kt new file mode 100644 index 000000000..7ba917be8 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuListUseCase.kt @@ -0,0 +1,25 @@ +package com.eatssu.android.domain.usecase.menu + +import com.eatssu.android.domain.model.Menu +import com.eatssu.android.domain.repository.MealRepository +import com.eatssu.android.domain.repository.MenuRepository +import com.eatssu.common.enums.MenuType +import com.eatssu.common.enums.Restaurant +import com.eatssu.common.enums.Time +import javax.inject.Inject + +class GetMenuListUseCase @Inject constructor( + private val menuRepository: MenuRepository, + private val mealRepository: MealRepository +) { + suspend operator fun invoke( + restaurant: Restaurant, + menuDate: String, + time: Time + ): List { + return when (restaurant.menuType) { + MenuType.FIXED -> menuRepository.getFixedMenuList(restaurant) + MenuType.VARIABLE -> mealRepository.getTodayMenuList(menuDate, restaurant, time) + } + } +} diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuNameListOfMealUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuNameListOfMealUseCase.kt index 694d5fae9..b4da52aba 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuNameListOfMealUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/menu/GetMenuNameListOfMealUseCase.kt @@ -1,14 +1,12 @@ package com.eatssu.android.domain.usecase.menu -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.domain.repository.MealRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetMenuNameListOfMealUseCase @Inject constructor( private val mealRepository: MealRepository, ) { - suspend operator fun invoke(menuId: Long): Flow> = + suspend operator fun invoke(menuId: Long): List = mealRepository.getMenuInfoByMealId(menuId) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/DeleteReviewUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/DeleteReviewUseCase.kt index dc0e863a8..4a02c6f4d 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/DeleteReviewUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/DeleteReviewUseCase.kt @@ -1,13 +1,11 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.domain.repository.ReviewRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class DeleteReviewUseCase @Inject constructor( private val reviewRepository: ReviewRepository, ) { - suspend operator fun invoke(reviewId: Long): Flow> = + suspend operator fun invoke(reviewId: Long): Boolean = reviewRepository.deleteReview(reviewId) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt index 4561a8998..185368493 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetImageUrlUseCase.kt @@ -1,9 +1,6 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.ImageResponse import com.eatssu.android.domain.repository.ReviewRepository -import kotlinx.coroutines.flow.Flow import java.io.File import javax.inject.Inject @@ -12,6 +9,6 @@ class GetImageUrlUseCase @Inject constructor( ) { suspend operator fun invoke( file: File - ): Flow> = - reviewRepository.getImageString(file) + ): String? = + reviewRepository.getImageString(file)?.url } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewInfoUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewInfoUseCase.kt index 9c270732f..bda5973d9 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewInfoUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewInfoUseCase.kt @@ -1,14 +1,13 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.GetMealReviewInfoResponse +import com.eatssu.android.data.dto.response.asReviewInfo +import com.eatssu.android.domain.model.ReviewInfo import com.eatssu.android.domain.repository.ReviewRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetMealReviewInfoUseCase @Inject constructor( private val reviewRepository: ReviewRepository, ) { - suspend operator fun invoke(mealId: Long): Flow> = - reviewRepository.getMealReviewInfo(mealId) + suspend operator fun invoke(mealId: Long): ReviewInfo? = + reviewRepository.getMealReviewInfo(mealId)?.asReviewInfo() } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewListUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewListUseCase.kt index d8100ea78..ea7fada8c 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewListUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMealReviewListUseCase.kt @@ -1,10 +1,8 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.GetReviewListResponse +import com.eatssu.android.domain.model.Review import com.eatssu.android.domain.repository.ReviewRepository import com.eatssu.common.enums.MenuType -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetMealReviewListUseCase @Inject constructor( @@ -12,6 +10,6 @@ class GetMealReviewListUseCase @Inject constructor( ) { suspend operator fun invoke( mealId: Long?, - ): Flow> = + ): List = reviewRepository.getReviewList(MenuType.VARIABLE.toString(), mealId, 0) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewInfoUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewInfoUseCase.kt index 6664e4e7f..2ce5d18e4 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewInfoUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewInfoUseCase.kt @@ -1,14 +1,13 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.GetMenuReviewInfoResponse +import com.eatssu.android.data.dto.response.asReviewInfo +import com.eatssu.android.domain.model.ReviewInfo import com.eatssu.android.domain.repository.ReviewRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetMenuReviewInfoUseCase @Inject constructor( private val reviewRepository: ReviewRepository, ) { - suspend operator fun invoke(menuId: Long): Flow> = - reviewRepository.getMenuReviewInfo(menuId) + suspend operator fun invoke(menuId: Long): ReviewInfo? = + reviewRepository.getMenuReviewInfo(menuId)?.asReviewInfo() } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewListUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewListUseCase.kt index a36d6ae5e..5cdff3d1a 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewListUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMenuReviewListUseCase.kt @@ -1,10 +1,8 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.GetReviewListResponse +import com.eatssu.android.domain.model.Review import com.eatssu.android.domain.repository.ReviewRepository import com.eatssu.common.enums.MenuType -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetMenuReviewListUseCase @Inject constructor( @@ -12,6 +10,6 @@ class GetMenuReviewListUseCase @Inject constructor( ) { suspend operator fun invoke( menuId: Long?, - ): Flow> = + ): List = reviewRepository.getReviewList(MenuType.FIXED.toString(), 0, menuId) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMyReviewsUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMyReviewsUseCase.kt index f8bb6c37b..c51cd8dd3 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMyReviewsUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/GetMyReviewsUseCase.kt @@ -1,14 +1,12 @@ package com.eatssu.android.domain.usecase.review -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.MyReviewResponse +import com.eatssu.android.domain.model.Review import com.eatssu.android.domain.repository.UserRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetMyReviewsUseCase @Inject constructor( private val userRepository: UserRepository, ) { - suspend operator fun invoke(): Flow> = + suspend operator fun invoke(): List = userRepository.getUserReviews() } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/ModifyReviewUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/ModifyReviewUseCase.kt index a6a3e3de8..e82104ee3 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/ModifyReviewUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/ModifyReviewUseCase.kt @@ -1,9 +1,7 @@ package com.eatssu.android.domain.usecase.review import com.eatssu.android.data.dto.request.ModifyReviewRequest -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.domain.repository.ReviewRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class ModifyReviewUseCase @Inject constructor( @@ -12,6 +10,6 @@ class ModifyReviewUseCase @Inject constructor( suspend operator fun invoke( reviewId: Long, body: ModifyReviewRequest, - ): Flow> = + ): Boolean = reviewRepository.modifyReview(reviewId, body) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/PostReportUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/PostReportUseCase.kt index 4ea5aabbc..7e29cb4c0 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/PostReportUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/PostReportUseCase.kt @@ -1,14 +1,12 @@ package com.eatssu.android.domain.usecase.review import com.eatssu.android.data.dto.request.ReportRequest -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.domain.repository.ReportRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class PostReportUseCase @Inject constructor( private val reportRepository: ReportRepository, ) { - suspend operator fun invoke(body: ReportRequest): Flow> = + suspend operator fun invoke(body: ReportRequest): Boolean = reportRepository.reportReview(body) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/review/WriteReviewUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/review/WriteReviewUseCase.kt index 98c33ad04..97d472f7c 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/review/WriteReviewUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/review/WriteReviewUseCase.kt @@ -1,14 +1,12 @@ package com.eatssu.android.domain.usecase.review import com.eatssu.android.data.dto.request.WriteReviewRequest -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.domain.repository.ReviewRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class WriteReviewUseCase @Inject constructor( private val reviewRepository: ReviewRepository, ) { - suspend operator fun invoke(menuId: Long, body: WriteReviewRequest): Flow> = + suspend operator fun invoke(menuId: Long, body: WriteReviewRequest): Boolean = reviewRepository.writeReview(menuId, body) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt index 7b2302c8e..740b0fb47 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/user/SetUserNicknameUseCase.kt @@ -23,9 +23,9 @@ class SetUserNicknameUseCase @Inject constructor( private val userRepository: UserRepository, @ApplicationContext private val context: Context ) { - suspend operator fun invoke(nickname: String) { + suspend operator fun invoke(nickname: String): Boolean { // 로컬 저장 MySharedPreferences.setUserName(context, nickname) - userRepository.updateUserName(ChangeNicknameRequest(nickname)) + return userRepository.updateUserName(ChangeNicknameRequest(nickname)) } } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/user/ValidateUserNameUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/user/ValidateUserNameUseCase.kt index 43cf7a646..b09f35878 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/user/ValidateUserNameUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/user/ValidateUserNameUseCase.kt @@ -1,14 +1,12 @@ package com.eatssu.android.domain.usecase.user -import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.domain.repository.UserRepository -import kotlinx.coroutines.flow.Flow import javax.inject.Inject class ValidateUserNameUseCase @Inject constructor( private val userRepository: UserRepository, ) { - suspend operator fun invoke(name: String): Flow> = + suspend operator fun invoke(name: String): Boolean = userRepository.checkUserNameValidation(name) } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt index e202b4c5e..94b47a1bb 100644 --- a/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt +++ b/app/src/main/java/com/eatssu/android/domain/usecase/widget/GetTodayMealUseCase.kt @@ -4,8 +4,6 @@ import com.eatssu.android.domain.repository.MealRepository import com.eatssu.android.presentation.widget.WidgetMealList import com.eatssu.common.enums.Restaurant import com.eatssu.common.enums.Time -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first import timber.log.Timber import java.net.UnknownHostException import java.nio.channels.UnresolvedAddressException @@ -42,19 +40,16 @@ class GetTodayMealUseCase @Inject constructor( date: String, restaurant: String ): MealState = runCatching { - val breakfastFlow = mealRepository.getTodayMeal(date, restaurant, Time.MORNING.name) - val lunchFlow = mealRepository.getTodayMeal(date, restaurant, Time.LUNCH.name) - val dinnerFlow = mealRepository.getTodayMeal(date, restaurant, Time.DINNER.name) + val breakfastList = mealRepository.getTodayMeal(date, restaurant, Time.MORNING.name) + val lunchList = mealRepository.getTodayMeal(date, restaurant, Time.LUNCH.name) + val dinnerList = mealRepository.getTodayMeal(date, restaurant, Time.DINNER.name) - combine(breakfastFlow, lunchFlow, dinnerFlow) { breakfastList, lunchList, dinnerList -> - - WidgetMealList( - breakfast = (breakfastList to "breakfast"), - lunch = (lunchList to "lunch"), - dinner = (dinnerList to "dinner"), - restaurant = Restaurant.valueOf(restaurant) - ) - }.first() // 여기서 Flow 실행 + WidgetMealList( + breakfast = (breakfastList to "breakfast"), + lunch = (lunchList to "lunch"), + dinner = (dinnerList to "dinner"), + restaurant = Restaurant.valueOf(restaurant) + ) }.fold( onSuccess = { result -> Timber.d("메뉴 가져오기 성공 $result") diff --git a/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt index fd73c8981..d38909875 100644 --- a/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt @@ -13,14 +13,12 @@ import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase import com.eatssu.android.domain.usecase.user.SetUserCollegeDepartmentUseCase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.time.LocalDate import javax.inject.Inject @@ -58,35 +56,26 @@ class MainViewModel @Inject constructor( private fun fetchAndCheckNickname() { viewModelScope.launch { _uiState.value = UiState.Loading - runCatching { - withContext(Dispatchers.IO) { getUserNickNameUseCase() } - }.onSuccess { nickname -> - // 1) 닉네임 없음/기본 프리셋 - if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { - _uiState.value = UiState.Success(MainState.NicknameNull) - _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) - return@launch // ← 아래 분기 실행 막기 - } - - // 2) 정상 닉네임 - _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) - _uiEvent.emit( - UiEvent.ShowToast( - String.format( - context.getString(R.string.hello_user), - nickname - ) - ) - ) - }.onFailure { e -> - _uiState.value = UiState.Error - _uiEvent.emit( - UiEvent.ShowToast( - context.getString(R.string.not_found) + + val nickname = getUserNickNameUseCase() + + // 1) 닉네임 없음/기본 프리셋 + if (nickname.isBlank() || nickname.startsWith("user-")) { + _uiState.value = UiState.Success(MainState.NicknameNull) + _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname))) + return@launch + } + + // 2) 정상 닉네임 + _uiState.value = UiState.Success(MainState.NicknameExists(nickname)) + _uiEvent.emit( + UiEvent.ShowToast( + String.format( + context.getString(R.string.hello_user), + nickname ) ) - Timber.e(e) - } + ) } } @@ -112,25 +101,21 @@ class MainViewModel @Inject constructor( private fun getUserDepartment() { viewModelScope.launch { - runCatching { - userRepository.getUserCollegeDepartment() - }.onSuccess { it -> - val college = it.first - val department = it.second - setUserCollegeDepartmentUseCase(college, department) - - _uiState.value = UiState.Success( - MainState.DepartmentState( - departmentName = department.departmentName, - showUserDepartmentBottomSheet = - (college.collegeId == -1 || department.departmentId == -1) - ) - ) - }.onFailure { e -> - Timber.e("getUserDepartment failed: ${e.message}") + val (college, department) = userRepository.getUserCollegeDepartment() ?: run { _uiState.value = UiState.Error _uiEvent.emit(UiEvent.ShowToast("정보를 불러올 수 없습니다.")) + return@launch } + + setUserCollegeDepartmentUseCase(college, department) + + _uiState.value = UiState.Success( + MainState.DepartmentState( + departmentName = department.departmentName, + showUserDepartmentBottomSheet = + (college.collegeId == -1 || department.departmentId == -1) + ) + ) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/UiState.kt b/app/src/main/java/com/eatssu/android/presentation/UiState.kt index 4a2f528f6..8f9df0298 100644 --- a/app/src/main/java/com/eatssu/android/presentation/UiState.kt +++ b/app/src/main/java/com/eatssu/android/presentation/UiState.kt @@ -6,7 +6,7 @@ sealed interface UiState { object Loading : UiState data class Success( - val data: T? = null, + val data: T, ) : UiState object Error : UiState diff --git a/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt b/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt index 3f1760db6..73cd2428a 100644 --- a/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/base/BaseActivity.kt @@ -25,6 +25,7 @@ import com.eatssu.android.presentation.common.NetworkConnection import com.eatssu.android.presentation.common.VersionViewModel import com.eatssu.android.presentation.common.VersionViewModelFactory import com.eatssu.android.presentation.login.LoginActivity +import com.eatssu.android.presentation.util.observeNetworkError import com.eatssu.common.EventLogger import com.eatssu.common.enums.ScreenId import com.google.android.material.card.MaterialCardView @@ -60,7 +61,7 @@ abstract class BaseActivity( toolbar = findViewById(R.id.toolbar) toolbarTitle = findViewById(R.id.toolbar_title) - backBtn =findViewById(R.id.mcv_setting) + backBtn = findViewById(R.id.mcv_setting) setSupportActionBar(toolbar) supportActionBar?.setDisplayShowTitleEnabled(false) // 툴바 기본 제목 비활성화 @@ -72,9 +73,12 @@ abstract class BaseActivity( networkCheck.register() // 네트워크 객체 등록 firebaseRemoteConfigRepository = FirebaseRemoteConfigRepository() - versionViewModel = ViewModelProvider(this, VersionViewModelFactory(firebaseRemoteConfigRepository))[VersionViewModel::class.java] + versionViewModel = ViewModelProvider( + this, + VersionViewModelFactory(firebaseRemoteConfigRepository) + )[VersionViewModel::class.java] - if(versionViewModel.checkForceUpdate()){ + if (versionViewModel.checkForceUpdate()) { showForceUpdateDialog() } @@ -119,20 +123,26 @@ abstract class BaseActivity( private fun observeTokenExpiration() { lifecycleScope.launch { TokenEventBus.tokenExpired.collect { - Toast.makeText(this@BaseActivity, - getString(R.string.token_expired), Toast.LENGTH_SHORT).show() + Toast.makeText( + this@BaseActivity, + getString(R.string.token_expired), Toast.LENGTH_SHORT + ).show() navigateToLogin() } } lifecycleScope.launch { TokenEventBus.tokenServerError.collect { - Toast.makeText(this@BaseActivity, - getString(R.string.token_server_error), Toast.LENGTH_SHORT) + Toast.makeText( + this@BaseActivity, + getString(R.string.token_server_error), Toast.LENGTH_SHORT + ) .show() navigateToLogin() } } + + observeNetworkError() } private fun navigateToLogin() { @@ -182,7 +192,5 @@ abstract class BaseActivity( } } - open fun shouldLogScreenId(): Boolean { - return true - } + open fun shouldLogScreenId(): Boolean = true } diff --git a/app/src/main/java/com/eatssu/android/presentation/base/NetworkErrorEventBus.kt b/app/src/main/java/com/eatssu/android/presentation/base/NetworkErrorEventBus.kt new file mode 100644 index 000000000..7cdd559a9 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/base/NetworkErrorEventBus.kt @@ -0,0 +1,19 @@ +package com.eatssu.android.presentation.base + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import timber.log.Timber + +/** application에서 발생하는 네트워크 에러 이벤트를 전달하기 위한 Bus */ +object NetworkErrorEventBus { + private val _networkError = MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val networkError = _networkError + + fun notifyNetworkError() { + Timber.e("NetworkErrorEventBus → Network error occurred") + _networkError.tryEmit(Unit) + } +} diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuAdapter.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuAdapter.kt index a4e3760ff..b571f9819 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuAdapter.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuAdapter.kt @@ -14,8 +14,7 @@ import com.eatssu.android.presentation.cafeteria.info.InfoBottomSheetFragment class MenuAdapter( private val fragmentManager: FragmentManager, - private val totalMenuList: ArrayList
- + private val sectionList: List
) : RecyclerView.Adapter() { class MyViewHolder( @@ -60,11 +59,11 @@ class MenuAdapter( } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { - totalMenuList[position].let { sectionModel -> + sectionList[position].let { sectionModel -> holder.bind(fragmentManager, sectionModel) } } - override fun getItemCount(): Int = totalMenuList.size + override fun getItemCount(): Int = sectionList.size } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt index 6c7320290..586b43401 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuFragment.kt @@ -1,29 +1,28 @@ package com.eatssu.android.presentation.cafeteria.menu +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager -import com.eatssu.android.data.dto.response.mapFixedMenuResponseToMenu -import com.eatssu.android.data.dto.response.mapTodayMenuResponseToMenu import com.eatssu.android.databinding.FragmentMenuBinding import com.eatssu.android.domain.model.Section import com.eatssu.android.presentation.MainViewModel import com.eatssu.android.presentation.UiState import com.eatssu.android.presentation.cafeteria.info.InfoViewModel -import com.eatssu.common.enums.MenuType import com.eatssu.common.enums.Restaurant import com.eatssu.common.enums.Time import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import timber.log.Timber import java.time.DayOfWeek import java.time.format.DateTimeFormatter @@ -36,28 +35,25 @@ class MenuFragment : Fragment() { private val infoViewModel by activityViewModels() private val menuViewModel by viewModels() - val foodCourtDataLoaded = MutableLiveData() - val snackCornerDataLoaded = MutableLiveData() - val haksikDataLoaded = MutableLiveData() - val dodamDataLoaded = MutableLiveData() - val dormitoryDataLoaded = MutableLiveData() - val facultyDataLoaded = MutableLiveData() - - private val totalMenuList = ArrayList
() + private val time: Time by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + arguments?.getSerializable(ARG_TIME, Time::class.java) ?: Time.LUNCH + } else { + @Suppress("DEPRECATION") + arguments?.getSerializable(ARG_TIME) as? Time ?: Time.LUNCH + } + } companion object { + private const val ARG_TIME = "ARG_TIME" + fun newInstance(time: Time): MenuFragment { - val fragment = MenuFragment() - val args = Bundle() - args.putSerializable("TIME", time) - fragment.arguments = args - return fragment + return MenuFragment().apply { + arguments = bundleOf(ARG_TIME to time) + } } } - private val time: Time - get() = arguments?.getSerializable("TIME") as Time //Todo deprecated - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -70,10 +66,8 @@ class MenuFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // StateFlow 수집은 단 1번만 실행 - collectMealData() - collectFixedMenuData() - collectUiState() + // UiState 관찰 + observeUiState() // 날짜 바뀔 때마다 ViewModel API 호출 observeViewModel() @@ -85,190 +79,55 @@ class MenuFragment : Fragment() { val menuDate = dataReceived.format(DateTimeFormatter.ofPattern("yyyyMMdd")) val dayOfWeek = dataReceived.dayOfWeek - if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY && time == Time.LUNCH) { - menuViewModel.loadFixedMenu(Restaurant.FOOD_COURT) - menuViewModel.loadFixedMenu(Restaurant.SNACK_CORNER) - } else { - foodCourtDataLoaded.value = true - snackCornerDataLoaded.value = true - checkDataLoaded() - } - - if (time != Time.LUNCH) { - foodCourtDataLoaded.value = true - snackCornerDataLoaded.value = true - checkDataLoaded() - } - - menuViewModel.loadTodayMeal(menuDate, Restaurant.HAKSIK, time) - menuViewModel.loadTodayMeal(menuDate, Restaurant.DODAM, time) - menuViewModel.loadTodayMeal(menuDate, Restaurant.DORMITORY, time) - menuViewModel.loadTodayMeal(menuDate, Restaurant.FACULTY, time) - } - } + // 로딩할 식당 목록 결정 + val restaurantsToLoad = buildList { + // 변동 메뉴 식당 + addAll(Restaurant.getVariableRestaurantList()) - private fun collectMealData() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.todayMealDataHaksik.collect { result -> - totalMenuList.removeAll { it.cafeteria == Restaurant.HAKSIK } - if (result.isNotEmpty()) { - totalMenuList.add( - Section( - MenuType.VARIABLE, Restaurant.HAKSIK, - result.mapTodayMenuResponseToMenu(), - infoViewModel.getRestaurantInfo(Restaurant.HAKSIK)?.location ?: "" - ) - ) - } - haksikDataLoaded.value = true - checkDataLoaded() + // 고정 메뉴 식당 (평일 점심만) + if (dayOfWeek != DayOfWeek.SATURDAY && dayOfWeek != DayOfWeek.SUNDAY && time == Time.LUNCH) { + add(Restaurant.FOOD_COURT) + add(Restaurant.SNACK_CORNER) } } - } - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.todayMealDataDodam.collect { result -> - totalMenuList.removeAll { it.cafeteria == Restaurant.DODAM } - if (result.isNotEmpty()) { - totalMenuList.add( - Section( - MenuType.VARIABLE, Restaurant.DODAM, - result.mapTodayMenuResponseToMenu(), - infoViewModel.getRestaurantInfo(Restaurant.DODAM)?.location ?: "" - ) - ) - } - dodamDataLoaded.value = true - checkDataLoaded() - } - } - } + Timber.d("Loading menus for date: $menuDate, time: $time, restaurants: $restaurantsToLoad") - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.todayMealDataDormitory.collect { result -> - totalMenuList.removeAll { it.cafeteria == Restaurant.DORMITORY } - if (result.isNotEmpty()) { - totalMenuList.add( - Section( - MenuType.VARIABLE, - Restaurant.DORMITORY, - result.mapTodayMenuResponseToMenu(), - infoViewModel.getRestaurantInfo(Restaurant.DORMITORY)?.location ?: "" - ) - ) - } - dormitoryDataLoaded.value = true - checkDataLoaded() - } - } - } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.todayMealDataFaculty.collect { result -> - totalMenuList.removeAll { it.cafeteria == Restaurant.FACULTY } - if (result.isNotEmpty()) { - totalMenuList.add( - Section( - MenuType.VARIABLE, Restaurant.FACULTY, - result.mapTodayMenuResponseToMenu(), - infoViewModel.getRestaurantInfo(Restaurant.FACULTY)?.location ?: "" - ) - ) - } - facultyDataLoaded.value = true - checkDataLoaded() - } - } + // 메뉴 로딩 + menuViewModel.loadMenus(restaurantsToLoad, menuDate, time) } } - private fun collectFixedMenuData() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.fixedMenuDataFood.collect { result -> - totalMenuList.removeAll { it.cafeteria == Restaurant.FOOD_COURT } - if (result.mapFixedMenuResponseToMenu().isNotEmpty()) { - totalMenuList.add( - Section( - MenuType.FIXED, - Restaurant.FOOD_COURT, - result.mapFixedMenuResponseToMenu(), - infoViewModel.getRestaurantInfo(Restaurant.FOOD_COURT)?.location ?: "" - ) + private fun observeUiState() = lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + menuViewModel.uiState.collect { state -> + if (state !is UiState.Success) return@collect + + val menuMap = state.data.menuMap + Timber.d("Menu map received: $menuMap") + + val sectionList = menuMap + .filter { (_, menuList) -> menuList.isNotEmpty() } + .map { (restaurant, menuList) -> + Section( + restaurant.menuType, + restaurant, + menuList, + infoViewModel.getRestaurantInfo(restaurant)?.location ?: "" ) } - foodCourtDataLoaded.value = true - checkDataLoaded() - } - } - } + .sortedBy { it.cafeteria.ordinal } - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.fixedMenuDataSnack.collect { result -> - totalMenuList.removeAll { it.cafeteria == Restaurant.SNACK_CORNER } - if (result.mapFixedMenuResponseToMenu().isNotEmpty()) { - totalMenuList.add( - Section( - MenuType.FIXED, - Restaurant.SNACK_CORNER, - result.mapFixedMenuResponseToMenu(), - infoViewModel.getRestaurantInfo(Restaurant.SNACK_CORNER)?.location ?: "" - ) - ) - } - snackCornerDataLoaded.value = true - checkDataLoaded() - } + setupRecyclerView(sectionList) } } } - private fun setupTodayRecyclerView() { + private fun setupRecyclerView(sectionList: List
) { binding.rv.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) - adapter = fragmentManager?.let { MenuAdapter(it, totalMenuList) } - } - } - - private fun checkDataLoaded() { - if (foodCourtDataLoaded.value == true && - snackCornerDataLoaded.value == true && - haksikDataLoaded.value == true && - dodamDataLoaded.value == true && - dormitoryDataLoaded.value == true && - facultyDataLoaded.value == true - ) { - totalMenuList.sortBy { it.cafeteria.ordinal } - setupTodayRecyclerView() - } - } - - private fun collectUiState() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.STARTED) { - menuViewModel.uiState.collect { state -> - when (state) { - is UiState.Init -> { - // init - } - is UiState.Loading -> { - // Loading - } - is UiState.Success -> { - // Success - } - is UiState.Error -> { - // Error - } - } - } - } + adapter = MenuAdapter(getParentFragmentManager(), sectionList) } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuViewModel.kt index 9879f9990..5d0bae676 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/menu/MenuViewModel.kt @@ -2,158 +2,46 @@ package com.eatssu.android.presentation.cafeteria.menu import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.data.dto.response.GetFixedMenuResponse -import com.eatssu.android.data.dto.response.GetMealResponse -import com.eatssu.android.data.service.MealService -import com.eatssu.android.data.service.MenuService -import com.eatssu.android.domain.model.MenuMini +import com.eatssu.android.domain.model.Menu +import com.eatssu.android.domain.usecase.menu.GetMenuListUseCase import com.eatssu.android.presentation.UiState import com.eatssu.common.enums.Restaurant import com.eatssu.common.enums.Time import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import timber.log.Timber import javax.inject.Inject @HiltViewModel class MenuViewModel @Inject constructor( - private val menuService: MenuService, - private val mealService: MealService, -) :ViewModel() { - - private val _todayMealDataDodam = MutableStateFlow>(arrayListOf()) - val todayMealDataDodam: StateFlow> = _todayMealDataDodam - - private val _todayMealDataHaksik = MutableStateFlow>(arrayListOf()) - val todayMealDataHaksik: StateFlow> = _todayMealDataHaksik - - private val _todayMealDataDormitory = - MutableStateFlow>(arrayListOf()) - val todayMealDataDormitory: StateFlow> = _todayMealDataDormitory - - private val _todayMealDataFaculty = - MutableStateFlow>(arrayListOf()) - val todayMealDataFaculty: StateFlow> = _todayMealDataFaculty - - private val _fixedMenuDataSnack = - MutableStateFlow(GetFixedMenuResponse(arrayListOf())) - val fixedMenuDataSnack: StateFlow = _fixedMenuDataSnack - - private val _fixedMenuDataKitchen = - MutableStateFlow(GetFixedMenuResponse(arrayListOf())) - val fixedMenuDataKitchen: StateFlow = _fixedMenuDataKitchen - - private val _fixedMenuDataFood = - MutableStateFlow(GetFixedMenuResponse(arrayListOf())) - val fixedMenuDataFood: StateFlow = _fixedMenuDataFood + private val getMenuListUseCase: GetMenuListUseCase, +) : ViewModel() { private val _uiState: MutableStateFlow> = MutableStateFlow(UiState.Init) val uiState: StateFlow> = _uiState.asStateFlow() - - fun loadTodayMeal( - menuDate: String, - restaurantType: Restaurant, - time: Time, - ) { + // 주어진 식당 리스트에 대해 메뉴 정보를 비동기로 가져와서 UI 상태를 업데이트 + fun loadMenus(restaurants: List, menuDate: String, time: Time) { _uiState.value = UiState.Loading - Timber.d("Debug", "loadTodayMeal called with type: $restaurantType") viewModelScope.launch { - mealService.getTodayMeal(menuDate, restaurantType.toString(), time.toString()) - .enqueue(object : Callback>> { - override fun onResponse( - call: Call>>, - response: Response>>, - ) { - val restaurantMenuData = response.body()?.result ?: arrayListOf() - - if (response.isSuccessful) { - Timber.d("onResponse 성공" + response.body()) - - when (restaurantType) { - Restaurant.HAKSIK -> _todayMealDataHaksik.value = restaurantMenuData - Restaurant.DODAM -> _todayMealDataDodam.value = restaurantMenuData - Restaurant.DORMITORY -> _todayMealDataDormitory.value = restaurantMenuData - Restaurant.FACULTY -> _todayMealDataFaculty.value = restaurantMenuData - else -> Timber.d("onResponse 실패. 잘못된 식당입니다.") - } - _uiState.value = UiState.Success(MenuState()) - - } else { - Timber.d("onResponse 실패 투데이밀" + response.code() + response.message()) - _uiState.value = UiState.Error - } - } - - override fun onFailure( - call: Call>>, - t: Throwable, - ) { - Timber.d("onFailure 에러: 나다${t.message}+ ${call}" + "ddd") - _uiState.value = UiState.Error - } - }) - } - } - - // Fixed Menu 데이터 로드도 유사한 방식으로 구현 - fun loadFixedMenu(restaurantType: Restaurant) { - Timber.d("Debug", "loadFixedMenu called with type: $restaurantType") - - _uiState.value = UiState.Loading - - viewModelScope.launch { - menuService.getFixMenu(restaurantType.toString()) - .enqueue(object : Callback> { - override fun onResponse( - call: Call>, - response: Response>, - ) { - if (response.isSuccessful) { - Timber.d("onResponse 성공" + response.body()) - val data = - response.body()?.result ?: GetFixedMenuResponse(arrayListOf()) - when (restaurantType) { - Restaurant.THE_KITCHEN -> _fixedMenuDataKitchen.value = data - Restaurant.FOOD_COURT -> _fixedMenuDataFood.value = data - Restaurant.SNACK_CORNER -> _fixedMenuDataSnack.value = data - - else -> { - Timber.d("onResponse 실패. 잘못된 식당 입니다.") - } - } - _uiState.value = UiState.Success(MenuState()) - } else { - Timber.d("onResponse 실패") - _uiState.value = UiState.Error - } - } - - override fun onFailure( - call: Call>, - t: Throwable, - ) { - Timber.d("onFailure 에러: ${t.message}") - _uiState.value = UiState.Error - } - }) + // async 함수로 Deferred를 만들어 메뉴 정보 한번에 가져오기 + val deferredMenus = restaurants.map { restaurant -> + async { + restaurant to getMenuListUseCase(restaurant, menuDate, time) + } + } + + val menuMap = deferredMenus.awaitAll().toMap() + _uiState.value = UiState.Success(MenuState(menuMap)) } } } data class MenuState( - var haksikMeal: ArrayList? = null, - var dodamMeal: ArrayList? = null, - var dormitoryMeal: ArrayList? = null, - var snackMenu: GetFixedMenuResponse? = null, - var foodcourtMenu: GetFixedMenuResponse? = null, - var menuOfMeal: List? = null, + val menuMap: Map> = emptyMap() ) \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewViewModel.kt index 3aca37e79..d84a320cb 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/list/ReviewViewModel.kt @@ -2,8 +2,6 @@ package com.eatssu.android.presentation.cafeteria.review.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.eatssu.android.data.dto.response.asReviewInfo -import com.eatssu.android.data.dto.response.toReviewList import com.eatssu.android.domain.model.Review import com.eatssu.android.domain.model.ReviewInfo import com.eatssu.android.domain.usecase.review.DeleteReviewUseCase @@ -16,10 +14,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -38,10 +32,7 @@ class ReviewViewModel @Inject constructor( private val _uiState: MutableStateFlow = MutableStateFlow(ReviewState()) val uiState: StateFlow = _uiState.asStateFlow() - fun loadReview( - menuType: String, - itemId: Long, - ) { + fun loadReview(menuType: String, itemId: Long) { when (menuType) { MenuType.FIXED.name -> { callMenuReviewInfo(itemId) @@ -63,193 +54,83 @@ class ReviewViewModel @Inject constructor( private fun callMenuReviewInfo(menuId: Long) { viewModelScope.launch { - getMenuReviewInfoUseCase(menuId).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { - it.copy( - loading = false, - error = false, - ) - } - Timber.d(e.toString()) - }.collectLatest { result -> - result.result?.apply { - if (mainRating == null) { - _uiState.update { - it.copy( - loading = false, - error = false, - reviewInfo = asReviewInfo(), - isEmpty = true - ) - } - } else { - _uiState.update { - it.copy( - loading = false, - error = false, - reviewInfo = asReviewInfo(), - isEmpty = false - ) - } - Timber.d("리뷰 있다") - } - } + _uiState.update { it.copy(loading = true) } + + val menuReviewInfo = getMenuReviewInfoUseCase(menuId) + _uiState.update { + it.copy( + loading = false, + error = false, + reviewInfo = menuReviewInfo, + isEmpty = menuReviewInfo == null, + ) } } } - private fun callMealReviewInfo( - mealId: Long, - ) { + private fun callMealReviewInfo(mealId: Long) { viewModelScope.launch { - getMealReviewInfoUseCase(mealId).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { - it.copy( - loading = false, - error = false, - ) - } - Timber.e(e.toString()) - }.collectLatest { result -> - result.result?.apply { - if (mainRating == null) { - _uiState.update { - it.copy( - loading = false, - error = false, - reviewInfo = asReviewInfo(), - isEmpty = true - ) - } - } else { - _uiState.update { - it.copy( - loading = false, - error = false, - reviewInfo = asReviewInfo(), - isEmpty = false - ) - } - Timber.d("리뷰 있다") - } - } + _uiState.update { it.copy(loading = true) } + + val mealReviewInfo = getMealReviewInfoUseCase(mealId) + _uiState.update { + it.copy( + loading = false, + error = false, + reviewInfo = mealReviewInfo, + isEmpty = mealReviewInfo == null, + ) } } } - private fun callMenuReviewList( - itemId: Long, - ) { + private fun callMenuReviewList(itemId: Long) { viewModelScope.launch { - getMenuReviewListUseCase(itemId).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { - it.copy( - loading = false, - error = true, - ) - } - Timber.e(e.toString()) - }.collectLatest { result -> - result.result?.apply { - if (numberOfElements == 0) { //리뷰 없음 - _uiState.update { - it.copy( - loading = false, - error = false, - isEmpty = true - ) - } - } else { //리뷰 있음 - _uiState.update { - it.copy( - loading = false, - error = false, - reviewList = this.toReviewList(), - isEmpty = false - ) - } - } - } + val menuReviewList = getMenuReviewListUseCase(itemId) + _uiState.update { + it.copy( + loading = false, + error = false, + reviewList = menuReviewList, + isEmpty = menuReviewList.isEmpty() + ) } } } - private fun callMealReviewList( - itemId: Long, - ) { + private fun callMealReviewList(itemId: Long) { viewModelScope.launch { - getMealReviewListUseCase(itemId).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { - it.copy( - loading = false, - error = false, - ) - } - Timber.e(e.toString()) - }.collectLatest { result -> - result.result?.apply { - if (numberOfElements == 0) { //리뷰 없음 - _uiState.update { - it.copy( - loading = false, - error = false, - isEmpty = true - ) - } - } else { //리뷰 있음 - _uiState.update { - it.copy( - loading = false, - error = false, - reviewList = this.toReviewList(), - isEmpty = false - ) - } - } - } + val reviewList = getMealReviewListUseCase(itemId) + _uiState.update { + it.copy( + loading = false, + error = false, + reviewList = reviewList, + isEmpty = reviewList.isEmpty() + ) } } } fun deleteReview(reviewId: Long) { viewModelScope.launch { - deleteReviewUseCase(reviewId).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> + _uiState.update { it.copy(loading = true) } + + val success = deleteReviewUseCase(reviewId) + if (!success) { _uiState.update { it.copy( error = true, -// toastMessage = context.getString(R.string.delete_not) ) } - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) + return@launch + } - _uiState.update { - it.copy( -// isDeleted = true, -// toastMessage = context.getString(R.string.delete_done) - ) - } + _uiState.update { + it.copy( + loading = false, + error = false, + ) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt index 04c3a85f0..385543b9c 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/modify/ModifyViewModel.kt @@ -10,13 +10,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -32,30 +27,28 @@ class ModifyViewModel @Inject constructor( body: ModifyReviewRequest, ) { viewModelScope.launch { - modifyReviewUseCase(reviewId, body).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> + _uiState.update { it.copy(loading = true) } + + val success = modifyReviewUseCase(reviewId, body) + if (!success) { _uiState.update { it.copy( loading = false, - error = false, - isDone = true, + error = true, + isDone = false, toastMessage = App.appContext.getString(R.string.modify_not) ) } - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) - _uiState.update { - it.copy( - loading = false, - error = false, - isDone = true, - toastMessage = App.appContext.getString(R.string.modify_done) - ) - } + return@launch + } + + _uiState.update { + it.copy( + loading = false, + error = false, + isDone = true, + toastMessage = App.appContext.getString(R.string.modify_done) + ) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportViewModel.kt index 1c4bbff92..a3c9ffcf2 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/report/ReportViewModel.kt @@ -1,19 +1,13 @@ package com.eatssu.android.presentation.cafeteria.review.report -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eatssu.android.data.dto.request.ReportRequest import com.eatssu.android.domain.usecase.review.PostReportUseCase -import com.eatssu.android.presentation.mypage.userinfo.UserInfoViewModel.Companion.TAG import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -31,15 +25,27 @@ class ReportViewModel fun postData(reviewId: Long, reportType: String, content: String) { viewModelScope.launch { - postReportUseCase(ReportRequest(reviewId, reportType, content)).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { it.copy(error = true, toastMessage = "신고가 실패하였습니다.") } - Log.e(TAG, e.toString()) - }.collectLatest { result -> - _uiState.update { it.copy(isDone = true, toastMessage = "신고가 완료되었습니다.") } + _uiState.update { it.copy(loading = true) } + + val success = postReportUseCase(ReportRequest(reviewId, reportType, content)) + if (!success) { + _uiState.update { + it.copy( + loading = false, + error = true, + toastMessage = "신고가 실패하였습니다." + ) + } + return@launch + } + + _uiState.update { + it.copy( + loading = false, + error = false, + isDone = true, + toastMessage = "신고가 완료되었습니다." + ) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt index b896f56da..9b888a6cd 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/ReviewWriteViewModel.kt @@ -12,13 +12,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -import timber.log.Timber import java.io.File import javax.inject.Inject @@ -29,7 +23,7 @@ class UploadReviewViewModel @Inject constructor( private val getImageUrlUseCase: GetImageUrlUseCase, ) : ViewModel() { - private val _uiState = MutableStateFlow>(UiState.Init) + private val _uiState = MutableStateFlow>(UiState.Init) val uiState = _uiState.asStateFlow() private val _uiEvent: MutableSharedFlow = MutableSharedFlow() @@ -37,35 +31,31 @@ class UploadReviewViewModel @Inject constructor( fun postReview(menuId: Long, reviewData: WriteReviewRequest) { viewModelScope.launch { - writeReviewUseCase(menuId, reviewData) - .onStart { - _uiState.value = UiState.Loading - } - .catch { e -> - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("리뷰 작성에 실패하였습니다.")) - Timber.e(e) - } - .collectLatest { - _uiState.value = UiState.Success() - _uiEvent.emit(UiEvent.ShowToast("리뷰가 작성되었습니다.")) - } + _uiState.value = UiState.Loading + val success = writeReviewUseCase(menuId, reviewData) + + if (!success) { + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast("리뷰 작성에 실패하였습니다.")) + return@launch + } + + _uiState.value = UiState.Success(Unit) + _uiEvent.emit(UiEvent.ShowToast("리뷰가 작성되었습니다.")) } } suspend fun saveS3(file: File): String? { - return getImageUrlUseCase(file) - .onStart { - _uiState.value = UiState.Loading - } - .catch { e -> - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("이미지 업로드에 실패하였습니다.")) - Timber.e(e) - } - .map { it.result?.url } - .firstOrNull() + _uiState.value = UiState.Loading + val url = getImageUrlUseCase(file) + + if (url == null) { + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast("이미지 업로드에 실패하였습니다.")) + return null + } + + _uiState.value = UiState.Success(Unit) + return url } } - -sealed class UploadReviewState diff --git a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/menu/VariableMenuViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/menu/VariableMenuViewModel.kt index 751523ce7..8696288a7 100644 --- a/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/menu/VariableMenuViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/cafeteria/review/write/menu/VariableMenuViewModel.kt @@ -3,17 +3,13 @@ package com.eatssu.android.presentation.cafeteria.review.write.menu import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.eatssu.android.data.dto.response.toMenuMiniList +import com.eatssu.android.data.dto.response.toMenuMini import com.eatssu.android.domain.model.MenuMini import com.eatssu.android.domain.usecase.menu.GetMenuNameListOfMealUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -31,35 +27,16 @@ class VariableMenuViewModel @Inject constructor( fun findMenuItemByMealId(mealId: Long) { Timber.d("findMenuItemByMealId: $mealId") viewModelScope.launch { - getMenuNameListUseCase( - mealId - ).onStart { - Timber.d("findMenuItemByMealId: onStart") - - _uiState.update { it.copy(loading = true) } - }.onCompletion { - Timber.d("findMenuItemByMealId: onCompletion") - - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - Timber.d("findMenuItemByMealId: catch $e") - - _uiState.update { - it.copy( - loading = false, - error = true, - ) - } - }.collectLatest { result -> - Timber.d("findMenuItemByMealId: ${result.toString()}") - _uiState.update { - it.copy( - loading = false, - error = false, - menuOfMeal = result.result?.toMenuMiniList() - ) - } + _uiState.update { it.copy(loading = true) } + val menuNameList = getMenuNameListUseCase(mealId) + _uiState.update { + it.copy( + loading = false, + error = false, + menuOfMeal = menuNameList.map { menuInfo -> menuInfo.toMenuMini() }) } + + Timber.d("findMenuItemByMealId: $menuNameList") } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/error/ServerErrorActivity.kt b/app/src/main/java/com/eatssu/android/presentation/error/ServerErrorActivity.kt new file mode 100644 index 000000000..2f0897e4d --- /dev/null +++ b/app/src/main/java/com/eatssu/android/presentation/error/ServerErrorActivity.kt @@ -0,0 +1,40 @@ +package com.eatssu.android.presentation.error + +import android.app.AlertDialog +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import com.eatssu.android.R + +class ServerErrorActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_server_error) + + val title = intent.getStringExtra(EXTRA_TITLE) ?: getString(R.string.server_error_title) + val message = + intent.getStringExtra(EXTRA_MESSAGE) ?: getString(R.string.server_error_message) + + showServerErrorDialog(title, message) + } + + private fun showServerErrorDialog(title: String, message: String) { + AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(getString(R.string.confirm)) { _, _ -> + finishAffinity() + } + .setCancelable(false) + .create() + .show() + } + + companion object { + const val EXTRA_TITLE = "extra_title" + const val EXTRA_MESSAGE = "extra_message" + } +} + diff --git a/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt index e4d40fc4f..84c5704f0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/intro/IntroActivity.kt @@ -10,6 +10,7 @@ import com.eatssu.android.presentation.MainActivity import com.eatssu.android.presentation.UiEvent import com.eatssu.android.presentation.UiState import com.eatssu.android.presentation.login.LoginActivity +import com.eatssu.android.presentation.util.observeNetworkError import com.eatssu.android.presentation.util.showToast import com.eatssu.android.presentation.util.startActivity import com.eatssu.common.EventLogger @@ -32,6 +33,11 @@ class IntroActivity : AppCompatActivity() { setContentView(binding.root) log() + observeState() + observeEvents() + } + + private fun observeState() { lifecycleScope.launch { introViewModel.uiState.collectLatest { state -> when (state) { @@ -49,16 +55,21 @@ class IntroActivity : AppCompatActivity() { else -> Unit } } + } + } + private fun observeEvents() { + lifecycleScope.launch { introViewModel.uiEvent.collectLatest { event -> when (event) { is UiEvent.ShowToast -> { - // 에러 메시지 표시 showToast(event.message) } } } } + + observeNetworkError() } private fun log() { diff --git a/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt index f23d63b2b..a7a2372e0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/intro/IntroViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eatssu.android.domain.usecase.auth.GetAccessTokenUseCase import com.eatssu.android.domain.usecase.auth.GetIsAccessTokenValidUseCase +import com.eatssu.android.domain.usecase.health.HealthCheckUseCase import com.eatssu.android.presentation.UiEvent import com.eatssu.android.presentation.UiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -17,8 +18,9 @@ import javax.inject.Inject @HiltViewModel class IntroViewModel @Inject constructor( + private val healthCheckUseCase: HealthCheckUseCase, private val getAccessTokenUseCase: GetAccessTokenUseCase, - private val getIsAccessTokenValidUseCase: GetIsAccessTokenValidUseCase + private val getIsAccessTokenValidUseCase: GetIsAccessTokenValidUseCase, ) : ViewModel() { private val _uiState: MutableStateFlow> = MutableStateFlow(UiState.Init) @@ -33,34 +35,31 @@ class IntroViewModel @Inject constructor( private fun autoLogin() { viewModelScope.launch { - val userAccessToken = getAccessTokenUseCase() - _uiState.value = UiState.Loading - try { - // 토큰 존재 여부 확인 - if (userAccessToken.isEmpty()) { - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다")) - return@launch - } else { - checkValid(userAccessToken) - } - } catch (e: Exception) { + // 서버와 통신 가능한지 먼저 확인 + if (!healthCheckUseCase()) { + // 아무 State 처리 없이 Return해도 NetworkErrorEventBus로 인해 오류 페이지로 이동 + return@launch + } + + val userAccessToken = getAccessTokenUseCase() + if (userAccessToken.isEmpty()) { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("오류가 발생했습니다: ${e.message}")) + _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다")) + return@launch } - } - } - private fun checkValid(userAccessToken: String) { - viewModelScope.launch { - if (getIsAccessTokenValidUseCase(userAccessToken)) { //토큰이 있고 유효함 - _uiState.value = UiState.Success(IntroState.ValidToken) - } else { //토큰이 있어도 유효하지 않음 + // 토큰이 있어도 유효하지 않음 + if (!getIsAccessTokenValidUseCase(userAccessToken)) { _uiState.value = UiState.Error _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다")) + return@launch } + + // 토큰이 있고 유효함 + _uiState.value = UiState.Success(IntroState.ValidToken) + } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt index 3abc4e64b..71762b7e2 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt @@ -70,6 +70,7 @@ class LoginActivity : } ?: Timber.e(error, "User info fetch failed") } } catch (error: Throwable) { + Timber.e(error, "Kakao login failed") handleKakaoLoginError(error) } } diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt index d0753c817..7dae2b179 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt @@ -21,7 +21,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject @@ -41,25 +40,21 @@ class LoginViewModel @Inject constructor( val uiEvent = _uiEvent.asSharedFlow() fun getKakaoLogin(email: String, providerID: String) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { _uiState.value = UiState.Loading - runCatching { - withContext(Dispatchers.IO) { - loginUseCase(LoginWithKakaoRequest(email, providerID)) - } - }.onSuccess { - setAccessTokenUseCase(it.accessToken) - setRefreshTokenUseCase(it.refreshToken) - setUserEmailUseCase(email) - _uiState.value = UiState.Success(LoginState.LoginSuccess) - - TokenStateManager.setTokenValid() - - }.onFailure { + val token = loginUseCase(LoginWithKakaoRequest(email, providerID)) ?: run { _uiState.value = UiState.Error _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.login_failed))) + return@launch } + + setAccessTokenUseCase(token.accessToken) + setRefreshTokenUseCase(token.refreshToken) + setUserEmailUseCase(email) + + _uiState.value = UiState.Success(LoginState.LoginSuccess) + TokenStateManager.setTokenValid() } } diff --git a/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentView.kt b/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentView.kt index 1cab2000d..7d83fca41 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentView.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/MapFragmentView.kt @@ -91,7 +91,7 @@ fun MapFragmentComposeView( // UiState에서 Success 상태인 실제 MapState 데이터만 추출 val mapState: MapState = when (val s = uiState) { - is UiState.Success -> s.data ?: MapState() + is UiState.Success -> s.data else -> MapState() } diff --git a/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt index 0bb797c4c..e145c2065 100644 --- a/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/map/MapViewModel.kt @@ -6,7 +6,6 @@ import com.eatssu.android.domain.model.Partnership import com.eatssu.android.domain.model.PartnershipRestaurant import com.eatssu.android.domain.repository.PartnershipRepository import com.eatssu.android.domain.usecase.user.GetPartnershipDetailUseCase -import com.eatssu.android.domain.usecase.user.GetUserCollegeDepartmentUseCase import com.eatssu.android.presentation.UiEvent import com.eatssu.android.presentation.UiState import com.eatssu.android.presentation.map.model.RestaurantInfo @@ -16,9 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject data class MapState( @@ -53,15 +50,8 @@ class MapViewModel @Inject constructor( viewModelScope.launch { _uiState.value = UiState.Loading - runCatching { partnershipRepository.getAllPartnerships() } - .onSuccess { data -> - _uiState.value = UiState.Success(MapState(partnerships = data)) - } - .onFailure { - Timber.e(it, "제휴 정보 로딩 실패") - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("제휴 정보를 불러오지 못했습니다.")) - } + val partnerships = partnershipRepository.getAllPartnerships() + _uiState.value = UiState.Success(MapState(partnerships = partnerships)) } } @@ -70,23 +60,15 @@ class MapViewModel @Inject constructor( viewModelScope.launch { _uiState.value = UiState.Loading - runCatching { partnershipRepository.getUserCollegePartnerships() } - .onSuccess { data -> - - _uiState.value = UiState.Success(MapState(partnerships = data)) - } - .onFailure { - Timber.e(it, "사용자 단과대 제휴 정보 로딩 실패") - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("내 단과대 제휴 정보를 불러오지 못했습니다.")) - } + val partnerships = partnershipRepository.getUserCollegePartnerships() + _uiState.value = UiState.Success(MapState(partnerships = partnerships)) } } fun selectPartnershipByStoreName(storeName: String, partnershipId: Int? = null) { val current = uiState.value if (current !is UiState.Success) return - val data = current.data ?: return + val data = current.data // 가게 단위의 Partnership 찾기 val partnership = data.partnerships.firstOrNull { it.storeName == storeName } ?: return @@ -119,7 +101,7 @@ class MapViewModel @Inject constructor( fun toggleDepartmentBottomSheet() { val current = uiState.value if (current is UiState.Success) { - current.data?.let { data -> + current.data.let { data -> _uiState.value = UiState.Success( data.copy(showDepartmentBottomSheet = !data.showDepartmentBottomSheet) ) @@ -131,7 +113,7 @@ class MapViewModel @Inject constructor( fun togglePartnershipBottomSheet() { val current = uiState.value if (current is UiState.Success) { - current.data?.let { data -> + current.data.let { data -> _uiState.value = UiState.Success( data.copy(showPartnershipBottomSheet = !data.showPartnershipBottomSheet) ) diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt index e9dec35e6..79eb35bf4 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageFragment.kt @@ -67,7 +67,7 @@ class MyPageFragment : BaseFragment(ScreenId.MYPAGE_MAIN) when (ui) { is UiState.Init, UiState.Loading -> Unit // 닉네임만 불러옴으로 로딩 인디케이터 없음 is UiState.Success -> { - ui.data?.let { render(it) } + render(ui.data) } is UiState.Error -> { diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt index a2264abfb..b028e43f6 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/MyPageViewModel.kt @@ -10,7 +10,6 @@ import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase import com.eatssu.android.presentation.UiEvent import com.eatssu.android.presentation.UiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -21,8 +20,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -65,21 +62,15 @@ class MyPageViewModel @Inject constructor( fun fetchMyInfo() { viewModelScope.launch { - runCatching { - withContext(Dispatchers.IO) { getUserNickNameUseCase() } - }.onSuccess { nickname -> - if (nickname.isNullOrBlank() || nickname.startsWith("user-")) { - _state.update { it.copy(nickname = null) } - _uiEvent.emit(UiEvent.ShowToast("닉네임을 설정해주세요.")) - } else { - _state.update { it.copy(nickname = nickname) } - } - }.onFailure { e -> - // 에러 화면을 꼭 별도로 보여주고 싶다면 uiState를 에러로 전환하는 방식 선택 - // 여기서는 '상태 유지 + 토스트'만 처리 - _uiEvent.emit(UiEvent.ShowToast("정보를 불러올 수 없습니다.")) - Timber.e(e) + val nickname = getUserNickNameUseCase() + + if (nickname.isBlank() || nickname.startsWith("user-")) { + _state.update { it.copy(nickname = null) } + _uiEvent.emit(UiEvent.ShowToast("닉네임을 설정해주세요.")) + return@launch } + + _state.update { it.copy(nickname = nickname) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt index e2b56f8f6..987327616 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutActivity.kt @@ -53,7 +53,7 @@ class SignOutActivity : } is UiState.Success -> { - if (it.data?.isSignOuted == true) { + if (it.data.isSignOuted) { val intent = Intent(this@SignOutActivity, LoginActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt index e4ab459e3..ad57a24fb 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/SignOutViewModel.kt @@ -30,20 +30,17 @@ class SignOutViewModel @Inject constructor( fun signOut() { viewModelScope.launch { _uiState.value = UiState.Loading - try { - val isSignOut = signOutUseCase() - if (isSignOut) { - _uiState.value = UiState.Success(SignOutState(isSignOuted = true)) - _uiEvent.emit(UiEvent.ShowToast("탈퇴가 완료되었습니다.")) - logoutUseCase() // 자동 로그인 정보 삭제 - } else { - _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("탈퇴에 실패했습니다.")) - } - } catch (e: Exception) { + + val success = signOutUseCase() + if (!success) { _uiState.value = UiState.Error - _uiEvent.emit(UiEvent.ShowToast("오류가 발생했습니다. $e")) + _uiEvent.emit(UiEvent.ShowToast("탈퇴에 실패했습니다.")) + return@launch } + + _uiState.value = UiState.Success(SignOutState(isSignOuted = true)) + _uiEvent.emit(UiEvent.ShowToast("탈퇴가 완료되었습니다.")) + logoutUseCase() // 자동 로그인 정보 삭제 } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt index 767e3b90f..08c1b9705 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/myreview/MyReviewViewModel.kt @@ -4,22 +4,16 @@ import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.eatssu.android.R -import com.eatssu.android.data.dto.response.toReviewList import com.eatssu.android.domain.model.Review -import com.eatssu.android.domain.usecase.review.GetMyReviewsUseCase import com.eatssu.android.domain.usecase.review.DeleteReviewUseCase +import com.eatssu.android.domain.usecase.review.GetMyReviewsUseCase import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -38,53 +32,43 @@ class MyReviewViewModel @Inject constructor( fun getMyReviews() { viewModelScope.launch { - getMyReviewsUseCase().onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { it.copy(error = true, toastMessage = "정보를 불러올 수 없습니다.") } - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) - - result.result?.apply { - if (dataList.isEmpty()) { - _uiState.update { it.copy(isEmpty = true) } - } else { - //Todo 리뷰 바인딩을... - _uiState.update { it.copy(myReviews = this.toReviewList()) } - } - - - } + _uiState.update { it.copy(loading = true) } + + val myReviewList = getMyReviewsUseCase() + _uiState.update { + it.copy( + loading = false, + error = false, + myReviews = myReviewList, + isEmpty = myReviewList.isEmpty() + ) } } } fun deleteReview(reviewId: Long) { viewModelScope.launch { - deleteReviewUseCase(reviewId).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> + _uiState.update { it.copy(loading = true) } + + val success = deleteReviewUseCase(reviewId) + if (!success) { _uiState.update { it.copy( + loading = false, error = true, toastMessage = context.getString(R.string.delete_not) ) } - Timber.e(e.toString()) - }.collectLatest { result -> - Timber.d(result.toString()) + return@launch + } - _uiState.update { - it.copy( - isDeleted = true, - toastMessage = context.getString(R.string.delete_done) - ) - } + _uiState.update { + it.copy( + loading = false, + error = false, + isDeleted = true, + toastMessage = context.getString(R.string.delete_done) + ) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt index f45d95988..eaf3f7ce5 100644 --- a/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/mypage/userinfo/UserInfoViewModel.kt @@ -13,10 +13,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber @@ -35,7 +31,7 @@ class UserInfoViewModel @Inject constructor( MutableStateFlow(UserNameChangeState()) val uiState: StateFlow = _uiState.asStateFlow() - init{ + init { loadUserInfo() loadCollegeList() loadDepartmentList(_uiState.value.selectedCollege.collegeId) @@ -60,33 +56,31 @@ class UserInfoViewModel @Inject constructor( } } - fun checkNickname(inputNickname: String) { - viewModelScope.launch { - validateUserNameUseCase(inputNickname).onStart { - _uiState.update { it.copy(loading = true, nickname = inputNickname) } - }.onCompletion { - _uiState.update { it.copy(loading = false) } - }.catch { e -> - _uiState.update { it.copy(error = true, toastMessage = "닉네임 중복 확인에 실패했습니다.") } - Timber.e(e.toString()) - }.collectLatest { result -> - if (result.result == true) { - _uiState.update { - it.copy( - isEnableName = true, - toastMessage = "사용가능한 닉네임 입니다.", - isNicknameChecked = true, - ) - } - } else { - _uiState.update { - it.copy( - isEnableName = false, - toastMessage = "이미 사용 중인 닉네임 입니다.", - ) - } - } + fun checkNickname(inputNickname: String) = viewModelScope.launch { + _uiState.update { it.copy(loading = true, nickname = inputNickname) } + + val valid = validateUserNameUseCase(inputNickname) + + if (!valid) { + _uiState.update { + it.copy( + loading = false, + error = false, + isEnableName = false, + toastMessage = "이미 사용 중인 닉네임 입니다.", + ) } + return@launch + } + + _uiState.update { + it.copy( + loading = false, + error = false, + isEnableName = true, + toastMessage = "사용가능한 닉네임 입니다.", + isNicknameChecked = true, + ) } } @@ -95,17 +89,9 @@ class UserInfoViewModel @Inject constructor( viewModelScope.launch { _uiState.update { it.copy(loading = true) } - try { - setUserNicknameUseCase(nickname) - _uiState.update { - it.copy( - loading = false, - isDone = true, - toastMessage = "닉네임 변경에 성공했습니다." - ) - } - } catch (e: Exception) { - Timber.e(e, "닉네임 변경 실패") + + val success = setUserNicknameUseCase(nickname) + if (!success) { _uiState.update { it.copy( loading = false, @@ -113,27 +99,35 @@ class UserInfoViewModel @Inject constructor( toastMessage = "닉네임 변경에 실패했습니다." ) } + return@launch + } + + _uiState.update { + it.copy( + loading = false, + isDone = true, + toastMessage = "닉네임 변경에 성공했습니다." + ) } } } - fun updateUserDepartment(){ + fun updateUserDepartment() { viewModelScope.launch { - runCatching { + val success = userRepository.setUserDepartment(_uiState.value.selectedDepartment.departmentId) - }.onSuccess { - Timber.d("학과 정보 업데이트 성공") - _uiState.update{ it.copy(success = true) } - val department = _uiState.value.selectedDepartment - val college = _uiState.value.selectedCollege - - setUserCollegeDepartmentUseCase(college, department) - - }.onFailure { e -> - Timber.e(e, "학과 정보 업데이트 실패") + if (!success) { _uiState.update { it.copy(error = true, toastMessage = "학과 정보 업데이트에 실패했습니다.") } + return@launch } + + _uiState.update { it.copy(success = true) } + + val department = _uiState.value.selectedDepartment + val college = _uiState.value.selectedCollege + + setUserCollegeDepartmentUseCase(college, department) } } @@ -163,25 +157,15 @@ class UserInfoViewModel @Inject constructor( fun loadCollegeList() { viewModelScope.launch { - runCatching { - userRepository.getTotalColleges() - }.onSuccess { colleges -> - _uiState.update { it.copy(collegeList = colleges) } - }.onFailure { e -> - Timber.e(e, "단과대 불러오기 실패") - } + val colleges = userRepository.getTotalColleges() + _uiState.update { it.copy(collegeList = colleges) } } } fun loadDepartmentList(collegeId: Int) { viewModelScope.launch { - runCatching { - userRepository.getTotalDepartments(collegeId) - }.onSuccess { departments -> - _uiState.update { it.copy(departmentList = departments) } - }.onFailure { e -> - Timber.e(e, "학과 불러오기 실패") - } + val departments = userRepository.getTotalDepartments(collegeId) + _uiState.update { it.copy(departmentList = departments) } } } diff --git a/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt b/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt index b40d27a41..0e3c70906 100644 --- a/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt +++ b/app/src/main/java/com/eatssu/android/presentation/util/ActivityUtil.kt @@ -3,7 +3,37 @@ package com.eatssu.android.presentation.util import android.app.Activity import android.content.Intent import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import com.eatssu.android.R +import com.eatssu.android.presentation.base.NetworkErrorEventBus +import com.eatssu.android.presentation.error.ServerErrorActivity +import kotlinx.coroutines.launch inline fun AppCompatActivity.startActivity(block: Intent.() -> Unit = {}) { startActivity(Intent(this, T::class.java).apply(block)) +} + +/** + * NetworkErrorEventBus를 구독하여 네트워크 에러 발생 시 ServerErrorActivity로 이동합니다. + */ +fun AppCompatActivity.observeNetworkError( + errorTitle: String? = null, + errorMessage: String? = null +) { + lifecycleScope.launch { + NetworkErrorEventBus.networkError.collect { + val intent = Intent(this@observeNetworkError, ServerErrorActivity::class.java).apply { + putExtra( + ServerErrorActivity.EXTRA_TITLE, + errorTitle ?: getString(R.string.server_error_title) + ) + putExtra( + ServerErrorActivity.EXTRA_MESSAGE, + errorMessage ?: getString(R.string.server_error_message) + ) + } + startActivity(intent) + finish() + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_server_error.xml b/app/src/main/res/layout/activity_server_error.xml new file mode 100644 index 000000000..934f6de84 --- /dev/null +++ b/app/src/main/res/layout/activity_server_error.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8138b485c..9ec27f240 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -174,7 +174,7 @@ 마이 마이페이지 - 로그인이 실패했습니다.\n + 로그인이 실패했습니다. 세션이 만료되었습니다. 다시 로그인 해주세요. 시스템 오류로 다시 로그인해주세요. @@ -189,4 +189,8 @@ eatssu.official https://eat-ssu.notion.site/1d2eeef75a1681ae800cf6ffa6faa37d?pvs=74 + + 통신 오류 + 서버와 통신할 수 없습니다.\n잠시 후 다시 시도해주세요. + 확인 \ No newline at end of file