diff --git a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/card/SuwikiClassReviewCard.kt b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/card/SuwikiClassReviewCard.kt index aae278214..b35ba8de5 100644 --- a/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/card/SuwikiClassReviewCard.kt +++ b/core/designsystem/src/main/java/com/suwiki/core/designsystem/component/card/SuwikiClassReviewCard.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -92,7 +93,7 @@ fun SuwikiClassReviewCard( .padding(vertical = 3.dp), ) Text( - text = professor, + text = if (professor == "null") stringResource(id = com.suwiki.core.ui.R.string.word_none) else professor, style = SuwikiTheme.typography.body7, color = Gray6A, ) diff --git a/core/ui/src/main/java/com/suwiki/core/ui/util/Consts.kt b/core/ui/src/main/java/com/suwiki/core/ui/util/Consts.kt index be909d0c0..5fb095914 100644 --- a/core/ui/src/main/java/com/suwiki/core/ui/util/Consts.kt +++ b/core/ui/src/main/java/com/suwiki/core/ui/util/Consts.kt @@ -5,6 +5,7 @@ const val ASK_SITE = "https://alike-pump-ae3.notion.site/SUWIKI-2cd58468e90b404f const val FEEDBACK_SITE = "https://forms.gle/tZByKoN6rJCysvNz6" const val TERMS_SITE = "https://sites.google.com/view/suwiki-policy-terms/" const val PRIVACY_POLICY_SITE = "https://sites.google.com/view/suwiki-policy-privacy" +const val PLAY_STORE_SITE = "https://play.google.com/store/apps/details?id=com.kunize.uswtimetable&hl=ko-KR" object REGEX { val ID = """^[A-Za-z0-9]{6,20}$""".toRegex() diff --git a/data/notice/src/main/java/com/suwiki/data/notice/datasource/RemoteNoticeDataSource.kt b/data/notice/src/main/java/com/suwiki/data/notice/datasource/RemoteNoticeDataSource.kt index 5283a80f0..3bd2fbbec 100644 --- a/data/notice/src/main/java/com/suwiki/data/notice/datasource/RemoteNoticeDataSource.kt +++ b/data/notice/src/main/java/com/suwiki/data/notice/datasource/RemoteNoticeDataSource.kt @@ -8,4 +8,8 @@ interface RemoteNoticeDataSource { suspend fun getNoticeList(page: Int): List suspend fun getNoticeDetail(id: Long): NoticeDetail + + suspend fun checkUpdateMandatory( + versionCode: Long, + ): Boolean } diff --git a/data/notice/src/main/java/com/suwiki/data/notice/repository/NoticeRepositoryImpl.kt b/data/notice/src/main/java/com/suwiki/data/notice/repository/NoticeRepositoryImpl.kt index 7168fea68..aaa221571 100644 --- a/data/notice/src/main/java/com/suwiki/data/notice/repository/NoticeRepositoryImpl.kt +++ b/data/notice/src/main/java/com/suwiki/data/notice/repository/NoticeRepositoryImpl.kt @@ -16,4 +16,8 @@ class NoticeRepositoryImpl @Inject constructor( override suspend fun getNoticeDetail(id: Long): NoticeDetail { return remoteNoticeDataSource.getNoticeDetail(id) } + + override suspend fun checkUpdateMandatory(versionCode: Long): Boolean { + return remoteNoticeDataSource.checkUpdateMandatory(versionCode) + } } diff --git a/domain/notice/src/main/java/com/suwiki/domain/notice/repository/NoticeRepository.kt b/domain/notice/src/main/java/com/suwiki/domain/notice/repository/NoticeRepository.kt index 37b8d6e94..8d6930315 100644 --- a/domain/notice/src/main/java/com/suwiki/domain/notice/repository/NoticeRepository.kt +++ b/domain/notice/src/main/java/com/suwiki/domain/notice/repository/NoticeRepository.kt @@ -8,4 +8,8 @@ interface NoticeRepository { suspend fun getNoticeList(page: Int): List suspend fun getNoticeDetail(id: Long): NoticeDetail + + suspend fun checkUpdateMandatory( + versionCode: Long, + ): Boolean } diff --git a/domain/notice/src/main/java/com/suwiki/domain/notice/usecase/CheckUpdateMandatoryUseCase.kt b/domain/notice/src/main/java/com/suwiki/domain/notice/usecase/CheckUpdateMandatoryUseCase.kt new file mode 100644 index 000000000..4be9715ee --- /dev/null +++ b/domain/notice/src/main/java/com/suwiki/domain/notice/usecase/CheckUpdateMandatoryUseCase.kt @@ -0,0 +1,13 @@ +package com.suwiki.domain.notice.usecase + +import com.suwiki.core.common.runCatchingIgnoreCancelled +import com.suwiki.domain.notice.repository.NoticeRepository +import javax.inject.Inject + +class CheckUpdateMandatoryUseCase @Inject constructor( + private val noticeRepository: NoticeRepository, +) { + suspend operator fun invoke(versionCode: Long): Result = runCatchingIgnoreCancelled { + noticeRepository.checkUpdateMandatory(versionCode) + } +} diff --git a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/detail/component/StatisticsContainer.kt b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/detail/component/StatisticsContainer.kt index 7230467a9..19b65a04a 100644 --- a/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/detail/component/StatisticsContainer.kt +++ b/feature/lectureevaluation/viewerreporter/src/main/java/com/suwiki/feature/lectureevaluation/viewerreporter/detail/component/StatisticsContainer.kt @@ -87,7 +87,7 @@ fun SuwikiReviewStatisticsContainer( .padding(vertical = 3.dp), ) Text( - text = data.info.professor, + text = if (data.info.professor == "null") stringResource(id = com.suwiki.core.ui.R.string.word_none) else data.info.professor, style = SuwikiTheme.typography.body7, color = Gray6A, ) diff --git a/feature/navigator/build.gradle.kts b/feature/navigator/build.gradle.kts index 255dbe957..1d628f261 100644 --- a/feature/navigator/build.gradle.kts +++ b/feature/navigator/build.gradle.kts @@ -8,6 +8,8 @@ android { } dependencies { + implementation(projects.domain.notice) + implementation(projects.feature.lectureevaluation.editor) implementation(projects.feature.lectureevaluation.my) implementation(projects.feature.lectureevaluation.viewerreporter) diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt index 375ef831d..ad9d0b035 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainContract.kt @@ -4,6 +4,9 @@ data class MainState( val toastMessage: String = "", val toastVisible: Boolean = false, val showNetworkErrorDialog: Boolean = false, + val showUpdateMandatoryDialog: Boolean = false, ) -sealed interface MainSideEffect +sealed interface MainSideEffect { + data object OpenPlayStoreSite : MainSideEffect +} diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt index ddf1ddb15..1495abe69 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainScreen.kt @@ -18,8 +18,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset @@ -33,6 +36,8 @@ import com.suwiki.core.designsystem.theme.GrayDA import com.suwiki.core.designsystem.theme.Primary import com.suwiki.core.designsystem.theme.White import com.suwiki.core.ui.extension.suwikiClickable +import com.suwiki.core.ui.extension.versionCode +import com.suwiki.core.ui.util.PLAY_STORE_SITE import com.suwiki.feature.lectureevaluation.editor.navigation.myEvaluationEditNavGraph import com.suwiki.feature.lectureevaluation.my.navigation.myEvaluationNavGraph import com.suwiki.feature.lectureevaluation.viewerreporter.navigation.lectureEvaluationNavGraph @@ -46,6 +51,7 @@ import com.suwiki.feature.timetable.navigation.timetableNavGraph import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import org.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect @Composable internal fun MainScreen( @@ -54,6 +60,17 @@ internal fun MainScreen( navigator: MainNavigator = rememberMainNavigator(), ) { val uiState = viewModel.collectAsState().value + val uriHandler = LocalUriHandler.current + val context = LocalContext.current + viewModel.collectSideEffect { sideEffect -> + when (sideEffect) { + MainSideEffect.OpenPlayStoreSite -> uriHandler.openUri(PLAY_STORE_SITE) + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.checkUpdateMandatory(context.versionCode) + } Scaffold( modifier = modifier, @@ -156,14 +173,24 @@ internal fun MainScreen( if (uiState.showNetworkErrorDialog) { SuwikiDialog( - headerText = stringResource(com.suwiki.feature.navigator.R.string.dialog_network_header), - bodyText = stringResource(com.suwiki.feature.navigator.R.string.dialog_network_body), + headerText = stringResource(R.string.dialog_network_header), + bodyText = stringResource(R.string.dialog_network_body), confirmButtonText = stringResource(id = com.suwiki.core.ui.R.string.word_confirm), onDismissRequest = viewModel::hideNetworkErrorDialog, onClickConfirm = viewModel::hideNetworkErrorDialog, ) } + if (uiState.showUpdateMandatoryDialog) { + SuwikiDialog( + headerText = stringResource(R.string.dialog_update_mandatory_header), + bodyText = stringResource(R.string.dialog_update_mandatory_body), + confirmButtonText = stringResource(id = com.suwiki.core.ui.R.string.word_confirm), + onDismissRequest = {}, + onClickConfirm = viewModel::openPlayStoreSite, + ) + } + SuwikiToast( visible = uiState.toastVisible, message = uiState.toastMessage, diff --git a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt index f5b820d1b..c7a93d5d3 100644 --- a/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt +++ b/feature/navigator/src/main/java/com/suwiki/feature/navigator/MainViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import com.suwiki.core.android.recordException import com.suwiki.core.model.exception.NetworkException import com.suwiki.core.model.exception.UnknownException +import com.suwiki.domain.notice.usecase.CheckUpdateMandatoryUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex @@ -11,16 +12,31 @@ import kotlinx.coroutines.sync.withLock import org.orbitmvi.orbit.Container import org.orbitmvi.orbit.ContainerHost import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import java.net.ConnectException import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor() : ContainerHost, ViewModel() { +class MainViewModel @Inject constructor( + private val checkUpdateMandatoryUseCase: CheckUpdateMandatoryUseCase, +) : ContainerHost, ViewModel() { override val container: Container = container(MainState()) private val mutex = Mutex() + private var isFirstVisit: Boolean = true + + fun checkUpdateMandatory(versionCode: Long) = intent { + if (isFirstVisit.not()) return@intent + checkUpdateMandatoryUseCase(versionCode) + .onSuccess { updateMandatory -> + reduce { state.copy(showUpdateMandatoryDialog = updateMandatory) } + } + isFirstVisit = true + } + + fun openPlayStoreSite() = intent { postSideEffect(MainSideEffect.OpenPlayStoreSite) } fun onShowToast(msg: String) = intent { mutex.withLock { diff --git a/feature/navigator/src/main/res/values/strings.xml b/feature/navigator/src/main/res/values/strings.xml index 9a477b064..398d47ee1 100644 --- a/feature/navigator/src/main/res/values/strings.xml +++ b/feature/navigator/src/main/res/values/strings.xml @@ -1,6 +1,8 @@ SUWIKI - 네트워크에 문제가 있어요. + 네트워크에 문제가 있어요. 네트워크 연결 상태 또는 수위키 서버에 문제가 있어요. 문제가 지속된다면 내 정보 -> 문의하기를 통해 연락 주세요. + 수위키 업데이트 + 원활한 서비스를 위해 업데이트를 진행해야 합니다. 확인을 누르면 앱 스토어로 이동합니다. diff --git a/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt b/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt index 20b6e0be3..21fdaff4b 100644 --- a/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt +++ b/remote/notice/src/main/java/com/suwiki/remote/notice/api/NoticeApi.kt @@ -4,6 +4,7 @@ import com.suwiki.core.network.retrofit.ApiResult import com.suwiki.remote.notice.response.DataResponse import com.suwiki.remote.notice.response.NoticeDetailResponse import com.suwiki.remote.notice.response.NoticeResponse +import com.suwiki.remote.notice.response.UpdateMandatoryResponse import retrofit2.http.GET import retrofit2.http.Query @@ -25,4 +26,10 @@ interface NoticeApi { suspend fun getNotice( @Query(QUERY_NOTICE_ID) id: Long, ): ApiResult> + + @GET("/client/version/update-mandatory") + suspend fun checkUpdateMandatory( + @Query("os") os: String = "android", + @Query("versionCode") versionCode: Long, + ): ApiResult } diff --git a/remote/notice/src/main/java/com/suwiki/remote/notice/datasource/RemoteNoticeDataSourceImpl.kt b/remote/notice/src/main/java/com/suwiki/remote/notice/datasource/RemoteNoticeDataSourceImpl.kt index bc0314450..aac43cd1d 100644 --- a/remote/notice/src/main/java/com/suwiki/remote/notice/datasource/RemoteNoticeDataSourceImpl.kt +++ b/remote/notice/src/main/java/com/suwiki/remote/notice/datasource/RemoteNoticeDataSourceImpl.kt @@ -18,4 +18,8 @@ class RemoteNoticeDataSourceImpl @Inject constructor( override suspend fun getNoticeDetail(id: Long): NoticeDetail { return noticeApi.getNotice(id).getOrThrow().data.toModel() } + + override suspend fun checkUpdateMandatory(versionCode: Long): Boolean { + return noticeApi.checkUpdateMandatory(versionCode = versionCode).getOrThrow().isUpdateMandatory + } } diff --git a/remote/notice/src/main/java/com/suwiki/remote/notice/response/UpdateMandatoryResponse.kt b/remote/notice/src/main/java/com/suwiki/remote/notice/response/UpdateMandatoryResponse.kt new file mode 100644 index 000000000..71baccb44 --- /dev/null +++ b/remote/notice/src/main/java/com/suwiki/remote/notice/response/UpdateMandatoryResponse.kt @@ -0,0 +1,8 @@ +package com.suwiki.remote.notice.response + +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateMandatoryResponse( + val isUpdateMandatory: Boolean, +)