diff --git a/app/src/main/java/com/whyranoid/walkie/KoinModules.kt b/app/src/main/java/com/whyranoid/walkie/KoinModules.kt index 1f6eed6..7111fb4 100644 --- a/app/src/main/java/com/whyranoid/walkie/KoinModules.kt +++ b/app/src/main/java/com/whyranoid/walkie/KoinModules.kt @@ -90,6 +90,7 @@ import com.whyranoid.presentation.screens.mypage.editprofile.EditProfileViewMode import com.whyranoid.presentation.screens.mypage.following.FollowingViewModel import com.whyranoid.presentation.screens.setting.SettingViewModel import com.whyranoid.presentation.viewmodel.AddPostViewModel +import com.whyranoid.presentation.util.ApiResponseDialog import com.whyranoid.presentation.viewmodel.CommunityScreenViewModel import com.whyranoid.presentation.viewmodel.RunningEditViewModel import com.whyranoid.presentation.viewmodel.RunningViewModel @@ -104,6 +105,7 @@ import com.whyranoid.presentation.viewmodel.challenge.ChallengeDetailViewModel import com.whyranoid.presentation.viewmodel.challenge.ChallengeExitViewModel import com.whyranoid.presentation.viewmodel.challenge.ChallengeMainViewModel import com.whyranoid.walkie.walkiedialog.DialogViewModel +import com.whyranoid.walkie.walkiedialog.NetworkInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response @@ -120,7 +122,19 @@ val viewModelModule = viewModel { ChallengeMainViewModel(get(), get(), get(), get(), get()) } viewModel { ChallengeDetailViewModel(get(), get()) } viewModel { ChallengeExitViewModel(get(), get()) } - viewModel { UserPageViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { + UserPageViewModel( + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get() + ) + } viewModel { RunningViewModel(get(), get(), get(), get(), get(), get()) } viewModel { RunningEditViewModel() } viewModel { SplashViewModel(get()) } @@ -200,7 +214,7 @@ val useCaseModule = single { GetMyFollowingUseCase(get(), get()) } single { SendCommentUseCase(get(), get()) } single { GetUserUseCase(get()) } - single { ChangeChallengeStatusUseCase(get(), get())} + single { ChangeChallengeStatusUseCase(get(), get()) } single { GetUserPostsUseCase(get(), get()) } } @@ -242,6 +256,17 @@ val networkModule = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, + ).addInterceptor( + NetworkInterceptor( + onRequest = { ApiResponseDialog.startLoading() }, + onResponse = { ApiResponseDialog.finishLoad(it) }, + excludedUrls = listOf( + Regex("/api/follow/(\\d+)/following"), // polling 방식 업데이트 + Regex("/api/follow/(\\d+)/walking-followings"), // polling 방식 업데이트 + Regex("/api/community/listup-post"), // 커뮤니티 탭, 자체 로딩바 있음 + Regex("/api/community/upload-post") // 게시글 업로드, 자체 로딩바 있음 + ) + ) ) .build() } diff --git a/app/src/main/java/com/whyranoid/walkie/walkiedialog/NetworkInterceptor.kt b/app/src/main/java/com/whyranoid/walkie/walkiedialog/NetworkInterceptor.kt new file mode 100644 index 0000000..1059923 --- /dev/null +++ b/app/src/main/java/com/whyranoid/walkie/walkiedialog/NetworkInterceptor.kt @@ -0,0 +1,41 @@ +package com.whyranoid.walkie.walkiedialog + +import android.util.Log +import okhttp3.Interceptor +import okhttp3.Response + +class NetworkInterceptor( + private val onRequest: () -> Unit, + private val onResponse: (isSuccessFul: Boolean) -> Unit, + private val excludedUrls: List, +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + Log.d("ju0828", request.url.toString()) + + Log.d("ju0828", "${request.url} validate = ${excludedUrls.all { request.url.toString().contains(it).not() }}") + + // 요청 URL이 제외할 URL과 같지 않으면 콜백 호출 + if (excludedUrls.all { request.url.toString().contains(it).not() }) { + onRequest() // 요청 전 콜백 호출 + Log.d("ju0828", "${request.url} request called") + + } + + return try { + val response = chain.proceed(request) + + // 응답 후 콜백 호출 + if (excludedUrls.all { request.url.toString().contains(it).not() }) { + onResponse(response.isSuccessful) // 응답 후 콜백 호출 + } + response + } catch (e: Exception) { + // 예외가 발생한 경우 (타임아웃 포함) + if (e is java.net.SocketTimeoutException) { + onResponse(false) + } + throw e // 예외를 다시 던져서 호출자에게 알림 + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/reusable/CircleProgressWithText.kt b/presentation/src/main/java/com/whyranoid/presentation/reusable/CircleProgressWithText.kt index e90bbb4..d57904e 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/reusable/CircleProgressWithText.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/reusable/CircleProgressWithText.kt @@ -1,6 +1,7 @@ package com.whyranoid.presentation.reusable import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.whyranoid.presentation.theme.WalkieColor @@ -28,16 +30,31 @@ fun CircleProgressWithText( modifier: Modifier = Modifier, text: String, ) { - Box(modifier = modifier.fillMaxSize().alpha(0.5f).background(WalkieColor.GrayDefault)) + Box( + modifier = modifier + .fillMaxSize() + .alpha(0.5f) + .background(WalkieColor.GrayDefault) + .pointerInput(Unit) { // 터치 이벤트 소비 + detectTapGestures(onPress = { + // 아무것도 하지 않음, 터치 이벤트 소비 + }) + }) Box(modifier = Modifier.fillMaxSize()) { Column( - modifier = modifier.width(200.dp).height(160.dp).align(Alignment.Center) - .clip(RoundedCornerShape(20.dp)).background(Color.White), + modifier = modifier + .width(200.dp) + .height(160.dp) + .align(Alignment.Center) + .clip(RoundedCornerShape(20.dp)) + .background(Color.White), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Text(text, style = WalkieTypography.SubTitle) - Spacer(modifier = Modifier.height(20.dp)) + if (text.isNotEmpty()) { + Text(text, style = WalkieTypography.SubTitle) + Spacer(modifier = Modifier.height(20.dp)) + } CircularProgressIndicator() } } diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt index e40e834..1e73ace 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/AppScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.BottomNavigation import androidx.compose.material.BottomNavigationItem import androidx.compose.material.Icon @@ -13,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -27,6 +29,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.whyranoid.domain.model.post.Post +import com.whyranoid.presentation.reusable.CircleProgressWithText import com.whyranoid.presentation.screens.Screen.Companion.bottomNavigationItems import com.whyranoid.presentation.screens.challenge.ChallengeCompleteScreen import com.whyranoid.presentation.screens.challenge.ChallengeDetailScreen @@ -47,6 +50,8 @@ import com.whyranoid.presentation.screens.signin.SignInScreen import com.whyranoid.presentation.screens.splash.SplashScreen import com.whyranoid.presentation.theme.WalkieColor import com.whyranoid.presentation.theme.WalkieTypography +import com.whyranoid.presentation.util.CustomDialog +import com.whyranoid.presentation.util.ApiResponseDialog import com.whyranoid.presentation.viewmodel.SplashState import com.whyranoid.presentation.viewmodel.SplashViewModel import org.koin.androidx.compose.koinViewModel @@ -249,5 +254,18 @@ fun AppScreenContent( ) } } + + val isLoading = ApiResponseDialog.isLoading.collectAsStateWithLifecycle() + val isError = ApiResponseDialog.isShowError.collectAsStateWithLifecycle() + if (isLoading.value) { + CircleProgressWithText(text = "") + } else if (isError.value) { + CustomDialog( + title = "네트워크 연결 실패", + description = "네트워크 연결이 끊겼거나 속도가 느립니다.\n다시 시도해주세요.", + onAction = { ApiResponseDialog.closeErrorDialog() }, + Modifier.clip(RoundedCornerShape(20.dp)) + ) + } } } diff --git a/presentation/src/main/java/com/whyranoid/presentation/util/ApiResponseManager.kt b/presentation/src/main/java/com/whyranoid/presentation/util/ApiResponseManager.kt new file mode 100644 index 0000000..885722c --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/util/ApiResponseManager.kt @@ -0,0 +1,33 @@ +package com.whyranoid.presentation.util + + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.concurrent.atomic.AtomicInteger + +object ApiResponseDialog { + private val loadingCount = AtomicInteger(0) + + private val _isLaoding = MutableStateFlow(false) + val isLoading get() = _isLaoding.asStateFlow() + + private val _isShowError = MutableStateFlow(false) + + val isShowError get() = _isShowError.asStateFlow() + + fun startLoading() { + if (loadingCount.incrementAndGet() > 0) _isLaoding.value = true + } + + fun finishLoad(isSuccessFul: Boolean) { + if (loadingCount.decrementAndGet() == 0) _isLaoding.value = false + if (isSuccessFul.not()) { + _isLaoding.value = false + _isShowError.value = true + } + } + + fun closeErrorDialog() { + _isShowError.value = false + } +} \ No newline at end of file