diff --git a/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt new file mode 100644 index 00000000..9ce6bca0 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityModule.kt @@ -0,0 +1,22 @@ +package com.sopt.clody.core.network + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkConnectivityModule { + + @Provides + @Singleton + fun provideNetworkConnectivityObserver( + @ApplicationContext context: Context, + ): NetworkConnectivityObserver { + return NetworkConnectivityObserver(context) + } +} diff --git a/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt new file mode 100644 index 00000000..626e4b43 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/network/NetworkConnectivityObserver.kt @@ -0,0 +1,79 @@ +package com.sopt.clody.core.network + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * 네트워크 연결 상태를 관찰하는 Observer. + * + * - `Available`: 인터넷에 연결되어 있음 + * - `Unavailable`: 인터넷 연결이 끊긴 상태 + * + */ +class NetworkConnectivityObserver @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + /** + * 네트워크 상태를 실시간으로 스트리밍하는 Flow. + * + * - 최초 구독 시 현재 상태를 먼저 전송 -> + * - 이후 네트워크 변경 이벤트를 수신하여 상태를 전송 + * - 중복 상태 전송은 [distinctUntilChanged]로 방지 하도록 함. + */ + val networkStatus: Flow = callbackFlow { + val callback = object : ConnectivityManager.NetworkCallback() { + + /** + * 네트워크가 변경되었을 때 호출됨. + * 유효한 인터넷 연결이 있는지 확인하여 상태를 전송. + */ + override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) { + val hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + trySend(if (hasInternet) NetworkStatus.Available else NetworkStatus.Unavailable) + } + + /** + * 네트워크 연결이 완전히 끊겼을 때 호출. + */ + override fun onLost(network: Network) { + trySend(NetworkStatus.Unavailable) + } + } + + trySend(if (isCurrentlyAvailable()) NetworkStatus.Available else NetworkStatus.Unavailable) + + val request = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + connectivityManager.registerNetworkCallback(request, callback) + + awaitClose { + connectivityManager.unregisterNetworkCallback(callback) + } + }.distinctUntilChanged() + + /** + * 현재 활성 네트워크가 인터넷에 연결되어 있는지를 반환 + * + * @return 인터넷 연결 여부 + */ + private fun isCurrentlyAvailable(): Boolean { + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } +} diff --git a/app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt b/app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt new file mode 100644 index 00000000..9f28341a --- /dev/null +++ b/app/src/main/java/com/sopt/clody/core/network/NetworkStatus.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.core.network + +sealed class NetworkStatus { + data object Available : NetworkStatus() + data object Unavailable : NetworkStatus() +} diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt index be351600..457471f1 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasource/DiaryRemoteDataSource.kt @@ -1,6 +1,5 @@ package com.sopt.clody.data.remote.datasource -import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto @@ -11,13 +10,13 @@ import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto interface DiaryRemoteDataSource { - suspend fun writeDiary(lang: String, date: String, content: List): ApiResponse - suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse - suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse - suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse - suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse - suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse - suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse + suspend fun writeDiary(lang: String, date: String, content: List): Result + suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): Result + suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result + suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result + suspend fun getMonthlyCalendarData(year: Int, month: Int): Result + suspend fun getMonthlyDiary(year: Int, month: Int): Result + suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result + suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result + suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): Result } diff --git a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt index 4522e637..f898978d 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/datasourceimpl/DiaryRemoteDataSourceImpl.kt @@ -2,45 +2,41 @@ package com.sopt.clody.data.remote.datasourceimpl import com.sopt.clody.data.remote.api.DiaryService import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource -import com.sopt.clody.data.remote.dto.base.ApiResponse import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto import com.sopt.clody.data.remote.dto.request.WriteDiaryRequestDto -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.dto.response.DraftDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto +import com.sopt.clody.data.remote.util.safeApiCall +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import javax.inject.Inject class DiaryRemoteDataSourceImpl @Inject constructor( private val diaryService: DiaryService, + private val errorMessageProvider: ErrorMessageProvider, ) : DiaryRemoteDataSource { - override suspend fun writeDiary(lang: String, date: String, content: List): ApiResponse = - diaryService.writeDiary(lang, WriteDiaryRequestDto(date, content)) - override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse = - diaryService.deleteDailyDiary(year = year, month = month, date = date) + override suspend fun writeDiary(lang: String, date: String, content: List) = + safeApiCall(errorMessageProvider) { diaryService.writeDiary(lang, WriteDiaryRequestDto(date, content)) } - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse = - diaryService.getDailyDiariesData(year = year, month = month, date = date) + override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.deleteDailyDiary(year, month, date) } - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse = - diaryService.getDiaryTime(year = year, month = month, date = date) + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.getDailyDiariesData(year, month, date) } - override suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse = - diaryService.getMonthlyCalendarData(year = year, month = month) + override suspend fun getDiaryTime(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.getDiaryTime(year, month, date) } - override suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse = - diaryService.getMonthlyDiary(year = year, month = month) + override suspend fun getMonthlyCalendarData(year: Int, month: Int) = + safeApiCall(errorMessageProvider) { diaryService.getMonthlyCalendarData(year, month) } - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse = - diaryService.getReplyDiary(year = year, month = month, date = date) + override suspend fun getMonthlyDiary(year: Int, month: Int) = + safeApiCall(errorMessageProvider) { diaryService.getMonthlyDiary(year, month) } - override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): ApiResponse = - diaryService.fetchDraftDiary(year = year, month = month, date = date) + override suspend fun getReplyDiary(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.getReplyDiary(year, month, date) } - override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto): ApiResponse = - diaryService.saveDraftDiary(request) + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int) = + safeApiCall(errorMessageProvider) { diaryService.fetchDraftDiary(year, month, date) } + + override suspend fun saveDraftDiary(request: SaveDraftDiaryRequestDto) = + safeApiCall(errorMessageProvider) { diaryService.saveDraftDiary(request) } } diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt b/app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt new file mode 100644 index 00000000..caf6a66b --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/util/ApiError.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.data.remote.util + +data class ApiError( + override val message: String, +) : Exception() diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt b/app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt deleted file mode 100644 index 798737f1..00000000 --- a/app/src/main/java/com/sopt/clody/data/remote/util/NetworkUtil.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.sopt.clody.data.remote.util - -import android.net.ConnectivityManager -import android.net.NetworkCapabilities - -class NetworkUtil(private val connectivityManager: ConnectivityManager) { - fun isNetworkAvailable(): Boolean { - val network = connectivityManager.activeNetwork ?: return false - val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false - return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } -} diff --git a/app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt b/app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt new file mode 100644 index 00000000..612c20d2 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/data/remote/util/SafeApiCall.kt @@ -0,0 +1,25 @@ +package com.sopt.clody.data.remote.util + +import com.sopt.clody.data.remote.dto.base.ApiResponse +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider +import java.io.IOException +import kotlin.coroutines.cancellation.CancellationException + +suspend fun safeApiCall( + errorMessageProvider: ErrorMessageProvider, + action: suspend () -> ApiResponse, +): Result { + return try { + val response = action() + response.data?.let { Result.success(it) } + ?: Result.failure(ApiError(errorMessageProvider.getTemporaryError())) + } catch (exception: Throwable) { + if (exception is CancellationException) throw exception + + val error = when (exception) { + is IOException -> ApiError(errorMessageProvider.getNetworkError()) + else -> ApiError(errorMessageProvider.getTemporaryError()) + } + Result.failure(error) + } +} diff --git a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt index f09d9787..98d02247 100644 --- a/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt +++ b/app/src/main/java/com/sopt/clody/data/repositoryimpl/DiaryRepositoryImpl.kt @@ -2,101 +2,39 @@ package com.sopt.clody.data.repositoryimpl import com.sopt.clody.data.remote.datasource.DiaryRemoteDataSource import com.sopt.clody.data.remote.dto.request.SaveDraftDiaryRequestDto -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto -import com.sopt.clody.data.remote.util.handleApiResponse -import com.sopt.clody.domain.model.DraftDiaryContents import com.sopt.clody.domain.repository.DiaryRepository -import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import retrofit2.HttpException import javax.inject.Inject class DiaryRepositoryImpl @Inject constructor( private val diaryRemoteDataSource: DiaryRemoteDataSource, ) : DiaryRepository { - override suspend fun writeDiary(lang: String, date: String, content: List): Result = - runCatching { - diaryRemoteDataSource.writeDiary(lang, date, content).handleApiResponse().getOrThrow() - } - override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int): Result = - runCatching { - diaryRemoteDataSource.deleteDailyDiary(year, month, day).handleApiResponse().getOrThrow() - } + override suspend fun writeDiary(lang: String, date: String, content: List) = + diaryRemoteDataSource.writeDiary(lang, date, content) - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result = - runCatching { - diaryRemoteDataSource.getDailyDiariesData(year, month, date).handleApiResponse() - }.getOrElse { exception -> - val errorMessage = when (exception) { - is HttpException -> { - when (exception.code()) { - in 400..499 -> ErrorMessages.FAILURE_TEMPORARY_MESSAGE - in 500..599 -> ErrorMessages.FAILURE_SERVER_MESSAGE - else -> ErrorMessages.UNKNOWN_ERROR - } - } - else -> exception.message ?: ErrorMessages.UNKNOWN_ERROR - } - Result.failure(Exception(errorMessage)) - } + override suspend fun deleteDailyDiary(year: Int, month: Int, day: Int) = + diaryRemoteDataSource.deleteDailyDiary(year, month, day) - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result = - runCatching { - diaryRemoteDataSource.getDiaryTime(year, month, date).data - } + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.getDailyDiariesData(year, month, date) - override suspend fun getMonthlyCalendarData(year: Int, month: Int): Result = - runCatching { - diaryRemoteDataSource.getMonthlyCalendarData(year, month).handleApiResponse() - }.getOrElse { exception -> - val errorMessage = when (exception) { - is HttpException -> { - when (exception.code()) { - in 400..499 -> ErrorMessages.FAILURE_TEMPORARY_MESSAGE - in 500..599 -> ErrorMessages.FAILURE_SERVER_MESSAGE - else -> ErrorMessages.UNKNOWN_ERROR - } - } + override suspend fun getDiaryTime(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.getDiaryTime(year, month, date) - else -> exception.message ?: ErrorMessages.UNKNOWN_ERROR - } - Result.failure(Exception(errorMessage)) - } + override suspend fun getMonthlyCalendarData(year: Int, month: Int) = + diaryRemoteDataSource.getMonthlyCalendarData(year, month) - override suspend fun getMonthlyDiary(year: Int, month: Int): Result = - runCatching { - diaryRemoteDataSource.getMonthlyDiary(year, month).handleApiResponse().getOrThrow() - } + override suspend fun getMonthlyDiary(year: Int, month: Int) = + diaryRemoteDataSource.getMonthlyDiary(year, month) - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result = - runCatching { - val response = diaryRemoteDataSource.getReplyDiary(year, month, date).data - if (response.content == null) { - throw IllegalStateException(FAILURE_TEMPORARY_MESSAGE) - } - response - } + override suspend fun getReplyDiary(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.getReplyDiary(year, month, date) - override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int): Result = - runCatching { - diaryRemoteDataSource - .fetchDraftDiary(year, month, date) - .handleApiResponse() - .getOrThrow() - .toDomain() - } + override suspend fun fetchDraftDiary(year: Int, month: Int, date: Int) = + diaryRemoteDataSource.fetchDraftDiary(year, month, date).map { it.toDomain() } - override suspend fun saveDraftDiary(date: String, contents: List): Result = - runCatching { - diaryRemoteDataSource - .saveDraftDiary(SaveDraftDiaryRequestDto(date = date, draftDiaries = contents)) - .handleApiResponse() - .getOrThrow() - } + override suspend fun saveDraftDiary(date: String, contents: List): Result { + val request = SaveDraftDiaryRequestDto(date, contents) + return diaryRemoteDataSource.saveDraftDiary(request) + } } diff --git a/app/src/main/java/com/sopt/clody/di/NetworkModule.kt b/app/src/main/java/com/sopt/clody/di/NetworkModule.kt index da77ddda..3fc7aeb5 100644 --- a/app/src/main/java/com/sopt/clody/di/NetworkModule.kt +++ b/app/src/main/java/com/sopt/clody/di/NetworkModule.kt @@ -1,12 +1,10 @@ package com.sopt.clody.di import android.content.Context -import android.net.ConnectivityManager import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.sopt.clody.BuildConfig import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.util.AuthInterceptor -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.data.remote.util.TimeZoneInterceptor import com.sopt.clody.domain.repository.TokenReissueRepository import dagger.Module @@ -79,11 +77,4 @@ object NetworkModule { .client(okHttpClient) .build() } - - @Provides - @Singleton - fun provideNetworkUtil(@ApplicationContext context: Context): NetworkUtil { - val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - return NetworkUtil(connectivityManager) - } } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt index 929c2e1f..382be324 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/FetchDraftDiaryUseCase.kt @@ -1,13 +1,11 @@ package com.sopt.clody.domain.usecase -import com.sopt.clody.domain.model.DraftDiaryContents import com.sopt.clody.domain.repository.DiaryRepository import javax.inject.Inject class FetchDraftDiaryUseCase @Inject constructor( - private val repository: DiaryRepository, + private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(year: Int, month: Int, day: Int): Result { - return repository.fetchDraftDiary(year, month, day) - } + suspend operator fun invoke(year: Int, month: Int, day: Int) = + diaryRepository.fetchDraftDiary(year, month, day) } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt index a1b2ffa6..e37e19cc 100644 --- a/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt +++ b/app/src/main/java/com/sopt/clody/domain/usecase/SaveDraftDiaryUseCase.kt @@ -6,7 +6,6 @@ import javax.inject.Inject class SaveDraftDiaryUseCase @Inject constructor( private val diaryRepository: DiaryRepository, ) { - suspend operator fun invoke(date: String, contents: List): Result { - return diaryRepository.saveDraftDiary(date, contents) - } + suspend operator fun invoke(date: String, contents: List) = + diaryRepository.saveDraftDiary(date, contents) } diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt new file mode 100644 index 00000000..56e43446 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/usecase/WriteDiaryUseCase.kt @@ -0,0 +1,11 @@ +package com.sopt.clody.domain.usecase + +import com.sopt.clody.domain.repository.DiaryRepository +import javax.inject.Inject + +class WriteDiaryUseCase @Inject constructor( + private val diaryRepository: DiaryRepository, +) { + suspend operator fun invoke(lang: String, date: String, content: List) = + diaryRepository.writeDiary(lang, date, content) +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt index ffe4a5ae..0fde1ef3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/SignUpViewModel.kt @@ -8,20 +8,23 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.airbnb.mvrx.withState import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.login.LoginSdk +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.datastore.OAuthDataStore import com.sopt.clody.data.datastore.OAuthProvider import com.sopt.clody.data.remote.dto.request.SignUpRequestDto import com.sopt.clody.data.remote.dto.response.SignUpResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.setting.screen.SettingOptionUrls import com.sopt.clody.presentation.utils.language.LanguageProvider +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel.Factory.BUFFERED +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow @@ -34,8 +37,9 @@ class SignUpViewModel @AssistedInject constructor( private val tokenRepository: TokenRepository, private val fcmTokenProvider: FcmTokenProvider, private val oAuthDataStore: OAuthDataStore, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -145,8 +149,8 @@ class SignUpViewModel @AssistedInject constructor( private suspend fun signUp(context: Context) { val state = withState(this@SignUpViewModel) { it } - if (!networkUtil.isNetworkAvailable()) { - setState { copy(errorMessage = "네트워크 연결을 확인해주세요.") } + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + setState { copy(errorMessage = errorMessageProvider.getNetworkCheckError()) } return } @@ -158,7 +162,7 @@ class SignUpViewModel @AssistedInject constructor( if (platform == OAuthProvider.GOOGLE) { val idToken = oAuthDataStore.getIdToken(platform = "google") if (idToken.isNullOrBlank()) { - setState { copy(errorMessage = "Google ID Token이 없습니다.", isLoading = false) } + setState { copy(errorMessage = errorMessageProvider.getGoogleIdTokenMissingError(), isLoading = false) } return } val request = SignUpRequestDto( @@ -172,7 +176,7 @@ class SignUpViewModel @AssistedInject constructor( } else { val idToken = oAuthDataStore.getIdToken(platform = "kakao") if (idToken.isNullOrBlank()) { - setState { copy(errorMessage = "Kakao ID Token이 없습니다.", isLoading = false) } + setState { copy(errorMessage = errorMessageProvider.getLoginFailedError(), isLoading = false) } return } val request = SignUpRequestDto( @@ -196,7 +200,7 @@ class SignUpViewModel @AssistedInject constructor( _sideEffects.send(SignUpContract.SignUpSideEffect.NavigateToTimeReminder) }, onFailure = { - setState { copy(errorMessage = "회원가입에 실패했어요~") } + setState { copy(errorMessage = errorMessageProvider.getSignupFailedError()) } }, ) setState { copy(isLoading = false) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt index 6e34cc55..07fba0d3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderViewModel.kt @@ -6,24 +6,25 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.extension.convertUTZtoKST -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class TimeReminderViewModel @Inject constructor( private val notificationRepository: NotificationRepository, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _timeReminderState = MutableStateFlow(TimeReminderState.Idle) @@ -34,8 +35,8 @@ class TimeReminderViewModel @Inject constructor( fun sendNotification(context: Context, isPermissionGranted: Boolean) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _timeReminderState.value = TimeReminderState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _timeReminderState.value = TimeReminderState.Failure(errorMessageProvider.getNetworkError()) return@launch } @@ -60,9 +61,9 @@ class TimeReminderViewModel @Inject constructor( }, onFailure = { error -> val errorMessage = if (error.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - error.localizedMessage ?: UNKNOWN_ERROR + error.localizedMessage ?: errorMessageProvider.getUnknownError() } _timeReminderState.value = TimeReminderState.Failure(errorMessage) }, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt index f3ea9ae2..dff813a9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListViewModel.kt @@ -2,14 +2,14 @@ package com.sopt.clody.presentation.ui.diarylist.screen import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.domain.repository.DiaryRepository -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.time.LocalDate import java.time.format.TextStyle @@ -19,7 +19,8 @@ import javax.inject.Inject @HiltViewModel class DiaryListViewModel @Inject constructor( private val diaryRepository: DiaryRepository, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _diaryListState = MutableStateFlow(DiaryListState.Idle) @@ -44,8 +45,8 @@ class DiaryListViewModel @Inject constructor( if (retryCount >= maxRetryCount) return _diaryListState.value = DiaryListState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _diaryListState.value = DiaryListState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _diaryListState.value = DiaryListState.Failure(errorMessageProvider.getNetworkError()) return@launch } val result = diaryRepository.getMonthlyDiary(year, month) @@ -57,12 +58,12 @@ class DiaryListViewModel @Inject constructor( onFailure = { retryCount++ if (retryCount >= maxRetryCount) { - DiaryListState.Failure(FAILURE_TEMPORARY_MESSAGE) + DiaryListState.Failure(errorMessageProvider.getTemporaryError()) } else { val errorMessage = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } DiaryListState.Failure(errorMessage) } @@ -85,8 +86,8 @@ class DiaryListViewModel @Inject constructor( fun deleteDailyDiary(year: Int, month: Int, day: Int) { _diaryDeleteState.value = DiaryDeleteState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() DiaryDeleteState.Failure(_failureDialogMessage.value) _showDiaryDeleteFailureDialog.value = true return@launch @@ -98,9 +99,9 @@ class DiaryListViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + it.localizedMessage ?: errorMessageProvider.getUnknownError() } _showDiaryDeleteFailureDialog.value = true DiaryDeleteState.Failure(_failureDialogMessage.value) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 71956140..a5f9d9a0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -197,7 +197,7 @@ fun HomeRoute( ClodyButton( text = stringResource(R.string.bottom_sheet_home_initial_draft_accept), onClick = { - homeViewModel.enableDraftAlarm(context) + homeViewModel.enableDraftAlarm() homeViewModel.updateFirstDraftUse(false) }, enabled = true, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 3518c1ba..fc5121fb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -1,14 +1,14 @@ package com.sopt.clody.presentation.ui.home.screen -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.core.fcm.FcmTokenProvider +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository @@ -16,10 +16,11 @@ import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState -import com.sopt.clody.presentation.utils.network.ErrorMessages +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.time.LocalDate import java.time.ZoneId @@ -29,10 +30,11 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val notificationRepository: NotificationRepository, - private val networkUtil: NetworkUtil, private val draftRepository: DraftRepository, private val fcmTokenProvider: FcmTokenProvider, private val reviewRepository: ReviewRepository, + private val errorMessageProvider: ErrorMessageProvider, + private val networkConnectivityObserver: NetworkConnectivityObserver, ) : ViewModel() { private val _calendarState = MutableStateFlow>(CalendarState.Idle) @@ -111,14 +113,14 @@ class HomeViewModel @Inject constructor( } } - fun setErrorState(isError: Boolean, message: String = ErrorMessages.FAILURE_TEMPORARY_MESSAGE) { + fun setErrorState(isError: Boolean, message: String = errorMessageProvider.getTemporaryError()) { _errorState.value = isError to message } fun loadCalendarData(year: Int, month: Int) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) return@launch } @@ -130,8 +132,8 @@ class HomeViewModel @Inject constructor( CalendarState.Success(it) }, onFailure = { exception -> - setErrorState(true, exception.message ?: ErrorMessages.UNKNOWN_ERROR) - CalendarState.Error(exception.message ?: ErrorMessages.UNKNOWN_ERROR) + setErrorState(true, errorMessageProvider.getTemporaryError()) + CalendarState.Error(errorMessageProvider.getTemporaryError()) }, ) } @@ -139,8 +141,8 @@ class HomeViewModel @Inject constructor( fun loadDailyDiariesData(year: Int, month: Int, date: Int) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) return@launch } @@ -156,8 +158,8 @@ class HomeViewModel @Inject constructor( DailyDiariesState.Success(dailyResponse) }, onFailure = { exception -> - setErrorState(true, exception.message ?: ErrorMessages.UNKNOWN_ERROR) - DailyDiariesState.Error(exception.message ?: ErrorMessages.UNKNOWN_ERROR) + setErrorState(true, errorMessageProvider.getTemporaryError()) + DailyDiariesState.Error(errorMessageProvider.getTemporaryError()) }, ) } @@ -178,7 +180,7 @@ class HomeViewModel @Inject constructor( DeleteDiaryState.Success }, onFailure = { - DeleteDiaryState.Failure(it.message ?: "Unknown error") + DeleteDiaryState.Failure(it.message ?: errorMessageProvider.getTemporaryError()) }, ) } @@ -254,10 +256,10 @@ class HomeViewModel @Inject constructor( return selected == today || selected == today.minusDays(1) } - fun enableDraftAlarm(context: Context) { + fun enableDraftAlarm() { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - setErrorState(true, ErrorMessages.FAILURE_NETWORK_MESSAGE) + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) return@launch } @@ -268,9 +270,13 @@ class HomeViewModel @Inject constructor( } } + private suspend fun isNetworkAvailable(): Boolean { + return networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + } + private suspend fun getNotificationInfo(): NotificationInfoResponseDto? { return notificationRepository.getNotificationInfo().getOrElse { - _draftAlarmChangeState.value = NotificationChangeState.Failure("알림 정보를 가져오는데 실패했습니다.") + _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) null } } @@ -293,7 +299,7 @@ class HomeViewModel @Inject constructor( _draftAlarmChangeState.value = NotificationChangeState.Success(it) }, onFailure = { - _draftAlarmChangeState.value = NotificationChangeState.Failure("이어쓰기 알림 설정에 실패했습니다.") + _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt index c7540425..8c866da6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/login/LoginViewModel.kt @@ -14,6 +14,7 @@ import com.sopt.clody.domain.repository.AuthRepository import com.sopt.clody.domain.repository.TokenRepository import com.sopt.clody.presentation.ui.login.LoginContract.LoginIntent import com.sopt.clody.presentation.utils.language.LanguageProvider +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -32,6 +33,7 @@ class LoginViewModel @AssistedInject constructor( private val fcmTokenProvider: FcmTokenProvider, private val oauthDataStore: OAuthDataStore, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : MavericksViewModel(initialState) { private val _intents = Channel(BUFFERED) @@ -68,7 +70,7 @@ class LoginViewModel @AssistedInject constructor( }, onFailure = { error -> setState { copy(isLoading = false) } - _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인에 실패했습니다")) + _sideEffects.send(LoginContract.LoginSideEffect.ShowError(errorMessageProvider.getLoginFailedError())) }, ) } @@ -76,7 +78,7 @@ class LoginViewModel @AssistedInject constructor( OAuthProvider.GOOGLE -> { val idToken = intent.idToken if (idToken.isNullOrBlank()) { - _sideEffects.send(LoginContract.LoginSideEffect.ShowError("로그인에 실패했습니다.")) + _sideEffects.send(LoginContract.LoginSideEffect.ShowError(errorMessageProvider.getLoginFailedError())) return } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt index 9dc9db8c..4a93338f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryViewModel.kt @@ -2,17 +2,18 @@ package com.sopt.clody.presentation.ui.replydiary import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.data.remote.util.ApiError import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.extension.throttleFirst -import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -22,7 +23,8 @@ import javax.inject.Inject @HiltViewModel class ReplyDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, - private val networkUtil: NetworkUtil, + private val errorMessageProvider: ErrorMessageProvider, + private val networkConnectivityObserver: NetworkConnectivityObserver, ) : ViewModel() { private val _replyDiaryState = MutableStateFlow(ReplyDiaryState.Idle) @@ -32,7 +34,7 @@ class ReplyDiaryViewModel @Inject constructor( private var lastMonth: Int = 0 private var lastDate: Int = 0 - private val _retryFlow = MutableSharedFlow() // 연속 클릭을 제어하기 위해 선언. + private val _retryFlow = MutableSharedFlow() init { setupRetryFlow() @@ -40,11 +42,11 @@ class ReplyDiaryViewModel @Inject constructor( private fun setupRetryFlow() { _retryFlow - .throttleFirst(2000L) // 2초 동안 첫 번째 이벤트만 발행. - .onEach { // Flow에서 발생한 이벤트를 받아서 getReplyDiaryInternal 호출. + .throttleFirst(2000L) + .onEach { getReplyDiaryInternal(lastYear, lastMonth, lastDate) } - .launchIn(viewModelScope) // Flow를 viewModelScope에서 실행하고 구독을 유지, 즉 viewmodel이 살아있는 동안 flow가 실행됨 + .launchIn(viewModelScope) } fun getReplyDiary(year: Int, month: Int, date: Int) { @@ -56,8 +58,9 @@ class ReplyDiaryViewModel @Inject constructor( private fun getReplyDiaryInternal(year: Int, month: Int, date: Int) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - updateState(ReplyDiaryState.Failure(FAILURE_NETWORK_MESSAGE)) + val isConnected = networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + if (!isConnected) { + updateState(ReplyDiaryState.Failure(errorMessageProvider.getNetworkError())) return@launch } @@ -73,7 +76,7 @@ class ReplyDiaryViewModel @Inject constructor( onSuccess = { data -> updateState( ReplyDiaryState.Success( - content = data.content ?: "", + content = data.content.orEmpty(), nickname = data.nickname, month = data.month, date = data.date, @@ -81,9 +84,12 @@ class ReplyDiaryViewModel @Inject constructor( ) }, onFailure = { throwable -> - updateState(ReplyDiaryState.Failure(ErrorMessages.FAILURE_TEMPORARY_MESSAGE)) - val errorMessage = throwable.localizedMessage ?: UNKNOWN_ERROR - Timber.tag("ReplyDiaryViewModel").e("API 요청 실패: %s", errorMessage) + val message = when (throwable) { + is ApiError -> errorMessageProvider.getApiError(throwable) + else -> errorMessageProvider.getTemporaryError() + } + updateState(ReplyDiaryState.Failure(message)) + Timber.tag("ReplyDiaryViewModel").e(throwable, "API 요청 실패") }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt index 3683b4a4..b66d127e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingViewModel.kt @@ -4,18 +4,19 @@ import android.app.Activity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.sopt.clody.core.ad.RewardAdShower +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.response.DiaryTimeResponseDto -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.data.remote.util.ApiError import com.sopt.clody.domain.repository.AdRepository import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.presentation.utils.extension.throttleFirst -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -27,8 +28,9 @@ import javax.inject.Inject class ReplyLoadingViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val adRepository: AdRepository, - private val networkUtil: NetworkUtil, private val rewardAdShower: RewardAdShower, + private val errorMessageProvider: ErrorMessageProvider, + private val networkConnectivityObserver: NetworkConnectivityObserver, ) : ViewModel() { private val _replyLoadingState = MutableStateFlow(ReplyLoadingState.Idle) @@ -65,9 +67,7 @@ class ReplyLoadingViewModel @Inject constructor( private fun setupRetryFlow() { _retryFlow .throttleFirst(2000L) - .onEach { - getDiaryTimeInternal(lastYear, lastMonth, lastDate) - } + .onEach { getDiaryTimeInternal(lastYear, lastMonth, lastDate) } .launchIn(viewModelScope) } @@ -82,8 +82,9 @@ class ReplyLoadingViewModel @Inject constructor( _replyLoadingState.value = ReplyLoadingState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _replyLoadingState.value = ReplyLoadingState.Failure(FAILURE_NETWORK_MESSAGE) + val isConnected = networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + if (!isConnected) { + _replyLoadingState.value = ReplyLoadingState.Failure(errorMessageProvider.getNetworkError()) return@launch } @@ -95,15 +96,9 @@ class ReplyLoadingViewModel @Inject constructor( private fun handleResult(result: Result) { result.fold( onSuccess = { data -> - val diaryWrittenDay = data.date.split("-") - var targetDateTime = LocalDateTime.of( - diaryWrittenDay[0].toInt(), - diaryWrittenDay[1].toInt(), - diaryWrittenDay[2].toInt(), - data.HH, - data.mm, - data.ss, - ).plusMinutes(if (data.isFirst) INITIAL_REMINDER_MINUTES else REGULAR_REMINDER_HOURS * 60) + val (y, m, d) = data.date.split("-").map { it.toInt() } + var targetDateTime = LocalDateTime.of(y, m, d, data.HH, data.mm, data.ss) + .plusMinutes(if (data.isFirst) INITIAL_REMINDER_MINUTES else REGULAR_REMINDER_HOURS * 60) if (_isAdCompleted.value || data.isFromAd) { targetDateTime = LocalDateTime.now() @@ -114,9 +109,13 @@ class ReplyLoadingViewModel @Inject constructor( _isWaitingForPatchResponse.value = false }, onFailure = { throwable -> - _replyLoadingState.value = ReplyLoadingState.Failure(FAILURE_TEMPORARY_MESSAGE) - val errorMessage = throwable.localizedMessage ?: UNKNOWN_ERROR - Timber.tag("ReplyLoadingViewModel").e("API 요청 실패: %s", errorMessage) + val message = if (throwable is ApiError) { + errorMessageProvider.getApiError(throwable) + } else { + errorMessageProvider.getTemporaryError() + } + _replyLoadingState.value = ReplyLoadingState.Failure(message) + Timber.tag("ReplyLoadingViewModel").e(throwable, "DiaryTime API 요청 실패") }, ) } @@ -138,7 +137,7 @@ class ReplyLoadingViewModel @Inject constructor( val startAdResult = adRepository.startAd(lastYear, lastMonth, lastDate) if (startAdResult.isFailure) { _isAdLoading.value = false - _adErrorMessage.value = "잠시 후 다시 시도해주세요!" + _adErrorMessage.value = errorMessageProvider.getTemporaryError() return@launch } @@ -152,7 +151,7 @@ class ReplyLoadingViewModel @Inject constructor( _isAdPreloaded = true showRewardedAd(activity) } else { - _adErrorMessage.value = "잠시 후 다시 시도해주세요!" + _adErrorMessage.value = errorMessageProvider.getTemporaryError() } } } @@ -164,7 +163,6 @@ class ReplyLoadingViewModel @Inject constructor( onAdRewarded = { viewModelScope.launch { _isWaitingForPatchResponse.value = true - adRepository.endAd(lastYear, lastMonth, lastDate).onSuccess { _replyLoadingState.value = ReplyLoadingState.Success(LocalDateTime.now()) _isWaitingForPatchResponse.value = false diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index c29f3268..dfdc1d8b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -3,25 +3,26 @@ package com.sopt.clody.presentation.ui.setting.notificationsetting.screen import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.Notification import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.extension.convertUTZtoKST -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class NotificationSettingViewModel @Inject constructor( private val notificationRepository: NotificationRepository, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _diaryAlarm = MutableStateFlow(false) @@ -62,8 +63,8 @@ class NotificationSettingViewModel @Inject constructor( if (retryCount >= maxRetryCount) return _notificationInfoState.value = NotificationInfoState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _notificationInfoState.value = NotificationInfoState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _notificationInfoState.value = NotificationInfoState.Failure(errorMessageProvider.getNetworkError()) return@launch } val result = notificationRepository.getNotificationInfo() @@ -79,12 +80,12 @@ class NotificationSettingViewModel @Inject constructor( onFailure = { retryCount++ if (retryCount >= maxRetryCount) { - NotificationInfoState.Failure(FAILURE_TEMPORARY_MESSAGE) + NotificationInfoState.Failure(errorMessageProvider.getTemporaryError()) } else { if (it.message?.contains("200") == false) { - NotificationInfoState.Failure(FAILURE_TEMPORARY_MESSAGE) + NotificationInfoState.Failure(errorMessageProvider.getTemporaryError()) } else { - NotificationInfoState.Failure(UNKNOWN_ERROR) + NotificationInfoState.Failure(errorMessageProvider.getUnknownError()) } } }, @@ -95,8 +96,8 @@ class NotificationSettingViewModel @Inject constructor( fun changeAlarm(context: Context, notificationType: Notification) { _notificationChangeState.value = NotificationChangeState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true return@launch } @@ -131,9 +132,9 @@ class NotificationSettingViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } _showFailureDialog.value = true _notificationChangeState.value = NotificationChangeState.Failure(_failureDialogMessage.value) @@ -145,8 +146,8 @@ class NotificationSettingViewModel @Inject constructor( fun changeNotificationTime(context: Context, timePeriod: TimePeriod, hour: String, minute: String) { _notificationTimeChangeState.value = NotificationTimeChangeState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true return@launch } @@ -169,9 +170,9 @@ class NotificationSettingViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } _showFailureDialog.value = true _notificationTimeChangeState.value = NotificationTimeChangeState.Failure(_failureDialogMessage.value) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt index 5c374523..0c240316 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/screen/AccountManagementViewModel.kt @@ -3,19 +3,19 @@ package com.sopt.clody.presentation.ui.setting.screen import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.datastore.TokenDataStore import com.sopt.clody.data.remote.dto.request.ModifyNicknameRequestDto -import com.sopt.clody.data.remote.util.NetworkUtil import com.sopt.clody.domain.repository.AccountManagementRepository import com.sopt.clody.presentation.ui.auth.signup.NicknameMessage import com.sopt.clody.presentation.utils.language.LanguageProvider -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider 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.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,9 +23,10 @@ import javax.inject.Inject class AccountManagementViewModel @Inject constructor( private val accountManagementRepository: AccountManagementRepository, private val tokenDataStore: TokenDataStore, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, @ApplicationContext private val context: Context, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _userInfoState = MutableStateFlow(UserInfoState.Idle) val userInfoState: StateFlow = _userInfoState @@ -61,8 +62,8 @@ class AccountManagementViewModel @Inject constructor( if (retryCount >= maxRetryCount) return _userInfoState.value = UserInfoState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _userInfoState.value = UserInfoState.Failure(FAILURE_NETWORK_MESSAGE) + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _userInfoState.value = UserInfoState.Failure(errorMessageProvider.getNetworkError()) return@launch } val result = accountManagementRepository.getUserInfo() @@ -74,12 +75,12 @@ class AccountManagementViewModel @Inject constructor( onFailure = { retryCount++ if (retryCount >= maxRetryCount) { - UserInfoState.Failure(FAILURE_TEMPORARY_MESSAGE) + UserInfoState.Failure(errorMessageProvider.getTemporaryError()) } else { val errorMessage = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } UserInfoState.Failure(errorMessage) } @@ -90,8 +91,8 @@ class AccountManagementViewModel @Inject constructor( fun changeNickname(modifyNicknameRequestDto: ModifyNicknameRequestDto) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true } _userNicknameState.value = UserNicknameState.Loading @@ -100,9 +101,9 @@ class AccountManagementViewModel @Inject constructor( onSuccess = { UserNicknameState.Success(it) }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - UNKNOWN_ERROR + errorMessageProvider.getUnknownError() } _showFailureDialog.value = true UserNicknameState.Failure(_failureDialogMessage.value) @@ -141,8 +142,8 @@ class AccountManagementViewModel @Inject constructor( fun revokeAccount() { _revokeAccountState.value = RevokeAccountState.Loading viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureDialogMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureDialogMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true } val result = accountManagementRepository.revokeAccount() @@ -153,9 +154,9 @@ class AccountManagementViewModel @Inject constructor( }, onFailure = { _failureDialogMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + it.localizedMessage ?: errorMessageProvider.getUnknownError() } _showFailureDialog.value = true RevokeAccountState.Failure(_failureDialogMessage.value) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt index b5a567ec..fff225a0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/writediary/screen/WriteDiaryViewModel.kt @@ -7,20 +7,19 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.sopt.clody.data.remote.util.NetworkUtil +import com.sopt.clody.core.network.NetworkConnectivityObserver +import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.usecase.FetchDraftDiaryUseCase import com.sopt.clody.domain.usecase.SaveDraftDiaryUseCase import com.sopt.clody.presentation.utils.extension.convertDateToKstDateTime import com.sopt.clody.presentation.utils.language.LanguageProvider -import com.sopt.clody.presentation.utils.network.ErrorMessages -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE -import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR +import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject @@ -30,9 +29,10 @@ class WriteDiaryViewModel @Inject constructor( private val diaryRepository: DiaryRepository, private val fetchDraftDiaryUseCase: FetchDraftDiaryUseCase, private val saveDraftDiaryUseCase: SaveDraftDiaryUseCase, - private val networkUtil: NetworkUtil, + private val networkConnectivityObserver: NetworkConnectivityObserver, private val draftRepository: DraftRepository, private val languageProvider: LanguageProvider, + private val errorMessageProvider: ErrorMessageProvider, ) : ViewModel() { private val _writeDiaryState = MutableStateFlow(WriteDiaryState.Idle) @@ -75,8 +75,8 @@ class WriteDiaryViewModel @Inject constructor( fun writeDiary(year: Int, month: Int, day: Int, contents: List) { viewModelScope.launch { - if (!networkUtil.isNetworkAvailable()) { - _failureMessage.value = FAILURE_NETWORK_MESSAGE + if (networkConnectivityObserver.networkStatus.first() == NetworkStatus.Unavailable) { + _failureMessage.value = errorMessageProvider.getNetworkError() _showFailureDialog.value = true return@launch } @@ -98,9 +98,9 @@ class WriteDiaryViewModel @Inject constructor( }, onFailure = { _failureMessage.value = if (it.message?.contains("200") == false) { - FAILURE_TEMPORARY_MESSAGE + errorMessageProvider.getTemporaryError() } else { - it.localizedMessage ?: UNKNOWN_ERROR + it.localizedMessage ?: errorMessageProvider.getUnknownError() } _showFailureDialog.value = true WriteDiaryState.Failure(_failureMessage.value) @@ -219,7 +219,7 @@ class WriteDiaryViewModel @Inject constructor( checkEmptyFieldsMessage() }.onFailure { ensureDefaultEntry() - _failureMessage.value = ErrorMessages.FETCH_TEMP_DIARY_FAILED + _failureMessage.value = errorMessageProvider.getFetchTempDiaryFailedError() _showFailureDialog.value = true } } @@ -233,7 +233,7 @@ class WriteDiaryViewModel @Inject constructor( _failureMessage.value = "" _showFailureDialog.value = false }.onFailure { e -> - _failureMessage.value = e.localizedMessage ?: UNKNOWN_ERROR + _failureMessage.value = e.localizedMessage ?: errorMessageProvider.getUnknownError() _showFailureDialog.value = true } } diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt new file mode 100644 index 00000000..71eb58cc --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageModule.kt @@ -0,0 +1,22 @@ +package com.sopt.clody.presentation.utils.network + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ErrorMessageModule { + + @Provides + @Singleton + fun provideErrorMessageProvider( + @ApplicationContext context: Context, + ): ErrorMessageProvider { + return ErrorMessageProviderImpl(context) + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt new file mode 100644 index 00000000..bac76132 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProvider.kt @@ -0,0 +1,16 @@ +package com.sopt.clody.presentation.utils.network + +import com.sopt.clody.data.remote.util.ApiError + +interface ErrorMessageProvider { + fun getTemporaryError(): String + fun getNetworkError(): String + fun getServerError(): String + fun getFetchTempDiaryFailedError(): String + fun getUnknownError(): String + fun getLoginFailedError(): String + fun getSignupFailedError(): String + fun getGoogleIdTokenMissingError(): String + fun getNetworkCheckError(): String + fun getApiError(apiError: ApiError): String +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt new file mode 100644 index 00000000..13b6f193 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessageProviderImpl.kt @@ -0,0 +1,52 @@ +package com.sopt.clody.presentation.utils.network + +import android.content.Context +import com.sopt.clody.R +import com.sopt.clody.data.remote.util.ApiError +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class ErrorMessageProviderImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : ErrorMessageProvider { + + override fun getTemporaryError(): String { + return context.getString(R.string.error_temporary) + } + + override fun getNetworkError(): String { + return context.getString(R.string.error_network) + } + + override fun getServerError(): String { + return context.getString(R.string.error_server) + } + + override fun getFetchTempDiaryFailedError(): String { + return context.getString(R.string.error_fetch_temp_diary_failed) + } + + override fun getUnknownError(): String { + return context.getString(R.string.error_unknown) + } + + override fun getLoginFailedError(): String { + return context.getString(R.string.error_login_failed) + } + + override fun getSignupFailedError(): String { + return context.getString(R.string.error_signup_failed) + } + + override fun getGoogleIdTokenMissingError(): String { + return context.getString(R.string.error_google_id_token_missing) + } + + override fun getNetworkCheckError(): String { + return context.getString(R.string.error_network_check) + } + + override fun getApiError(apiError: ApiError): String { + return apiError.message + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt b/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt deleted file mode 100644 index 3db50f7e..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/utils/network/ErrorMessages.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.sopt.clody.presentation.utils.network - -object ErrorMessages { - const val FAILURE_NETWORK_MESSAGE = "서비스 접속이 원활하지 않아요.\n네트워크 연결을 확인해주세요." - const val FAILURE_TEMPORARY_MESSAGE = "일시적인 오류가 발생했어요.\n잠시 후 다시 시도해주세요." - const val FAILURE_SERVER_MESSAGE = "서버 오류가 발생했어요.\n잠시 후 다시 시도해주세요." - const val FETCH_TEMP_DIARY_FAILED = "임시저장 불러오기에 실패했어요.\n잠시 후 다시 시도해주세요." - const val UNKNOWN_ERROR = "알수없는 에러" -} diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index dc4a25c8..913d128c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -196,4 +196,15 @@ 일기 삭제 중 오류가 발생했습니다. Chrome이 설치되어 있어야 로그인이 가능합니다. 루팅된 기기에서는 로그인할 수 없습니다. + + + 서비스 접속이 원활하지 않아요.\n네트워크 연결을 확인해주세요. + 일시적인 오류가 발생했어요.\n잠시 후 다시 시도해주세요. + 서버 오류가 발생했어요.\n잠시 후 다시 시도해주세요. + 임시저장 불러오기에 실패했어요.\n잠시 후 다시 시도해주세요. + 알수없는 에러 + 로그인에 실패했어요. + 회원가입에 실패했어요. + Google ID Token이 없습니다. + 네트워크 연결을 확인해주세요. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9aea0081..5db7f1b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -197,4 +197,15 @@ Login is not available on rooted devices for security reasons. Failed to load data. An unexpected error has occurred. + + + Network connection is unstable.\nPlease check your network connection. + A temporary error has occurred.\nPlease try again later. + A server error has occurred.\nPlease try again later. + Failed to load draft.\nPlease try again later. + Unknown error + Login failed. + Sign up failed. + Google ID Token is missing. + Please check your network connection. diff --git a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt index 4829638c..a85085a4 100644 --- a/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt +++ b/app/src/test/java/com/sopt/clody/datasource/FakeDiaryRemoteDataSource.kt @@ -10,37 +10,38 @@ import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.ReplyDiaryResponseDto import com.sopt.clody.data.remote.dto.response.WriteDiaryResponseDto +import com.sopt.clody.data.remote.util.ApiError class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { var draftDiariesResponse: ApiResponse? = null var saveDraftResponse: ApiResponse? = null - override suspend fun writeDiary(date: String, content: List): ApiResponse { + override suspend fun writeDiary(date: String, content: List): Result { throw NotImplementedError() } - override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun deleteDailyDiary(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } - override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun getDailyDiariesData(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } - override suspend fun getDiaryTime(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun getDiaryTime(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } - override suspend fun getMonthlyCalendarData(year: Int, month: Int): ApiResponse { + override suspend fun getMonthlyCalendarData(year: Int, month: Int): Result { throw NotImplementedError() } - override suspend fun getMonthlyDiary(year: Int, month: Int): ApiResponse { + override suspend fun getMonthlyDiary(year: Int, month: Int): Result { throw NotImplementedError() } - override suspend fun getReplyDiary(year: Int, month: Int, date: Int): ApiResponse { + override suspend fun getReplyDiary(year: Int, month: Int, date: Int): Result { throw NotImplementedError() } @@ -48,16 +49,16 @@ class FakeDiaryRemoteDataSource : DiaryRemoteDataSource { year: Int, month: Int, date: Int, - ): ApiResponse { - return draftDiariesResponse - ?: throw IllegalStateException("draftDiariesResponse not set") + ): Result { + return draftDiariesResponse?.let { Result.success(it.data!!) } + ?: Result.failure(ApiError("draftDiariesResponse not set")) } override suspend fun saveDraftDiary( request: SaveDraftDiaryRequestDto, - ): ApiResponse { - return saveDraftResponse - ?: throw IllegalStateException("saveDraftResponse not set") + ): Result { + return saveDraftResponse?.let { Result.success(Unit) } + ?: Result.failure(ApiError("saveDraftResponse not set")) } fun setDraftDiariesResponse(list: List) {