diff --git a/app/src/main/java/com/wafflestudio/siksha2/compose/ui/community/PostDetailScreen.kt b/app/src/main/java/com/wafflestudio/siksha2/compose/ui/community/PostDetailScreen.kt index 20e5bbd8..8da1f62d 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/compose/ui/community/PostDetailScreen.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/compose/ui/community/PostDetailScreen.kt @@ -94,7 +94,6 @@ fun PostDetailRoute( postDetailViewModel: PostDetailViewModel = hiltViewModel() ) { val postUiState by postDetailViewModel.postUiState.collectAsState() - val board by postDetailViewModel.board.collectAsState() val comments = postDetailViewModel.commentPagingData.collectAsLazyPagingItems() val isAnonymous by postDetailViewModel.isAnonymous.collectAsState() @@ -102,7 +101,7 @@ fun PostDetailRoute( is PostUiState.Success -> { PostDetailScreenSuccess( post = (postUiState as PostUiState.Success).post, - board = board, + board = (postUiState as PostUiState.Success).board, comments = comments, postDetailEvent = postDetailViewModel.postDetailEvent, isAnonymous = isAnonymous, diff --git a/app/src/main/java/com/wafflestudio/siksha2/di/NetworkModule.kt b/app/src/main/java/com/wafflestudio/siksha2/di/NetworkModule.kt index 88edb244..cffd29aa 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/di/NetworkModule.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/di/NetworkModule.kt @@ -5,7 +5,9 @@ import com.squareup.moshi.Moshi import com.wafflestudio.siksha2.BuildConfig import com.wafflestudio.siksha2.R import com.wafflestudio.siksha2.network.SikshaApi +import com.wafflestudio.siksha2.network.result.ResultCallAdapterFactory import com.wafflestudio.siksha2.preferences.SikshaPrefObjects +import com.wafflestudio.siksha2.preferences.serializer.Serializer import dagger.hilt.android.qualifiers.ApplicationContext import dagger.Module import dagger.Provides @@ -48,10 +50,16 @@ object NetworkModule { @Provides @Singleton - fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi, @ApplicationContext context: Context): Retrofit { + fun provideRetrofit( + okHttpClient: OkHttpClient, + moshi: Moshi, + @ApplicationContext context: Context, + serializer: Serializer + ): Retrofit { return Retrofit.Builder() .client(okHttpClient) .baseUrl(context.getString(R.string.server_base_url)) + .addCallAdapterFactory(ResultCallAdapterFactory(serializer)) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() } diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt b/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt index 4925e135..bb2f9960 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/network/SikshaApi.kt @@ -2,6 +2,7 @@ package com.wafflestudio.siksha2.network import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.network.dto.* +import com.wafflestudio.siksha2.network.result.NetworkResult import okhttp3.MultipartBody import retrofit2.Response import retrofit2.http.* @@ -12,17 +13,17 @@ interface SikshaApi { suspend fun fetchMenuGroups( @Query("start_date") startDate: LocalDate, @Query("end_date") endDate: LocalDate - ): FetchMenuGroupsResult + ): NetworkResult @GET("/menus/{menu_id}") - suspend fun fetchMenuById(@Path(value = "menu_id") menuId: Long): Menu + suspend fun fetchMenuById(@Path(value = "menu_id") menuId: Long): NetworkResult @GET("/reviews/") suspend fun fetchReviews( @Query("menu_id") menuId: Long, @Query("page") page: Long, @Query("per_page") perPage: Long - ): FetchReviewsResult + ): NetworkResult @GET("/reviews/filter") suspend fun fetchReviewsWithImage( @@ -30,13 +31,13 @@ interface SikshaApi { @Query("page") page: Long, @Query("per_page") perPage: Long, @Query("etc") etc: Boolean = true - ): FetchReviewsResult + ): NetworkResult @GET("/restaurants/") - suspend fun fetchRestaurants(): FetchRestaurantsResult + suspend fun fetchRestaurants(): NetworkResult @POST("/reviews/") - suspend fun leaveMenuReview(@Body req: LeaveReviewParam): LeaveReviewResult + suspend fun leaveMenuReview(@Body req: LeaveReviewParam): NetworkResult @Multipart @POST("/reviews/images") @@ -45,35 +46,35 @@ interface SikshaApi { @Part("score") score: Long, @Part comment: MultipartBody.Part, @Part images: List - ): LeaveReviewResult + ): NetworkResult @POST("/auth/login/kakao") - suspend fun loginKakao(@Header("kakao-token") kakaoToken: String): LoginOAuthResult + suspend fun loginKakao(@Header("kakao-token") kakaoToken: String): NetworkResult @POST("/auth/login/google") - suspend fun loginGoogle(@Header("google-token") googleToken: String): LoginOAuthResult + suspend fun loginGoogle(@Header("google-token") googleToken: String): NetworkResult @DELETE("/auth/") suspend fun deleteAccount() @POST("/auth/refresh") - suspend fun refreshToken(@Header("authorization-token") token: String): LoginOAuthResult + suspend fun refreshToken(@Header("authorization-token") token: String): NetworkResult @GET("/reviews/comments/recommendation") suspend fun fetchRecommendationReviewComments(@Query("score") score: Long): - FetchRecommendationReviewCommentsResult + NetworkResult @GET("/reviews/dist") suspend fun fetchReviewDistribution(@Query("menu_id") menuId: Long): - FetchReviewDistributionResult + NetworkResult @POST("/voc") suspend fun sendVoc( @Body req: VocParam - ) + ): NetworkResult @GET("/auth/me/image") - suspend fun getUserData(): GetUserDataResult + suspend fun getUserData(): NetworkResult @Multipart @PATCH("/auth/me/image/profile") @@ -81,69 +82,69 @@ interface SikshaApi { @Part image: MultipartBody.Part?, @Part("change_to_default_image") changeToDefaultImage: Boolean, @Part nickname: MultipartBody.Part? - ): GetUserDataResult + ): NetworkResult @GET("/auth/nicknames/validate") suspend fun checkNickname( @Query("nickname") nickname: String - ) + ): NetworkResult @GET("/versions/android") - suspend fun getVersion(): GetVersionResult + suspend fun getVersion(): NetworkResult @POST("/menus/{menu_id}/like") - suspend fun postLikeMenu(@Path("menu_id") menuId: Long): MenuLikeOrUnlikeResponse + suspend fun postLikeMenu(@Path("menu_id") menuId: Long): NetworkResult @POST("/menus/{menu_id}/unlike") - suspend fun postUnlikeMenu(@Path("menu_id") menuId: Long): MenuLikeOrUnlikeResponse + suspend fun postUnlikeMenu(@Path("menu_id") menuId: Long): NetworkResult @GET("/community/boards") - suspend fun getBoards(): GetBoardsResult + suspend fun getBoards(): NetworkResult @GET("/community/boards/{board_id}") suspend fun getBoard( @Path("board_id") boardId: Long - ): GetBoardResult + ): NetworkResult @GET("/community/posts") suspend fun getPosts( @Query("board_id") boardId: Long, @Query("page") page: Long, @Query("per_page") perPage: Int - ): GetPostsResult + ): NetworkResult @GET("/community/posts/me") suspend fun getUserPosts( @Query("page") page: Long, @Query("per_page") perPage: Int - ): GetPostsResult + ): NetworkResult @GET("/community/posts/{post_id}") suspend fun getPost( @Path("post_id") postId: Long - ): GetPostResult + ): NetworkResult @GET("/community/comments") suspend fun getComments( @Query("post_id") postId: Long, @Query("page") page: Long, @Query("per_page") perPage: Int - ): GetCommentsResult + ): NetworkResult @POST("/community/comments") suspend fun postComment( @Body body: PostCommentRequestBody - ): PostCommentResponse + ): NetworkResult @POST("/community/posts/{post_id}/like") suspend fun postLikePost( @Path("post_id") postId: Long - ): PostLikePostResponse + ): NetworkResult @POST("/community/posts/{post_id}/unlike") suspend fun postUnlikePost( @Path("post_id") postId: Long - ): PostUnlikePostResponse + ): NetworkResult @Multipart @POST("/community/posts") @@ -153,7 +154,7 @@ interface SikshaApi { @Part content: MultipartBody.Part, @Part("anonymous") anonymous: Boolean, @Part images: List - ): CreatePostResponse + ): NetworkResult @Multipart @PATCH("/community/posts/{post_id}") @@ -164,17 +165,17 @@ interface SikshaApi { @Part content: MultipartBody.Part, @Part("anonymous") anonymous: Boolean, @Part images: List - ): PatchPostResponse + ): NetworkResult @POST("/community/comments/{comment_id}/like") suspend fun postLikeComment( @Path("comment_id") commentId: Long - ): PostLikeCommentResponse + ): NetworkResult @POST("/community/comments/{comment_id}/unlike") suspend fun postUnlikeComment( @Path("comment_id") commentId: Long - ): PostUnlikeCommentResponse + ): NetworkResult @DELETE("community/posts/{postId}") suspend fun deletePost( @@ -190,14 +191,14 @@ interface SikshaApi { suspend fun reportPost( @Path("post_id") postId: Long, @Body requestBody: ReportPostRequestBody - ): ReportPostResponse + ): NetworkResult @POST("/community/comments/{comment_id}/report") suspend fun reportComment( @Path("comment_id") commentId: Long, @Body requestBody: ReportCommentRequestBody - ): ReportCommentResponse + ): NetworkResult @GET("/community/posts/popular/trending") - suspend fun getTrendingPosts(): GetTrendingPostsResponse + suspend fun getTrendingPosts(): NetworkResult } diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/dto/core/ErrorDto.kt b/app/src/main/java/com/wafflestudio/siksha2/network/dto/core/ErrorDto.kt new file mode 100644 index 00000000..0cf59b4f --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/network/dto/core/ErrorDto.kt @@ -0,0 +1,24 @@ +package com.wafflestudio.siksha2.network.dto.core + +import com.squareup.moshi.JsonClass + +// TODO: error dto 형식 서버와 확정 +/*@JsonClass(generateAdapter = true) +data class ErrorDto( + @Json(name = "detail") val details: List, + val body: String?, + val message: String, +) + +data class ErrorDetail( + val type: String, + val location: List, + val msg: String, + val input: String?, + val url: String, +)*/ + +@JsonClass(generateAdapter = true) +data class ErrorDto( + val message: String? +) diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/result/NetworkResult.kt b/app/src/main/java/com/wafflestudio/siksha2/network/result/NetworkResult.kt new file mode 100644 index 00000000..137d7f6e --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/network/result/NetworkResult.kt @@ -0,0 +1,41 @@ +package com.wafflestudio.siksha2.network.result + +import java.io.IOException + +sealed interface NetworkResult { + data class Success(val body: T) : NetworkResult + data class Failure(val message: String) : NetworkResult + data class NetworkError(val exception: IOException) : NetworkResult + data class UnknownError(val t: Throwable?) : NetworkResult + + fun map(transform: (T) -> R): NetworkResult = when (this) { + is Success -> Success(transform(body)) + is Failure -> this + is NetworkError -> this + is UnknownError -> this + } + + fun onSuccess(action: (value: T) -> Unit): NetworkResult = apply { + if (this is Success) action(body) + } + + fun onFailure(action: (message: String) -> Unit): NetworkResult = apply { + if (this is Failure) action(message) + } + + fun onNetworkError(action: (exception: IOException) -> Unit): NetworkResult = apply { + if (this is NetworkError) action(exception) + } + + fun onUnknownError(action: (exception: Throwable?) -> Unit): NetworkResult = apply { + if (this is UnknownError) action(t) + } + + fun onError(action: (t: Throwable?) -> Unit): NetworkResult = apply { + when (this) { + is NetworkError -> action(exception) + is UnknownError -> action(t) + else -> Unit + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCall.kt b/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCall.kt new file mode 100644 index 00000000..f444b864 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCall.kt @@ -0,0 +1,96 @@ +package com.wafflestudio.siksha2.network.result + +import com.wafflestudio.siksha2.network.dto.core.ErrorDto +import com.wafflestudio.siksha2.preferences.serializer.Serializer +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException +import java.lang.IllegalStateException +import java.lang.UnsupportedOperationException + +class ResultCall( + private val call: Call, + private val serializer: Serializer +) : Call> { + override fun enqueue(callback: Callback>) { + call.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val body = response.body() + val code = response.code() + val error = response.errorBody()?.string() + + if (response.isSuccessful) { + if (body != null) { + callback.onResponse( + this@ResultCall, + Response.success(NetworkResult.Success(body)) + ) + } else { + callback.onResponse( + this@ResultCall, + Response.success(NetworkResult.UnknownError(IllegalStateException("body is null"))) + ) + } + } else { + if (error == null) { + callback.onResponse( + this@ResultCall, + Response.success(NetworkResult.UnknownError(IllegalStateException("errorbody is null"))) + ) + return + } + + val errorDto = parseError(error) + if (errorDto == null) { + callback.onResponse( + this@ResultCall, + Response.success(NetworkResult.UnknownError(IllegalStateException("errorbody parsing failed"))) + ) + return + } + + callback.onResponse( + this@ResultCall, + Response.success(NetworkResult.Failure(errorDto.message ?: "알 수 없는 에러입니다.")) + ) + } + } + + override fun onFailure(call: Call, t: Throwable) { + val errorResponse = when (t) { + is IOException -> NetworkResult.NetworkError(t) + else -> NetworkResult.UnknownError(t) + } + callback.onResponse(this@ResultCall, Response.success(errorResponse)) + } + }) + } + + private fun parseError(errorBody: String): ErrorDto? { + val errorDto = runCatching { + serializer.deserialize( + errorBody, + ErrorDto::class.java + ) + }.getOrNull() + + return errorDto + } + + override fun clone(): Call> = ResultCall(call.clone(), serializer) + + override fun execute(): Response> = throw UnsupportedOperationException("ResultCall doesn't use execute()") + + override fun isExecuted(): Boolean = call.isExecuted + + override fun cancel() = call.cancel() + + override fun isCanceled(): Boolean = call.isCanceled + + override fun request(): Request = call.request() + + override fun timeout(): Timeout = call.timeout() +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCallAdapter.kt b/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCallAdapter.kt new file mode 100644 index 00000000..6b5bb7cd --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCallAdapter.kt @@ -0,0 +1,15 @@ +package com.wafflestudio.siksha2.network.result + +import com.wafflestudio.siksha2.preferences.serializer.Serializer +import retrofit2.Call +import retrofit2.CallAdapter +import java.lang.reflect.Type + +class ResultCallAdapter( + private val responseType: Type, + private val serializer: Serializer +) : CallAdapter>> { + override fun responseType(): Type = responseType + + override fun adapt(call: Call): Call> = ResultCall(call, serializer) +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCallAdapterFactory.kt b/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCallAdapterFactory.kt new file mode 100644 index 00000000..23beac60 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/network/result/ResultCallAdapterFactory.kt @@ -0,0 +1,41 @@ +package com.wafflestudio.siksha2.network.result + +import com.wafflestudio.siksha2.preferences.serializer.Serializer +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class ResultCallAdapterFactory( + private val serializer: Serializer +) : CallAdapter.Factory() { + + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + if (getRawType(returnType) != Call::class.java) { + return null + } + + check(returnType is ParameterizedType) { + "return type must be parameterized as Call> or Call>" + } + + val responseType = getParameterUpperBound(0, returnType) + + if (getRawType(responseType) != NetworkResult::class.java) { + return null + } + + check(responseType is ParameterizedType) { + "Response must be parameterized as NetworkResult or NetworkResult" + } + + val bodyType = getParameterUpperBound(0, responseType) + + return ResultCallAdapter(bodyType, serializer) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/CommunityRepository.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/CommunityRepository.kt index f83be514..214123f7 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/CommunityRepository.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/CommunityRepository.kt @@ -1,12 +1,19 @@ package com.wafflestudio.siksha2.repositories import com.wafflestudio.siksha2.models.Board +import com.wafflestudio.siksha2.models.Comment import com.wafflestudio.siksha2.models.Post import com.wafflestudio.siksha2.network.SikshaApi import com.wafflestudio.siksha2.network.dto.PostCommentRequestBody import com.wafflestudio.siksha2.network.dto.ReportPostRequestBody import com.wafflestudio.siksha2.network.dto.ReportCommentRequestBody +import com.wafflestudio.siksha2.network.dto.ReportCommentResponse +import com.wafflestudio.siksha2.network.dto.ReportPostResponse +import com.wafflestudio.siksha2.network.dto.core.BoardDto +import com.wafflestudio.siksha2.network.dto.core.CommentDto +import com.wafflestudio.siksha2.network.dto.core.PostDto +import com.wafflestudio.siksha2.network.result.NetworkResult import retrofit2.Response import com.wafflestudio.siksha2.preferences.SikshaPrefObjects @@ -27,33 +34,33 @@ class CommunityRepository @Inject constructor( ) { val isAnonymous = sikshaPrefObjects.communityIsAnonymous.asFlow() - suspend fun getBoards(): List { - return api.getBoards().map { it.toBoard() } + suspend fun getBoards(): NetworkResult> { + return api.getBoards().map { it.map(BoardDto::toBoard) } } - suspend fun getBoard(boardId: Long): Board { - return api.getBoard(boardId).toBoard() + suspend fun getBoard(boardId: Long): NetworkResult { + return api.getBoard(boardId).map(BoardDto::toBoard) } fun getUserPostPagingSource() = UserPostPagingSource(api) fun getPostPagingSource(boardId: Long) = PostPagingSource(boardId, api) - suspend fun getPost(postId: Long): Post { - return api.getPost(postId).toPost() + suspend fun getPost(postId: Long): NetworkResult { + return api.getPost(postId).map(PostDto::toPost) } fun commentPagingSource(postId: Long) = CommentPagingSource(postId, api) - suspend fun addCommentToPost(postId: Long, content: String, isAnonymous: Boolean) { - api.postComment(PostCommentRequestBody(postId, content, isAnonymous)) + suspend fun addCommentToPost(postId: Long, content: String, isAnonymous: Boolean): NetworkResult { + return api.postComment(PostCommentRequestBody(postId, content, isAnonymous)).map {} } - suspend fun likePost(postId: Long): Post { - return api.postLikePost(postId).toPost() + suspend fun likePost(postId: Long): NetworkResult { + return api.postLikePost(postId).map(PostDto::toPost) } - suspend fun unlikePost(postId: Long): Post { - return api.postUnlikePost(postId).toPost() + suspend fun unlikePost(postId: Long): NetworkResult { + return api.postUnlikePost(postId).map(PostDto::toPost) } suspend fun createPost( @@ -62,8 +69,8 @@ class CommunityRepository @Inject constructor( content: MultipartBody.Part, anonymous: Boolean, images: List - ): Post { - return api.postCreatePost(boardId, title, content, anonymous, images).toPost() + ): NetworkResult { + return api.postCreatePost(boardId, title, content, anonymous, images).map(PostDto::toPost) } suspend fun patchPost( @@ -73,16 +80,16 @@ class CommunityRepository @Inject constructor( content: MultipartBody.Part, anonymous: Boolean, images: List - ): Post { - return api.postPatchPost(postId, boardId, title, content, anonymous, images).toPost() + ): NetworkResult { + return api.postPatchPost(postId, boardId, title, content, anonymous, images).map(PostDto::toPost) } - suspend fun likeComment(commentId: Long) { - api.postLikeComment(commentId) + suspend fun likeComment(commentId: Long): NetworkResult { + return api.postLikeComment(commentId).map(CommentDto::toComment) } - suspend fun unlikeComment(commentId: Long) { - api.postUnlikeComment(commentId) + suspend fun unlikeComment(commentId: Long): NetworkResult { + return api.postUnlikeComment(commentId).map(CommentDto::toComment) } suspend fun deletePost(postId: Long): Response { @@ -93,18 +100,18 @@ class CommunityRepository @Inject constructor( return api.deleteComment(commentId) } - suspend fun reportPost(postId: Long, reason: String) { - api.reportPost(postId, ReportPostRequestBody(reason)) + suspend fun reportPost(postId: Long, reason: String): NetworkResult { + return api.reportPost(postId, ReportPostRequestBody(reason)) } - suspend fun reportComment(commentId: Long, reason: String) { - api.reportComment(commentId, ReportCommentRequestBody(reason)) + suspend fun reportComment(commentId: Long, reason: String): NetworkResult { + return api.reportComment(commentId, ReportCommentRequestBody(reason)) } - suspend fun getTrendingPosts(): List { + suspend fun getTrendingPosts(): NetworkResult> { return withContext(Dispatchers.IO) { - api.getTrendingPosts().result.map { - it.toPost() + api.getTrendingPosts().map { + it.result.map(PostDto::toPost) } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt index 4de68a16..43889255 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt @@ -8,9 +8,12 @@ import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.models.MenuGroup import com.wafflestudio.siksha2.models.Review import com.wafflestudio.siksha2.network.SikshaApi +import com.wafflestudio.siksha2.network.dto.FetchRecommendationReviewCommentsResult +import com.wafflestudio.siksha2.network.dto.FetchReviewDistributionResult import com.wafflestudio.siksha2.network.dto.FetchReviewsResult import com.wafflestudio.siksha2.network.dto.LeaveReviewParam import com.wafflestudio.siksha2.network.dto.LeaveReviewResult +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewPagingSource import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewWithImagePagingSource import com.wafflestudio.siksha2.utils.toLocalDate @@ -33,11 +36,17 @@ class MenuRepository @Inject constructor( withContext(Dispatchers.IO) { val startDate = date.minusDays(1) val endDate = date.plusDays(1) - val payload = sikshaApi.fetchMenuGroups(startDate, endDate).result - .map { - DailyMenu(it.date.toLocalDate(), it) + when (val response = sikshaApi.fetchMenuGroups(startDate, endDate)) { + is NetworkResult.Success -> { + val payload = response.body.result.map { + DailyMenu(it.date.toLocalDate(), it) + } + dailyMenusDao.insertDailyMenus(payload) + } + else -> { + throw RuntimeException("") } - dailyMenusDao.insertDailyMenus(payload) + } } } @@ -52,7 +61,7 @@ class MenuRepository @Inject constructor( return dailyMenusDao.getDailyMenuByDate(date) } - suspend fun getMenuById(menuId: Long): Menu { + suspend fun getMenuById(menuId: Long): NetworkResult { return sikshaApi.fetchMenuById(menuId) } @@ -70,39 +79,51 @@ class MenuRepository @Inject constructor( ).flow } - suspend fun leaveMenuReview(menuId: Long, score: Double, comment: String): LeaveReviewResult { + suspend fun leaveMenuReview(menuId: Long, score: Double, comment: String): NetworkResult { return sikshaApi.leaveMenuReview(LeaveReviewParam(menuId, score, comment)) } - suspend fun leaveMenuReviewImage(menuId: Long, score: Long, comment: MultipartBody.Part, images: List): LeaveReviewResult { + suspend fun leaveMenuReviewImage(menuId: Long, score: Long, comment: MultipartBody.Part, images: List): NetworkResult { return sikshaApi.leaveMenuReviewImages(menuId, score, comment, images) } - suspend fun getReviewRecommendationComments(score: Long): String { - return sikshaApi.fetchRecommendationReviewComments(score).comment + suspend fun getReviewRecommendationComments(score: Long): NetworkResult { + return sikshaApi.fetchRecommendationReviewComments(score) } - suspend fun getReviewDistribution(menuId: Long): List { - return sikshaApi.fetchReviewDistribution(menuId).dist + suspend fun getReviewDistribution(menuId: Long): NetworkResult { + return sikshaApi.fetchReviewDistribution(menuId) } - suspend fun getFirstReviewPhotoByMenuId(menuId: Long): FetchReviewsResult { + suspend fun getFirstReviewPhotoByMenuId(menuId: Long): NetworkResult { return sikshaApi.fetchReviewsWithImage(menuId, 1L, 5) } - suspend fun likeMenuById(menuId: Long): Menu { + suspend fun likeMenuById(menuId: Long): NetworkResult { return withContext(Dispatchers.IO) { - val menu = sikshaApi.postLikeMenu(menuId) - updateMenuInLocal(menu) - return@withContext menu + val response = sikshaApi.postLikeMenu(menuId) + when (response) { + is NetworkResult.Success -> { + val menu = response.body + updateMenuInLocal(menu) + } + else -> { } + } + return@withContext response } } - suspend fun unlikeMenuById(menuId: Long): Menu { + suspend fun unlikeMenuById(menuId: Long): NetworkResult { return withContext(Dispatchers.IO) { - val menu = sikshaApi.postUnlikeMenu(menuId) - updateMenuInLocal(menu) - return@withContext menu + val response = sikshaApi.postUnlikeMenu(menuId) + when (response) { + is NetworkResult.Success -> { + val menu = response.body + updateMenuInLocal(menu) + } + else -> { } + } + return@withContext response } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/RestaurantRepository.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/RestaurantRepository.kt index 7a1b5778..30539acd 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/RestaurantRepository.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/RestaurantRepository.kt @@ -3,10 +3,12 @@ package com.wafflestudio.siksha2.repositories import com.wafflestudio.siksha2.db.RestaurantsDao import com.wafflestudio.siksha2.models.RestaurantInfo import com.wafflestudio.siksha2.network.SikshaApi +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.preferences.SikshaPrefObjects import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext +import java.io.IOException import javax.inject.Inject import javax.inject.Singleton @@ -22,8 +24,15 @@ class RestaurantRepository @Inject constructor( suspend fun syncWithServer() { withContext(Dispatchers.IO) { - val data = sikshaApi.fetchRestaurants() - restaurantsDao.update(data.result) + when (val response = sikshaApi.fetchRestaurants()) { + is NetworkResult.Success -> { + val data = response.body + restaurantsDao.update(data.result) + } + else -> { + throw IOException("") + } + } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/UserStatusManager.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/UserStatusManager.kt index 628fe8cb..212a2323 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/UserStatusManager.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/UserStatusManager.kt @@ -13,7 +13,11 @@ import com.wafflestudio.siksha2.models.toUser import com.wafflestudio.siksha2.models.toVersion import com.wafflestudio.siksha2.network.OAuthProvider import com.wafflestudio.siksha2.network.SikshaApi +import com.wafflestudio.siksha2.network.dto.GetVersionResult +import com.wafflestudio.siksha2.network.dto.LoginOAuthResult import com.wafflestudio.siksha2.network.dto.VocParam +import com.wafflestudio.siksha2.network.dto.core.UserDto +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.preferences.SikshaPrefObjects import com.wafflestudio.siksha2.utils.showToast import okhttp3.MultipartBody @@ -26,20 +30,34 @@ class UserStatusManager @Inject constructor( private val sikshaApi: SikshaApi, private val sikshaPrefObjects: SikshaPrefObjects ) { - suspend fun loginWithOAuthToken(provider: OAuthProvider, token: String) { + suspend fun loginWithOAuthToken(provider: OAuthProvider, token: String): NetworkResult { val tokenWithPrefix = attachBearerPrefix(token) - val (accessToken) = when (provider) { + val response = when (provider) { OAuthProvider.GOOGLE -> sikshaApi.loginGoogle(tokenWithPrefix) OAuthProvider.KAKAO -> sikshaApi.loginKakao(tokenWithPrefix) } - sikshaPrefObjects.oAuthProvider.setValue(provider) - sikshaPrefObjects.accessToken.setValue(attachBearerPrefix(accessToken)) + when (response) { + is NetworkResult.Success -> { + val accessToken = response.body.accessToken + sikshaPrefObjects.oAuthProvider.setValue(provider) + sikshaPrefObjects.accessToken.setValue(attachBearerPrefix(accessToken)) + } + else -> { } + } + return response } - suspend fun refreshUserToken() { + suspend fun refreshUserToken(): Boolean { sikshaPrefObjects.accessToken.getValue().let { - val (accessToken) = sikshaApi.refreshToken(it) - sikshaPrefObjects.accessToken.setValue(attachBearerPrefix(accessToken)) + when (val response = sikshaApi.refreshToken(it)) { + is NetworkResult.Success -> { + val accessToken = response.body.accessToken + sikshaPrefObjects.accessToken.setValue(attachBearerPrefix(accessToken)) + return true + } + // 로그인 실패시 do nothing -> 다시 로그인 시나리오 타게 냅두기 + else -> return false + } } } @@ -77,26 +95,26 @@ class UserStatusManager @Inject constructor( } } - suspend fun sendVoc(voc: String, platform: String) { + suspend fun sendVoc(voc: String, platform: String): NetworkResult { val vocParam = VocParam(voc = voc, platform = platform) - sikshaApi.sendVoc(vocParam) + return sikshaApi.sendVoc(vocParam) } - suspend fun getUserData(): User { - return sikshaApi.getUserData().toUser() + suspend fun getUserData(): NetworkResult { + return sikshaApi.getUserData().map(UserDto::toUser) } - suspend fun updateUserProfile(nickname: String?, changeToDefaultImage: Boolean, image: MultipartBody.Part?): User { + suspend fun updateUserProfile(nickname: String?, changeToDefaultImage: Boolean, image: MultipartBody.Part?): NetworkResult { val nicknameBody = nickname?.let { MultipartBody.Part.createFormData("nickname", it) } - return sikshaApi.updateUserData(image, changeToDefaultImage, nicknameBody).toUser() + return sikshaApi.updateUserData(image, changeToDefaultImage, nicknameBody).map(UserDto::toUser) } - suspend fun checkNickname(nickname: String) { - sikshaApi.checkNickname(nickname) + suspend fun checkNickname(nickname: String): NetworkResult { + return sikshaApi.checkNickname(nickname) } - suspend fun getVersion(): Version { - return sikshaApi.getVersion().toVersion() + suspend fun getVersion(): NetworkResult { + return sikshaApi.getVersion().map(GetVersionResult::toVersion) } // TODO: applicationContext 주입받아서 사용 (but google login 에서 activity 필요...) diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/CommentPagingSource.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/CommentPagingSource.kt index 4091566e..baabf595 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/CommentPagingSource.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/CommentPagingSource.kt @@ -4,6 +4,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import com.wafflestudio.siksha2.models.Comment import com.wafflestudio.siksha2.network.SikshaApi +import com.wafflestudio.siksha2.network.result.NetworkResult class CommentPagingSource( val postId: Long, @@ -12,12 +13,16 @@ class CommentPagingSource( override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: STARTING_KEY - val response = api.getComments(postId, page, params.loadSize) - return LoadResult.Page( - data = response.result.map { it.toComment() }, - prevKey = if (page == STARTING_KEY) null else page - 1, - nextKey = if (response.hasNext) page + (params.loadSize / ITEMS_PER_PAGE) else null - ) + return when (val response = api.getComments(postId, page, params.loadSize)) { + is NetworkResult.Success -> { + LoadResult.Page( + data = response.body.result.map { it.toComment() }, + prevKey = if (page == STARTING_KEY) null else page - 1, + nextKey = if (response.body.hasNext) page + (params.loadSize / ITEMS_PER_PAGE) else null + ) + } + else -> LoadResult.Error(RuntimeException("")) + } } override fun getRefreshKey(state: PagingState): Long? { diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/PostPagingSource.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/PostPagingSource.kt index 6b9e0c49..b7376378 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/PostPagingSource.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/PostPagingSource.kt @@ -4,8 +4,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import com.wafflestudio.siksha2.models.Post import com.wafflestudio.siksha2.network.SikshaApi -import retrofit2.HttpException -import java.io.IOException +import com.wafflestudio.siksha2.network.result.NetworkResult class PostPagingSource( val boardId: Long, @@ -13,26 +12,15 @@ class PostPagingSource( ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: STARTING_KEY - return try { - val response = api.getPosts( - boardId = boardId, - page = page, - perPage = params.loadSize - ) - - LoadResult.Page( - data = response.result.map { it.toPost() }, - prevKey = when (page) { - STARTING_KEY -> null - else -> page - 1 - }, - nextKey = if (response.hasNext) page + params.loadSize / ITEMS_PER_PAGE else null - ) - } catch (e: HttpException) { - e.printStackTrace() - LoadResult.Error(e) - } catch (e: IOException) { - LoadResult.Error(e) + return when (val response = api.getPosts(boardId, page, params.loadSize)) { + is NetworkResult.Success -> { + LoadResult.Page( + data = response.body.result.map { it.toPost() }, + prevKey = if (page == STARTING_KEY) null else page - 1, + nextKey = if (response.body.hasNext) page + (params.loadSize / ITEMS_PER_PAGE) else null + ) + } + else -> LoadResult.Error(RuntimeException("")) } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/UserPostPagingSource.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/UserPostPagingSource.kt index db247da1..f152da5d 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/UserPostPagingSource.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/pagingsource/UserPostPagingSource.kt @@ -4,33 +4,25 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import com.wafflestudio.siksha2.models.Post import com.wafflestudio.siksha2.network.SikshaApi -import retrofit2.HttpException -import java.io.IOException +import com.wafflestudio.siksha2.network.result.NetworkResult class UserPostPagingSource( private val api: SikshaApi ) : PagingSource() { override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: PostPagingSource.STARTING_KEY - return try { - val response = api.getUserPosts( - page = page, - perPage = params.loadSize - ) - - LoadResult.Page( - data = response.result.map { it.toPost() }, - prevKey = when (page) { - PostPagingSource.STARTING_KEY -> null - else -> page - 1 - }, - nextKey = if (response.hasNext) page + params.loadSize / PostPagingSource.ITEMS_PER_PAGE else null - ) - } catch (e: HttpException) { - e.printStackTrace() - LoadResult.Error(e) - } catch (e: IOException) { - LoadResult.Error(e) + return when (val response = api.getUserPosts(page, params.loadSize)) { + is NetworkResult.Success -> { + LoadResult.Page( + data = response.body.result.map { it.toPost() }, + prevKey = when (page) { + PostPagingSource.STARTING_KEY -> null + else -> page - 1 + }, + nextKey = if (response.body.hasNext) page + params.loadSize / ITEMS_PER_PAGE else null + ) + } + else -> LoadResult.Error(RuntimeException("")) } } @@ -40,5 +32,6 @@ class UserPostPagingSource( companion object { const val STARTING_KEY = 1L + const val ITEMS_PER_PAGE = 10 } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/SplashActivity.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/SplashActivity.kt index 52336c1c..11a21e91 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/SplashActivity.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/SplashActivity.kt @@ -21,6 +21,7 @@ import com.kakao.sdk.user.UserApiClient import com.wafflestudio.siksha2.R import com.wafflestudio.siksha2.databinding.ActivitySplashBinding import com.wafflestudio.siksha2.network.OAuthProvider +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.UserStatusManager import com.wafflestudio.siksha2.utils.setVisibleOrGone import com.wafflestudio.siksha2.utils.showToast @@ -29,9 +30,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import retrofit2.HttpException import timber.log.Timber -import java.io.IOException import javax.inject.Inject @AndroidEntryPoint @@ -87,14 +86,18 @@ class SplashActivity : AppCompatActivity() { private fun onOAuthSuccess(provider: OAuthProvider, token: String) { lifecycleScope.launch { - try { - userStatusManager.loginWithOAuthToken(provider, token) - startActivity(Intent(this@SplashActivity, RootActivity::class.java)) - finish() - } catch (e: HttpException) { - showToast("인증 실패") - } catch (e: IOException) { - showToast(getString(R.string.common_network_error)) + when (val loginResponse = userStatusManager.loginWithOAuthToken(provider, token)) { + is NetworkResult.Success -> { + startActivity(Intent(this@SplashActivity, RootActivity::class.java)) + finish() + } + is NetworkResult.Failure -> { + showToast(loginResponse.message) + } + is NetworkResult.NetworkError -> { + showToast(getString(R.string.common_network_error)) + } + else -> showToast(getString(R.string.common_unknown_error)) } } } @@ -160,12 +163,6 @@ class SplashActivity : AppCompatActivity() { } private suspend fun checkLoginStatus(): Boolean { - return try { - userStatusManager.refreshUserToken() - true - } catch (e: HttpException) { - // do nothing - 다시 로그인 시나리오 타게 냅두기 - false - } + return userStatusManager.refreshUserToken() } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/CommentReportViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/CommentReportViewModel.kt index e32f2353..3c620404 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/CommentReportViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/CommentReportViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wafflestudio.siksha2.models.User +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.CommunityRepository import com.wafflestudio.siksha2.repositories.UserStatusManager import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,7 +13,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch -import retrofit2.HttpException import javax.inject.Inject @HiltViewModel @@ -36,30 +36,24 @@ class CommentReportViewModel @Inject constructor( private fun fetchUser() { viewModelScope.launch { - runCatching { - _user.value = userStatusManager.getUserData() - }.onFailure { - // TODO: 예외 대응 필요 + when (val response = userStatusManager.getUserData()) { + is NetworkResult.Success -> { + _user.value = response.body + } + is NetworkResult.Failure -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed(response.message)) + is NetworkResult.NetworkError -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("네트워크 연결이 불안정합니다.")) + else -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("알 수 없는 오류가 발생했습니다.")) } } } fun reportComment(reportContent: String) { viewModelScope.launch { - runCatching { - communityRepository.reportComment(commentId, reportContent) - }.onSuccess { - _commentReportEvent.emit(CommentReportEvent.ReportCommentSuccess) - }.onFailure { throwable -> - when (throwable) { - is HttpException -> { - when (throwable.code()) { - 409 -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("이미 신고한 게시글입니다.")) - else -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("알 수 없는 오류입니다.")) - } - } - else -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("알 수 없는 오류입니다.")) - } + when (val response = communityRepository.reportComment(commentId, reportContent)) { + is NetworkResult.Success -> _commentReportEvent.emit(CommentReportEvent.ReportCommentSuccess) + is NetworkResult.Failure -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed(response.message)) + is NetworkResult.NetworkError -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("네트워크 연결이 불안정합니다.")) + else -> _commentReportEvent.emit(CommentReportEvent.ReportCommentFailed("알 수 없는 오류가 발생했습니다.")) } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostCreateViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostCreateViewModel.kt index b045594f..b45bbb7a 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostCreateViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostCreateViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wafflestudio.siksha2.models.Board import com.wafflestudio.siksha2.models.Post +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.CommunityRepository import com.wafflestudio.siksha2.utils.ImageUtil import com.wafflestudio.siksha2.utils.showToast @@ -74,7 +75,7 @@ class PostCreateViewModel @Inject constructor( init { viewModelScope.launch { runCatching { - _boards.value = communityRepository.getBoards() + _boards.value = (communityRepository.getBoards() as NetworkResult.Success).body val boardId: Long = PostCreateFragmentArgs.fromSavedStateHandle(savedStateHandle).boardId val postId: Long = PostEditFragmentArgs.fromSavedStateHandle(savedStateHandle).postId _postCreateEvent.emit(PostCreateEvent.FetchPostProcessing) @@ -95,12 +96,12 @@ class PostCreateViewModel @Inject constructor( } private suspend fun createPostInit(boardId: Long) { - _board.value = communityRepository.getBoard(boardId) + _board.value = (communityRepository.getBoard(boardId) as NetworkResult.Success).body // FIXME: 임시로 casting } private suspend fun editPostInit(postId: Long) { - _post.value = communityRepository.getPost(postId) - _board.value = communityRepository.getBoard(post.value.boardId) + _post.value = (communityRepository.getPost(postId) as NetworkResult.Success).body // FIXME: 임시로 casting + _board.value = (communityRepository.getBoard(post.value.boardId) as NetworkResult.Success).body // FIXME: 임시로 casting _title.value = post.value.title _content.value = post.value.content _imageUrisToUpload.value = post.value.etc?.images?.map { Uri.parse(it) } ?: listOf() @@ -141,11 +142,18 @@ class PostCreateViewModel @Inject constructor( } val titleBody = MultipartBody.Part.createFormData("title", title.value) val contentBody = MultipartBody.Part.createFormData("content", content.value) - var response: Post? - imageList.let { - response = communityRepository.createPost(boardId, titleBody, contentBody, anonymous, imageList) + val response: NetworkResult = imageList.let { + communityRepository.createPost(boardId, titleBody, contentBody, anonymous, imageList) + } + when (response) { + is NetworkResult.Success -> { + _createdPostId.value = response.body.id + } + else -> { + _postCreateEvent.emit(PostCreateEvent.UploadPostFailed) + return@launch + } } - _createdPostId.value = response?.id ?: -1 }.onSuccess { _postCreateEvent.emit(PostCreateEvent.UploadPostSuccess) }.onFailure { @@ -172,7 +180,15 @@ class PostCreateViewModel @Inject constructor( val response = imageList.let { communityRepository.patchPost(_post.value.id, boardId, titleBody, contentBody, anonymous, imageList) } - _createdPostId.value = response.id + when (response) { + is NetworkResult.Success -> { + _createdPostId.value = response.body.id + } + else -> { + _postCreateEvent.emit(PostCreateEvent.UploadPostFailed) + return@launch + } + } }.onSuccess { _postCreateEvent.emit(PostCreateEvent.UploadPostSuccess) }.onFailure { diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostDetailViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostDetailViewModel.kt index 1340cb34..ef51d724 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostDetailViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostDetailViewModel.kt @@ -10,6 +10,7 @@ import androidx.paging.cachedIn import com.wafflestudio.siksha2.models.Board import com.wafflestudio.siksha2.models.Comment import com.wafflestudio.siksha2.models.Post +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.CommunityRepository import com.wafflestudio.siksha2.repositories.pagingsource.CommentPagingSource import dagger.hilt.android.lifecycle.HiltViewModel @@ -21,7 +22,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import retrofit2.HttpException import javax.inject.Inject @ExperimentalCoroutinesApi @@ -34,9 +34,6 @@ class PostDetailViewModel @Inject constructor( private val _postUiState = MutableStateFlow(PostUiState.Loading) val postUiState: StateFlow = _postUiState - private val _board = MutableStateFlow(Board.Empty) - val board: StateFlow = _board - val commentPagingData = Pager( config = PagingConfig( pageSize = CommentPagingSource.ITEMS_PER_PAGE @@ -63,23 +60,31 @@ class PostDetailViewModel @Inject constructor( private fun refreshPost(postId: Long) { viewModelScope.launch { - runCatching { - val post = communityRepository.getPost(postId) - if (!post.available) { - _postUiState.value = PostUiState.Failed("신고가 누적되어 숨겨진 게시글입니다.") - return@runCatching - } - _postUiState.value = PostUiState.Success(post) - _board.value = communityRepository.getBoard(post.boardId) - }.onFailure { throwable -> - val errorMessage = (throwable as? HttpException)?.let { - when (it.code()) { - 404 -> "존재하지 않는 글입니다." - else -> "게시글을 불러올 수 없습니다." + communityRepository.getPost(postId) + .onSuccess { post -> + if (!post.available) { + _postUiState.value = PostUiState.Failed("신고가 누적되어 숨겨진 게시글입니다.") + return@onSuccess } - } ?: "게시글을 불러올 수 없습니다." - _postUiState.value = PostUiState.Failed(errorMessage) - } + launch { + communityRepository.getBoard(post.boardId) + .onSuccess { board -> + _postUiState.value = PostUiState.Success(post, board) + } + .onFailure { message -> + _postUiState.value = PostUiState.Failed(message) + } + .onError { + _postUiState.value = PostUiState.Failed("게시판을 불러올 수 없습니다.") + } + } + } + .onFailure { message -> + _postUiState.value = PostUiState.Failed(message) + } + .onError { + _postUiState.value = PostUiState.Failed("게시글을 불러올 수 없습니다.") + } } } @@ -87,43 +92,46 @@ class PostDetailViewModel @Inject constructor( if (content.isEmpty()) return val post = (postUiState.value as? PostUiState.Success)?.post ?: return viewModelScope.launch { - runCatching { - communityRepository.addCommentToPost(post.id, content, isAnonymous) - }.onSuccess { - _postDetailEvent.emit(PostDetailEvent.AddCommentSuccess) - refreshPost(post.id) - }.onFailure { - _postDetailEvent.emit(PostDetailEvent.AddCommentFailed) + when (communityRepository.addCommentToPost(post.id, content, isAnonymous)) { + is NetworkResult.Success -> { + _postDetailEvent.emit(PostDetailEvent.AddCommentSuccess) + refreshPost(post.id) + } + else -> { + _postDetailEvent.emit(PostDetailEvent.AddCommentFailed) + } } } } fun togglePostLike() { val post = (postUiState.value as? PostUiState.Success)?.post ?: return + val board = (postUiState.value as? PostUiState.Success)?.board ?: return viewModelScope.launch { - runCatching { - val updatedPost = when (post.isLiked) { - true -> communityRepository.unlikePost(post.id) - false -> communityRepository.likePost(post.id) + val togglePostListResponse = when (post.isLiked) { + true -> communityRepository.unlikePost(post.id) + false -> communityRepository.likePost(post.id) + } + when (togglePostListResponse) { + is NetworkResult.Success -> { + _postUiState.value = PostUiState.Success(togglePostListResponse.body, board) } - _postUiState.value = PostUiState.Success(updatedPost) - }.onFailure { - // TODO: 예외 처리 + is NetworkResult.Failure -> _postUiState.value = PostUiState.Failed(togglePostListResponse.message) + is NetworkResult.NetworkError -> _postUiState.value = PostUiState.Failed("네트워크 연결이 불안정합니다.") + else -> _postUiState.value = PostUiState.Failed("알 수 없는 오류가 발생했습니다.") } } } fun toggleCommentLike(comment: Comment) { viewModelScope.launch { - runCatching { - when (comment.isLiked) { - true -> communityRepository.unlikeComment(comment.id) - false -> communityRepository.likeComment(comment.id) - } - }.onSuccess { - _postDetailEvent.emit(PostDetailEvent.ToggleCommentLikeSuccess) - }.onFailure { - _postDetailEvent.emit(PostDetailEvent.ToggleCommentLikeFailed) + val response = when (comment.isLiked) { + true -> communityRepository.unlikeComment(comment.id) + false -> communityRepository.likeComment(comment.id) + } + when (response) { + is NetworkResult.Success -> _postDetailEvent.emit(PostDetailEvent.ToggleCommentLikeSuccess) + else -> _postDetailEvent.emit(PostDetailEvent.ToggleCommentLikeFailed) } } } @@ -164,7 +172,7 @@ class PostDetailViewModel @Inject constructor( } sealed interface PostUiState { - class Success(val post: Post) : PostUiState + class Success(val post: Post, val board: Board) : PostUiState class Failed(val errorMessage: String) : PostUiState object Loading : PostUiState } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostListViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostListViewModel.kt index af6ca23a..8b498a7b 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostListViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostListViewModel.kt @@ -11,6 +11,7 @@ import androidx.paging.filter import androidx.paging.map import com.wafflestudio.siksha2.models.Board import com.wafflestudio.siksha2.models.Post +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.CommunityRepository import com.wafflestudio.siksha2.repositories.pagingsource.PostPagingSource.Companion.ITEMS_PER_PAGE import com.wafflestudio.siksha2.utils.Selectable @@ -26,7 +27,6 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import java.io.IOException import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -78,13 +78,16 @@ class PostListViewModel @Inject constructor( fun getBoards() { viewModelScope.launch { - try { - _boards.value = communityRepository.getBoards().map { board -> - board.toDataWithState(false) + when (val response = communityRepository.getBoards()) { + is NetworkResult.Success -> { + _boards.value = response.body.map { board -> + board.toDataWithState(false) + } + selectBoard(0) + } + else -> { + // TODO: error handling } - selectBoard(0) - } catch (e: IOException) { - // TODO: error handler } } } @@ -98,15 +101,16 @@ class PostListViewModel @Inject constructor( fun fetchTrendingPosts() { viewModelScope.launch { _trendingPostsUiState.value = TrendingPostsUiState.Loading - runCatching { - val trendingPosts = communityRepository.getTrendingPosts() - _trendingPostsUiState.value = if (trendingPosts.isNotEmpty()) { - TrendingPostsUiState.Success(communityRepository.getTrendingPosts()) - } else { - TrendingPostsUiState.Failed + when (val response = communityRepository.getTrendingPosts()) { + is NetworkResult.Success -> { + val trendingPosts = response.body + _trendingPostsUiState.value = if (trendingPosts.isNotEmpty()) { + TrendingPostsUiState.Success(trendingPosts) + } else { + TrendingPostsUiState.Failed + } } - }.onFailure { - _trendingPostsUiState.value = TrendingPostsUiState.Failed + else -> _trendingPostsUiState.value = TrendingPostsUiState.Failed } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostReportViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostReportViewModel.kt index 9d2d7688..1d49bb90 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostReportViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/PostReportViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wafflestudio.siksha2.models.User +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.CommunityRepository import com.wafflestudio.siksha2.repositories.UserStatusManager import dagger.hilt.android.lifecycle.HiltViewModel @@ -12,7 +13,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch -import retrofit2.HttpException import javax.inject.Inject @HiltViewModel @@ -36,30 +36,22 @@ class PostReportViewModel @Inject constructor( private fun fetchUser() { viewModelScope.launch { - runCatching { - _user.value = userStatusManager.getUserData() - }.onFailure { - // TODO: 예외 대응 필요 + when (val response = userStatusManager.getUserData()) { + is NetworkResult.Success -> _user.value = response.body + is NetworkResult.Failure -> _postReportEvent.emit(PostReportEvent.ReportPostFailed(response.message)) + is NetworkResult.NetworkError -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("네트워크 연결이 불안정합니다.")) + else -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("알 수 없는 오류가 발생했습니다.")) } } } fun reportPost(reportContent: String) { viewModelScope.launch { - runCatching { - communityRepository.reportPost(postId, reportContent) - }.onSuccess { - _postReportEvent.emit(PostReportEvent.ReportPostSuccess) - }.onFailure { throwable -> - when (throwable) { - is HttpException -> { - when (throwable.code()) { - 409 -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("이미 신고한 게시글입니다.")) - else -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("알 수 없는 오류입니다.")) - } - } - else -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("알 수 없는 오류입니다.")) - } + when (val response = communityRepository.reportPost(postId, reportContent)) { + is NetworkResult.Success -> _postReportEvent.emit(PostReportEvent.ReportPostSuccess) + is NetworkResult.Failure -> _postReportEvent.emit(PostReportEvent.ReportPostFailed(response.message)) + is NetworkResult.NetworkError -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("네트워크 연결이 불안정합니다.")) + else -> _postReportEvent.emit(PostReportEvent.ReportPostFailed("알 수 없는 오류가 발생했습니다.")) } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/UserPostListViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/UserPostListViewModel.kt index e609bbe9..97eed77f 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/UserPostListViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/community/UserPostListViewModel.kt @@ -10,6 +10,7 @@ import androidx.paging.cachedIn import androidx.paging.map import com.wafflestudio.siksha2.models.Board import com.wafflestudio.siksha2.models.Post +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.CommunityRepository import com.wafflestudio.siksha2.repositories.pagingsource.PostPagingSource import com.wafflestudio.siksha2.utils.Selectable @@ -18,10 +19,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import java.io.IOException import javax.inject.Inject @HiltViewModel @@ -59,13 +58,15 @@ class UserPostListViewModel@Inject constructor( } private suspend fun getBoards() { - try { - // Fetch the boards and map them with the state - _boards.value = communityRepository.getBoards().mapIndexed { index, board -> - board.toDataWithState(index == 0) // Always select the first board + when (val response = communityRepository.getBoards()) { + is NetworkResult.Success -> { + _boards.value = response.body.mapIndexed { index, board -> + board.toDataWithState(index == 0) // Always select the first board + } + } + else -> { + // TODO: Error handling } - } catch (e: IOException) { - // TODO: error handler } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantFragment.kt index 8a13767a..f14d0599 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantFragment.kt @@ -3,7 +3,6 @@ package com.wafflestudio.siksha2.ui.main.restaurant import android.animation.ObjectAnimator import android.os.Bundle import android.view.* -import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -14,6 +13,7 @@ import com.wafflestudio.siksha2.R import com.wafflestudio.siksha2.components.CalendarSelectView import com.wafflestudio.siksha2.databinding.FragmentDailyRestaurantBinding import com.wafflestudio.siksha2.models.MealsOfDay +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.ui.main.MainFragmentDirections import com.wafflestudio.siksha2.ui.restaurantInfo.RestaurantInfoBottomSheet import com.wafflestudio.siksha2.utils.KakaoLinkHelper @@ -22,7 +22,6 @@ import com.wafflestudio.siksha2.utils.setVisibleOrGone import com.wafflestudio.siksha2.utils.showToast import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import java.io.IOException import java.time.LocalDate import java.time.LocalTime import kotlin.math.abs @@ -148,10 +147,11 @@ class DailyRestaurantFragment : Fragment() { }, onMenuItemToggleLikeClickListener = { menuId, isCurrentlyLiked -> viewLifecycleOwner.lifecycleScope.launch { - try { - vm.toggleMenuLike(menuId, isCurrentlyLiked) - } catch (e: IOException) { - showToast(getString(R.string.common_network_error), Toast.LENGTH_SHORT) + when (val response = vm.toggleMenuLike(menuId, isCurrentlyLiked)) { + is NetworkResult.Success -> { } + is NetworkResult.Failure -> showToast(response.message) + is NetworkResult.NetworkError -> showToast(getString(R.string.common_network_error)) + else -> showToast(getString(R.string.common_unknown_error)) } } }, diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantViewModel.kt index a875f419..30e88db1 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/restaurant/DailyRestaurantViewModel.kt @@ -6,8 +6,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import com.wafflestudio.siksha2.models.MealsOfDay +import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.models.MenuGroup import com.wafflestudio.siksha2.models.RestaurantInfo +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.MenuRepository import com.wafflestudio.siksha2.repositories.RestaurantRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -74,8 +76,8 @@ class DailyRestaurantViewModel @Inject constructor( } } - suspend fun toggleMenuLike(id: Long, isCurrentlyLiked: Boolean) { - when (isCurrentlyLiked) { + suspend fun toggleMenuLike(id: Long, isCurrentlyLiked: Boolean): NetworkResult { + return when (isCurrentlyLiked) { true -> menuRepository.unlikeMenuById(id) false -> menuRepository.likeMenuById(id) } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingViewModel.kt index fa9eebb1..939299bb 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/SettingViewModel.kt @@ -10,6 +10,7 @@ import com.wafflestudio.siksha2.BuildConfig import com.wafflestudio.siksha2.models.RestaurantInfo import com.wafflestudio.siksha2.models.RestaurantOrder import com.wafflestudio.siksha2.models.User +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.RestaurantRepository import com.wafflestudio.siksha2.repositories.UserStatusManager import com.wafflestudio.siksha2.utils.ImageUtil.getCompressedImage @@ -20,7 +21,6 @@ import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody -import retrofit2.HttpException import javax.inject.Inject @HiltViewModel @@ -44,32 +44,34 @@ class SettingViewModel @Inject constructor( init { viewModelScope.launch { - runCatching { - _userData.value = userStatusManager.getUserData() - checkAppVersion() - }.onFailure { - // TODO: 유저 정보 받아오지 못했을 때 처리 필요 + when (val response = userStatusManager.getUserData()) { + is NetworkResult.Success -> _userData.value = response.body + is NetworkResult.Failure -> _settingEvent.emit(SettingEvent.ChangeProfileFailed(response.message)) + is NetworkResult.NetworkError -> _settingEvent.emit(SettingEvent.ChangeProfileFailed("네트워크 연결이 불안정합니다.")) + else -> _settingEvent.emit(SettingEvent.ChangeProfileFailed("알 수 없는 오류가 발생했습니다.")) } + checkAppVersion() } } private suspend fun checkAppVersion() { - val version = userStatusManager.getVersion() - - val latestVersion = version.version - val minVersion = version.minVersion + when (val response = userStatusManager.getVersion()) { + is NetworkResult.Success -> { + val version = response.body + val latestVersion = version.version + val minVersion = version.minVersion + if (!isValidVersion(latestVersion) || !isValidVersion(minVersion) || !isValidVersion(packageVersion)) { + _isLatestAppVersion.value = false + return + } + val latestVersionCode = versionToLong(latestVersion) + val minVersionCode = versionToLong(minVersion) + val packageVersionCode = versionToLong(packageVersion) - // TODO: 버전이 잘못된 pattern을 가졌을 때의 처리 (필요한가?) - if (!isValidVersion(latestVersion) || !isValidVersion(minVersion) || !isValidVersion(packageVersion)) { - _isLatestAppVersion.value = false - return + _isLatestAppVersion.value = packageVersionCode in minVersionCode..latestVersionCode + } + else -> { } } - - val latestVersionCode = versionToLong(latestVersion) - val minVersionCode = versionToLong(minVersion) - val packageVersionCode = versionToLong(packageVersion) - - _isLatestAppVersion.value = packageVersionCode in minVersionCode..latestVersionCode } private fun versionToLong(version: String): Long { @@ -128,13 +130,12 @@ class SettingViewModel @Inject constructor( profileUrlCache = _userData.value?.profileUrl } - private suspend fun getNicknameToUpdate(nickname: String): String? { + private suspend fun getNicknameToUpdate(nickname: String): NetworkResult? { val currentNickname = _userData.value?.nickname - if (currentNickname == nickname) { - return null + return if (currentNickname == nickname) { + null } else { - userStatusManager.checkNickname(nickname) - return nickname + userStatusManager.checkNickname(nickname).map { _ -> nickname } } } @@ -144,7 +145,7 @@ class SettingViewModel @Inject constructor( return profileUrlCache.let { val uri = Uri.parse(it) getCompressedImage(context, uri) - }?.let { file -> + }.let { file -> val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) MultipartBody.Part.createFormData("image", file.name, requestBody) } @@ -152,43 +153,46 @@ class SettingViewModel @Inject constructor( fun patchUserData(context: Context, imageChanged: Boolean, nickname: String) { viewModelScope.launch { - runCatching { - if (nickname.isEmpty()) { - _settingEvent.emit(SettingEvent.ChangeProfileFailed("닉네임 칸이 비어있습니다.")) - return@runCatching + if (nickname.isEmpty()) { + _settingEvent.emit(SettingEvent.ChangeProfileFailed("닉네임 칸이 비어있습니다.")) + return@launch + } + + val nicknameToUpdate: String? + when (val nicknameToUpdateResponse = getNicknameToUpdate(nickname)) { + is NetworkResult.Failure -> { + _settingEvent.emit(SettingEvent.ChangeProfileFailed(nicknameToUpdateResponse.message)) + return@launch + } + is NetworkResult.NetworkError -> { + _settingEvent.emit(SettingEvent.ChangeProfileFailed("네트워크 연결이 불안정합니다.")) + return@launch + } + is NetworkResult.UnknownError -> { + _settingEvent.emit(SettingEvent.ChangeProfileFailed("알 수 없는 오류가 발생했습니다.")) + return@launch } + is NetworkResult.Success -> { nicknameToUpdate = nicknameToUpdateResponse.body } + else -> nicknameToUpdate = null + } + val imageToUpdate = getImageToUpdate(context, imageChanged) - val nicknameToUpdate = getNicknameToUpdate(nickname) - val imageToUpdate = getImageToUpdate(context, imageChanged) + if (nicknameToUpdate == null && !imageChanged) { + _settingEvent.emit(SettingEvent.ChangeProfileFailed("수정 사항이 없습니다.")) + return@launch + } - if (nicknameToUpdate == null && !imageChanged) { - _settingEvent.emit(SettingEvent.ChangeProfileFailed("수정 사항이 없습니다.")) - return@runCatching + val isDefaultImage = profileUrlCache == null + when (val response = userStatusManager.updateUserProfile(nicknameToUpdate, isDefaultImage, imageToUpdate)) { + is NetworkResult.Success -> { + _userData.value = response.body + _settingEvent.emit(SettingEvent.ChangeProfileSuccess) } - - val isDefaultImage = profileUrlCache == null - val updatedUserData = userStatusManager.updateUserProfile(nicknameToUpdate, isDefaultImage, imageToUpdate) - _userData.value = updatedUserData - }.onFailure { - when (it) { - is HttpException -> { - when (it.code()) { - 409 -> { - _settingEvent.emit(SettingEvent.ChangeProfileFailed("이미 존재하는 닉네임입니다.")) - } - - else -> { - _settingEvent.emit(SettingEvent.ChangeProfileFailed("일시적인 오류가 발생했습니다.")) - } - } - } - - else -> { - _settingEvent.emit(SettingEvent.ChangeProfileFailed("일시적인 오류가 발생했습니다.")) - } + is NetworkResult.Failure -> { + _settingEvent.emit(SettingEvent.ChangeProfileFailed(response.message)) } - }.onSuccess { - _settingEvent.emit(SettingEvent.ChangeProfileSuccess) + is NetworkResult.NetworkError -> _settingEvent.emit(SettingEvent.ChangeProfileFailed("네트워크 연결이 불안정합니다.")) + else -> _settingEvent.emit(SettingEvent.ChangeProfileFailed("알 수 없는 오류가 발생했습니다.")) } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/userAccount/UserProfileFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/userAccount/UserProfileFragment.kt index 0fd77533..10febf4c 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/userAccount/UserProfileFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/userAccount/UserProfileFragment.kt @@ -25,6 +25,7 @@ import com.wafflestudio.siksha2.ui.main.setting.SettingEvent import com.wafflestudio.siksha2.ui.main.setting.SettingViewModel import com.wafflestudio.siksha2.utils.showToast import kotlinx.coroutines.launch +import timber.log.Timber class UserProfileFragment : Fragment() { private lateinit var binding: FragmentUserProfileBinding @@ -119,6 +120,7 @@ class UserProfileFragment : Fragment() { is SettingEvent.ChangeProfileFailed -> { showToast(it.errorMessage) + Timber.d("ChangeProfileFailed") } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/voc/VocFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/voc/VocFragment.kt index 4ba5a057..7c289867 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/voc/VocFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/main/setting/voc/VocFragment.kt @@ -11,12 +11,11 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.wafflestudio.siksha2.R import com.wafflestudio.siksha2.databinding.FragmentVocBinding +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.UserStatusManager import com.wafflestudio.siksha2.utils.showToast import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch -import timber.log.Timber -import java.io.IOException import javax.inject.Inject @AndroidEntryPoint @@ -34,11 +33,14 @@ class VocFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { lifecycleScope.launch { - try { - val userData = userStatusManager.getUserData() - binding.idText.text = "ID " + userData.id - } catch (e: IOException) { - showToast("네트워크 연결이 불안정합니다.") + when (val response = userStatusManager.getUserData()) { + is NetworkResult.Success -> { + val userData = response.body + binding.idText.text = "ID " + userData.id + } + is NetworkResult.Failure -> showToast(response.message) + is NetworkResult.NetworkError -> showToast(getString(R.string.common_network_error)) + else -> showToast(getString(R.string.common_unknown_error)) } } binding.commentEdit.filters = binding.commentEdit.filters + InputFilter.LengthFilter(500) @@ -62,13 +64,14 @@ class VocFragment : Fragment() { binding.submitButton.setOnClickListener { lifecycleScope.launch { - try { - userStatusManager.sendVoc(voc = binding.commentEdit.text.toString(), platform = "Android") - showToast("문의가 정상적으로 등록되었습니다.") - findNavController().popBackStack() - } catch (e: IOException) { - Timber.e(e) - showToast("네트워크 연결이 불안정합니다.") + when (val response = userStatusManager.sendVoc(voc = binding.commentEdit.text.toString(), platform = "Android")) { + is NetworkResult.Success -> { + showToast(getString(R.string.send_voc_success)) + findNavController().popBackStack() + } + is NetworkResult.Failure -> showToast(response.message) + is NetworkResult.NetworkError -> showToast(getString(R.string.common_network_error)) + else -> showToast(getString(R.string.common_unknown_error)) } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt index a2e859bd..799ccc28 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt @@ -23,12 +23,11 @@ import com.wafflestudio.siksha2.R import com.wafflestudio.siksha2.components.OnRatingChangeListener import com.wafflestudio.siksha2.components.ReviewImageView import com.wafflestudio.siksha2.databinding.FragmentLeaveReviewBinding +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.utils.hasFinalConsInKr import com.wafflestudio.siksha2.utils.setVisibleOrGone import com.wafflestudio.siksha2.utils.showToast import kotlinx.coroutines.launch -import okio.IOException -import retrofit2.HttpException class LeaveReviewFragment : Fragment() { private lateinit var binding: FragmentLeaveReviewBinding @@ -139,27 +138,30 @@ class LeaveReviewFragment : Fragment() { binding.submitButton.setOnClickListener { lifecycleScope.launch { - try { - vm.leaveReview( - context = requireContext(), - score = binding.rating.rating.toDouble(), - comment = binding.commentEdit.text.toString().ifEmpty { - binding.commentEdit.hint.toString() - } - ) - showToast("평가가 등록되었습니다.") - findNavController().popBackStack() - } catch (e: HttpException) { - // TODO: 서버에 400 이 더 적절하지 않을 지 믈어보기 - if (e.code() == 403) { - showToast("같은 메뉴에 리뷰를 여러 번 남길 수 없습니다.") + val response = vm.leaveReview( + context = requireContext(), + score = binding.rating.rating.toDouble(), + comment = binding.commentEdit.text.toString().ifEmpty { + binding.commentEdit.hint.toString() + } + ) + when (response) { + is NetworkResult.Success -> { + // showToast(R.string.leave_review_success.toString()) + showToast(getString(R.string.leave_review_success)) + findNavController().popBackStack() + } + is NetworkResult.Failure -> { + showToast(response.message) + } + is NetworkResult.NetworkError -> { + showToast(getString(R.string.common_network_error)) + } + else -> { + showToast(getString(R.string.common_unknown_error)) } - } catch (e: IOException) { - e.printStackTrace() - showToast("네트워크 연결이 불안정합니다.") - } finally { - vm.notifySendReviewEnd() } + vm.notifySendReviewEnd() } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt index 67e58330..cfe3666d 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt @@ -13,13 +13,13 @@ import androidx.paging.LoadState import androidx.recyclerview.widget.LinearLayoutManager import com.wafflestudio.siksha2.R import com.wafflestudio.siksha2.databinding.FragmentMenuDetailBinding +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.utils.dp import com.wafflestudio.siksha2.utils.showToast import com.wafflestudio.siksha2.utils.setVisibleOrGone import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import java.io.IOException import kotlin.math.round @AndroidEntryPoint @@ -185,10 +185,11 @@ class MenuDetailFragment : Fragment() { binding.menuLikeButton.setOnClickListener { vm.menu.value?.isLiked?.let { viewLifecycleOwner.lifecycleScope.launch { - try { - vm.toggleLike(args.menuId, it) - } catch (e: IOException) { - showToast(getString(R.string.common_network_error)) + when (val response = vm.toggleLike(args.menuId, it)) { + is NetworkResult.Success -> { } + is NetworkResult.Failure -> showToast(response.message) + is NetworkResult.NetworkError -> showToast(getString(R.string.common_network_error)) + else -> showToast(getString(R.string.common_unknown_error)) } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt index e5fb4e64..83486d32 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt @@ -9,6 +9,8 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.models.Review +import com.wafflestudio.siksha2.network.dto.LeaveReviewResult +import com.wafflestudio.siksha2.network.result.NetworkResult import com.wafflestudio.siksha2.repositories.MenuRepository import com.wafflestudio.siksha2.utils.ImageUtil import com.wafflestudio.siksha2.utils.showToast @@ -18,7 +20,7 @@ import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody -import java.io.IOException +import timber.log.Timber import javax.inject.Inject @HiltViewModel @@ -60,32 +62,37 @@ class MenuDetailViewModel @Inject constructor( fun refreshMenu(menuId: Long) { _networkResultState.value = State.LOADING viewModelScope.launch { - try { - _menu.value = menuRepository.getMenuById(menuId) - _networkResultState.value = State.SUCCESS - } catch (e: IOException) { - _networkResultState.value = State.FAILED + val result = menuRepository.getMenuById(menuId) + when (result) { + is NetworkResult.Success -> { + _menu.value = result.body + _networkResultState.value = State.SUCCESS + } + else -> _networkResultState.value = State.FAILED } } } fun refreshImages(menuId: Long) { viewModelScope.launch { - try { - val data = menuRepository.getFirstReviewPhotoByMenuId(menuId) - _imageCount.value = data.totalCount - val urlList = emptyList().toMutableList() - for (i in 0 until 3) { - if (i < data.result.size) { - data.result[i].etc?.images?.get(0)?.let { - urlList.add(it) + when (val response = menuRepository.getFirstReviewPhotoByMenuId(menuId)) { + is NetworkResult.Success -> { + val data = response.body + _imageCount.value = data.totalCount + val urlList = emptyList().toMutableList() + for (i in 0 until 3) { + if (i < data.result.size) { + data.result[i].etc?.images?.get(0)?.let { + urlList.add(it) + } } } + _imageUrlList.value = urlList + } + else -> { + _imageUrlList.value = emptyList() + _networkResultState.value = State.FAILED } - _imageUrlList.value = urlList - } catch (e: IOException) { - _imageUrlList.value = emptyList() - _networkResultState.value = State.FAILED } } } @@ -101,20 +108,20 @@ class MenuDetailViewModel @Inject constructor( fun getRecommendationReview(score: Long) { // TODO: LruCache 로 캐싱해놓고 꺼내쓰기 viewModelScope.launch { - try { - _commentHint.value = menuRepository.getReviewRecommendationComments(score) - } catch (e: IOException) { - _commentHint.value = "" + when (val response = menuRepository.getReviewRecommendationComments(score)) { + is NetworkResult.Success -> { + _commentHint.value = response.body.comment + } + else -> _commentHint.value = "" } } } fun refreshReviewDistribution(menuId: Long) { viewModelScope.launch { - try { - _reviewDistribution.value = menuRepository.getReviewDistribution(menuId) - } catch (e: IOException) { - _reviewDistribution.value = emptyList() + when (val response = menuRepository.getReviewDistribution(menuId)) { + is NetworkResult.Success -> _reviewDistribution.value = response.body.dist + else -> _reviewDistribution.value = emptyList() } } } @@ -147,17 +154,25 @@ class MenuDetailViewModel @Inject constructor( _leaveReviewState.value = ReviewState.WAITING } - suspend fun toggleLike(id: Long, isCurrentlyLiked: Boolean) { - val updatedMenu = when (isCurrentlyLiked) { + suspend fun toggleLike(id: Long, isCurrentlyLiked: Boolean): NetworkResult { + val menuUpdateResponse = when (isCurrentlyLiked) { true -> menuRepository.unlikeMenuById(id) false -> menuRepository.likeMenuById(id) } - _menu.postValue(updatedMenu) + when (menuUpdateResponse) { + is NetworkResult.Success -> { + _menu.postValue(menuUpdateResponse.body) + } + else -> { } + } + return menuUpdateResponse } - suspend fun leaveReview(context: Context, score: Double, comment: String) { - val menuId = _menu.value?.id ?: return - if (_imageUriList.value?.isNotEmpty() == true) { + suspend fun leaveReview(context: Context, score: Double, comment: String): NetworkResult? { + Timber.d("LeaveReview ${_menu.value?.id}") + val menuId = _menu.value?.id ?: return null + Timber.d("not null") + val response = if (_imageUriList.value?.isNotEmpty() == true) { context.showToast("이미지 압축 중입니다.") _leaveReviewState.value = ReviewState.COMPRESSING val imageList = _imageUriList.value?.map { @@ -173,6 +188,7 @@ class MenuDetailViewModel @Inject constructor( } else { menuRepository.leaveMenuReview(menuId, score, comment) } + return response } enum class State { diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewPagingSource.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewPagingSource.kt index dd031158..34a4cc43 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewPagingSource.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewPagingSource.kt @@ -5,8 +5,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import com.wafflestudio.siksha2.models.Review import com.wafflestudio.siksha2.network.SikshaApi -import okio.IOException -import retrofit2.HttpException +import com.wafflestudio.siksha2.network.result.NetworkResult class MenuReviewPagingSource( private val api: SikshaApi, @@ -15,22 +14,15 @@ class MenuReviewPagingSource( override suspend fun load(params: LoadParams): LoadResult { val key = params.key ?: STARTING_PAGE_INDEX - return try { - val response = api.fetchReviews(menuId, key, params.loadSize.toLong()) - val prevKey = if (key == 1L) null else key - 1 - val nextKey = if (response.result.isEmpty()) null else if (key == STARTING_PAGE_INDEX) key + params.loadSize / PAGE_LOAD_SIZE else key + 1 - - LoadResult.Page( - response.result, - prevKey, - nextKey - ) - } catch (e: HttpException) { - // TODO: 마지막 페이지 일 때 다음페이지 항상 404 뜨면서 페이징이 종료됨 - // 기능상 문제는 없지만 api에서 다음 페이지 유무 확인할 수 있도록 변경 요청하기 - return LoadResult.Error(e) - } catch (e: IOException) { - return LoadResult.Error(e) + return when (val response = api.fetchReviews(menuId, key, params.loadSize.toLong())) { + is NetworkResult.Success -> { + LoadResult.Page( + data = response.body.result, + prevKey = if (key == 1L) null else key - 1, + nextKey = if (response.body.result.isEmpty()) null else if (key == STARTING_PAGE_INDEX) key + params.loadSize / PAGE_LOAD_SIZE else key + 1 + ) + } + else -> LoadResult.Error(RuntimeException("")) } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewWithImagePagingSource.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewWithImagePagingSource.kt index 776a8543..f8054795 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewWithImagePagingSource.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuReviewWithImagePagingSource.kt @@ -5,8 +5,7 @@ import androidx.paging.PagingSource import androidx.paging.PagingState import com.wafflestudio.siksha2.models.Review import com.wafflestudio.siksha2.network.SikshaApi -import okio.IOException -import retrofit2.HttpException +import com.wafflestudio.siksha2.network.result.NetworkResult class MenuReviewWithImagePagingSource( private val api: SikshaApi, @@ -15,22 +14,15 @@ class MenuReviewWithImagePagingSource( override suspend fun load(params: LoadParams): LoadResult { val key = params.key ?: STARTING_PAGE_INDEX - return try { - val response = api.fetchReviewsWithImage(menuId, key, params.loadSize.toLong()) - val prevKey = if (key == 1L) null else key - 1 - val nextKey = if (response.result.isEmpty()) null else if (key == STARTING_PAGE_INDEX) key + params.loadSize / PAGE_LOAD_SIZE else key + 1 - - LoadResult.Page( - response.result, - prevKey, - nextKey - ) - } catch (e: HttpException) { - // TODO: 마지막 페이지 일 때 다음페이지 항상 404 뜨면서 페이징이 종료됨 - // 기능상 문제는 없지만 api에서 다음 페이지 유무 확인할 수 있도록 변경 요청하기 - return LoadResult.Error(e) - } catch (e: IOException) { - return LoadResult.Error(e) + return when (val response = api.fetchReviewsWithImage(menuId, key, params.loadSize.toLong())) { + is NetworkResult.Success -> { + LoadResult.Page( + data = response.body.result, + prevKey = if (key == 1L) null else key - 1, + nextKey = if (response.body.result.isEmpty()) null else if (key == STARTING_PAGE_INDEX) key + params.loadSize / PAGE_LOAD_SIZE else key + 1 + ) + } + else -> LoadResult.Error(RuntimeException("")) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7d46dda..98861836 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ 문의할 내용을 남겨주세요. 문의할 내용을 작성해주세요. 전송하기 + 문의가 정상적으로 등록되었습니다. 로그아웃 정말로 로그아웃 하시겠습니까? 로그아웃 @@ -40,6 +41,7 @@ \' 사진 추가 사진은 최대 3개까지 등록할 수 있습니다. + 평가가 등록되었습니다. 리뷰 아직 해당 메뉴의 리뷰가 없습니다.\n리뷰를 남겨보세요 @@ -54,6 +56,7 @@ 순서를 지정할 식당이 없습니다. 즐겨찾기에 추가된 식당이 없습니다.\n식당 탭에서 별을 눌러 추가해보세요. 네트워크 연결이 불안정합니다. + 알 수 없는 오류가 발생했습니다. 식단 평가 %1$d 자 / %2$d 자 돌아가기