diff --git a/README.md b/README.md index 3a447e8ac..fc1c61501 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,12 @@
+## ANDROID ARTICLE +πŸ”— [TERNING TISTORY](https://terning.tistory.com/category/Android) ## DESIGN SYSTEM πŸ”— [TERNING DESIGN SYSTEM](https://teamterning.github.io/Terning-Android/index.html) - ## KANBAN BOARD πŸ”— [TERNING PROJECT](https://github.com/orgs/teamterning/projects/1) diff --git a/core/local/src/main/java/com/terning/core/local/TerningDataStore.kt b/core/local/src/main/java/com/terning/core/local/TerningDataStore.kt index 23d3bef82..80678eadb 100644 --- a/core/local/src/main/java/com/terning/core/local/TerningDataStore.kt +++ b/core/local/src/main/java/com/terning/core/local/TerningDataStore.kt @@ -7,5 +7,6 @@ interface TerningDataStore { var userId: Long var alarmAvailable: Boolean var hasRequestedPermission: Boolean + var serverNoticeTimestamp: Long fun clearInfo() -} \ No newline at end of file +} diff --git a/core/local/src/main/java/com/terning/core/local/TerningDataStoreImpl.kt b/core/local/src/main/java/com/terning/core/local/TerningDataStoreImpl.kt index 4d0bc4dcb..5df74c64e 100644 --- a/core/local/src/main/java/com/terning/core/local/TerningDataStoreImpl.kt +++ b/core/local/src/main/java/com/terning/core/local/TerningDataStoreImpl.kt @@ -31,6 +31,10 @@ class TerningDataStoreImpl @Inject constructor( get() = dataStore.getBoolean(PERMISSION_REQUESTED, false) set(value) = dataStore.edit { putBoolean(PERMISSION_REQUESTED, value) } + override var serverNoticeTimestamp: Long + get() = dataStore.getLong(LAST_NOTICE_TIME, 0L) + set(value) = dataStore.edit { putLong(LAST_NOTICE_TIME, value) } + override fun clearInfo() { dataStore.edit().clear().apply() } @@ -42,5 +46,6 @@ class TerningDataStoreImpl @Inject constructor( private const val USER_ID = "USER_ID" private const val ALARM = "ALARM" private const val PERMISSION_REQUESTED = "PERMISSION_REQUESTED" + private const val LAST_NOTICE_TIME = "LAST_NOTICE_TIME" } -} \ No newline at end of file +} diff --git a/data/user/src/main/java/com/terning/data/user/repositoryimpl/UserRepositoryImpl.kt b/data/user/src/main/java/com/terning/data/user/repositoryimpl/UserRepositoryImpl.kt index 1234c2749..4278feb11 100644 --- a/data/user/src/main/java/com/terning/data/user/repositoryimpl/UserRepositoryImpl.kt +++ b/data/user/src/main/java/com/terning/data/user/repositoryimpl/UserRepositoryImpl.kt @@ -48,4 +48,24 @@ class UserRepositoryImpl @Inject constructor( override fun clearInfo() { terningDataStore.clearInfo() } -} \ No newline at end of file + + override fun hasNoticeCooldownPassed(): Boolean { + val lastShownTimestamp = terningDataStore.serverNoticeTimestamp + + if (lastShownTimestamp == 0L) return true + + val currentTime = System.currentTimeMillis() + + val elapsedTime = currentTime - lastShownTimestamp + + return elapsedTime > THREE_HOURS_MS + } + + override fun setNoticeTimestampToNow() { + terningDataStore.serverNoticeTimestamp = System.currentTimeMillis() + } + + companion object { + private const val THREE_HOURS_MS = 3 * 60 * 60 * 1000L + } +} diff --git a/domain/update/src/main/java/com/terning/domain/update/entity/UpdateState.kt b/domain/update/src/main/java/com/terning/domain/update/entity/UpdateState.kt index f875f3277..13ba7e9be 100644 --- a/domain/update/src/main/java/com/terning/domain/update/entity/UpdateState.kt +++ b/domain/update/src/main/java/com/terning/domain/update/entity/UpdateState.kt @@ -5,4 +5,5 @@ sealed class UpdateState { data object NoUpdateAvailable : UpdateState() data class MajorUpdateAvailable(val title: String, val content: String) : UpdateState() data class PatchUpdateAvailable(val title: String, val content: String) : UpdateState() -} \ No newline at end of file + data object ServerNoticeAvailable: UpdateState() +} diff --git a/domain/user/src/main/java/com/terning/domain/user/repository/UserRepository.kt b/domain/user/src/main/java/com/terning/domain/user/repository/UserRepository.kt index a55eaa25f..82c809b31 100644 --- a/domain/user/src/main/java/com/terning/domain/user/repository/UserRepository.kt +++ b/domain/user/src/main/java/com/terning/domain/user/repository/UserRepository.kt @@ -26,4 +26,7 @@ interface UserRepository { fun getPermissionRequested(): Boolean fun clearInfo() + + fun hasNoticeCooldownPassed(): Boolean + fun setNoticeTimestampToNow() } diff --git a/feature/splash/src/main/java/com/terning/feature/splash/SplashRoute.kt b/feature/splash/src/main/java/com/terning/feature/splash/SplashRoute.kt index 5c3590b0c..da0d1565b 100644 --- a/feature/splash/src/main/java/com/terning/feature/splash/SplashRoute.kt +++ b/feature/splash/src/main/java/com/terning/feature/splash/SplashRoute.kt @@ -1,5 +1,7 @@ package com.terning.feature.splash +import android.content.Context +import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -15,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -29,9 +32,9 @@ import com.terning.core.designsystem.theme.TerningMain import com.terning.core.designsystem.theme.TerningPointTheme import com.terning.core.designsystem.theme.White import com.terning.core.designsystem.type.DeeplinkType -import com.terning.feature.splash.SplashUiState import com.terning.feature.splash.component.TerningMajorUpdateDialog import com.terning.feature.splash.component.TerningPatchUpdateDialog +import com.terning.feature.splash.component.TerningServerNoticeDialog import kotlinx.coroutines.launch @Composable @@ -106,7 +109,11 @@ internal fun SplashRoute( SplashScreen( splashUiState = updateState.toUi(), onUpdateButtonClick = context::launchPlayStore, - onUpdateSkipButtonClick = viewModel::checkIfAccessTokenAvailable + onUpdateSkipButtonClick = viewModel::checkIfAccessTokenAvailable, + onDetailButtonClick = { + navigateToServerWebView(context) + viewModel.checkIfAccessTokenAvailable() + } ) } @@ -115,6 +122,7 @@ private fun SplashScreen( splashUiState: SplashUiState, onUpdateButtonClick: () -> Unit, onUpdateSkipButtonClick: () -> Unit, + onDetailButtonClick: () -> Unit, ) { when (splashUiState) { is SplashUiState.MajorUpdateAvailable -> { @@ -138,6 +146,13 @@ private fun SplashScreen( } } + is SplashUiState.ServerNoticeAvailable -> { + TerningServerNoticeDialog( + onDismissButtonClick = onUpdateSkipButtonClick, + onDetailButtonClick = onDetailButtonClick, + ) + } + else -> {} } @@ -156,6 +171,13 @@ private fun SplashScreen( } } +private fun navigateToServerWebView(context: Context) { + CustomTabsIntent.Builder().build().launchUrl(context, SERVER_URL.toUri()) +} + +private const val SERVER_URL = + "https://abundant-quiver-13f.notion.site/2a22867b52c180649a5bfdf1704820a3?pvs=73" + @Preview(showBackground = true) @Composable private fun SplashScreenPreview() { @@ -164,6 +186,7 @@ private fun SplashScreenPreview() { splashUiState = SplashUiState.NoUpdateAvailable, onUpdateButtonClick = {}, onUpdateSkipButtonClick = {}, + onDetailButtonClick = {}, ) } } diff --git a/feature/splash/src/main/java/com/terning/feature/splash/SplashUiState.kt b/feature/splash/src/main/java/com/terning/feature/splash/SplashUiState.kt index e9fd82630..85610a605 100644 --- a/feature/splash/src/main/java/com/terning/feature/splash/SplashUiState.kt +++ b/feature/splash/src/main/java/com/terning/feature/splash/SplashUiState.kt @@ -9,6 +9,7 @@ sealed class SplashUiState { data object NoUpdateAvailable : SplashUiState() data class MajorUpdateAvailable(val title: String, val content: String) : SplashUiState() data class PatchUpdateAvailable(val title: String, val content: String) : SplashUiState() + data object ServerNoticeAvailable : SplashUiState() } fun UpdateState.toUi(): SplashUiState = when (this) { @@ -16,4 +17,5 @@ fun UpdateState.toUi(): SplashUiState = when (this) { UpdateState.NoUpdateAvailable -> SplashUiState.NoUpdateAvailable is UpdateState.MajorUpdateAvailable -> SplashUiState.MajorUpdateAvailable(title, content) is UpdateState.PatchUpdateAvailable -> SplashUiState.PatchUpdateAvailable(title, content) + is UpdateState.ServerNoticeAvailable -> SplashUiState.ServerNoticeAvailable } diff --git a/feature/splash/src/main/java/com/terning/feature/splash/SplashViewModel.kt b/feature/splash/src/main/java/com/terning/feature/splash/SplashViewModel.kt index e04448fe0..b7f7b2aa4 100644 --- a/feature/splash/src/main/java/com/terning/feature/splash/SplashViewModel.kt +++ b/feature/splash/src/main/java/com/terning/feature/splash/SplashViewModel.kt @@ -18,7 +18,7 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( private val userRepository: UserRepository, - private val getLatestVersionUseCase: GetUpdateStateUseCase + private val getLatestVersionUseCase: GetUpdateStateUseCase, ) : ViewModel() { private val _sideEffects = MutableSharedFlow() @@ -47,6 +47,15 @@ class SplashViewModel @Inject constructor( private fun checkIfUpdateNotAvailable(updateState: UpdateState) { if (updateState is UpdateState.NoUpdateAvailable) { + checkServerNotice() + } + } + + private fun checkServerNotice() = viewModelScope.launch { + if (userRepository.hasNoticeCooldownPassed()) { + _updateState.value = UpdateState.ServerNoticeAvailable + userRepository.setNoticeTimestampToNow() + } else { checkIfAccessTokenAvailable() } } @@ -58,4 +67,4 @@ class SplashViewModel @Inject constructor( companion object { private const val DELAY_TIME = 500L } -} \ No newline at end of file +} diff --git a/feature/splash/src/main/java/com/terning/feature/splash/component/TerningServerNoticeDialog.kt b/feature/splash/src/main/java/com/terning/feature/splash/component/TerningServerNoticeDialog.kt new file mode 100644 index 000000000..b41cee8fd --- /dev/null +++ b/feature/splash/src/main/java/com/terning/feature/splash/component/TerningServerNoticeDialog.kt @@ -0,0 +1,125 @@ +package com.terning.feature.splash.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.terning.core.designsystem.component.dialog.NoticeDialogButton +import com.terning.core.designsystem.theme.Back +import com.terning.core.designsystem.theme.Black +import com.terning.core.designsystem.theme.Grey150 +import com.terning.core.designsystem.theme.Grey200 +import com.terning.core.designsystem.theme.Grey350 +import com.terning.core.designsystem.theme.Grey400 +import com.terning.core.designsystem.theme.Grey500 +import com.terning.core.designsystem.theme.TerningMain +import com.terning.core.designsystem.theme.TerningMain2 +import com.terning.core.designsystem.theme.TerningPointTheme +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.core.designsystem.theme.White +import com.terning.feature.splash.R + +@Composable +internal fun TerningServerNoticeDialog( + onDismissButtonClick: () -> Unit, + onDetailButtonClick: () -> Unit, +) { + Dialog( + onDismissRequest = { }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false, + ) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(horizontal = 28.dp) + .background(color = White, shape = RoundedCornerShape(20.dp)) + .padding(12.dp) + ) { + Text( + text = stringResource(R.string.dialog_server_title), + style = TerningTheme.typography.title2, + color = Grey500, + modifier = Modifier.padding(top = 20.dp) + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = stringResource(R.string.dialog_bodytitle), + style = TerningTheme.typography.body4.copy(textAlign = TextAlign.Center), + overflow = TextOverflow.Clip, + color = Grey400, + ) + Spacer(modifier = Modifier.height(26.dp)) + Column( + modifier = Modifier + .clip(RoundedCornerShape(5.dp)) + .background(Back) + .padding( + vertical = 18.dp, + horizontal = 58.dp + ) + ) { + Text( + text = stringResource(R.string.dialog_server_over_title), + style = TerningTheme.typography.body6, + color = Black, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.dialog_server_over_day), + style = TerningTheme.typography.detail4, + color = Grey500 + ) + } + Spacer(modifier = Modifier.height(26.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + NoticeDialogButton( + text = stringResource(R.string.dialog_dismiss), + contentColor = Grey350, + pressedContainerColor = Grey200, + containerColor = Grey150, + onClick = onDismissButtonClick, + modifier = Modifier.weight(1f) + ) + NoticeDialogButton( + text = stringResource(R.string.dialog_detail), + contentColor = White, + pressedContainerColor = TerningMain2, + containerColor = TerningMain, + onClick = onDetailButtonClick, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 780) +@Composable +private fun TerningPatchUpdateDialogPreview() { + TerningPointTheme { + TerningServerNoticeDialog( + onDismissButtonClick = {}, + onDetailButtonClick = {}, + ) + } +} diff --git a/feature/splash/src/main/res/values/strings.xml b/feature/splash/src/main/res/values/strings.xml index da9308db8..e5a4d9149 100644 --- a/feature/splash/src/main/res/values/strings.xml +++ b/feature/splash/src/main/res/values/strings.xml @@ -6,4 +6,14 @@ λ‹€μŒμ— ν•˜κΈ° μ—…λ°μ΄νŠΈ ν•˜κΈ° μ—…λ°μ΄νŠΈ ν•˜λŸ¬ κ°€κΈ° - \ No newline at end of file + + 터닝 μ„œλΉ„μŠ€ μ’…λ£Œ μ•ˆλ‚΄ + κ·Έλ™μ•ˆ 터닝을 μ‚¬λž‘ν•΄μ£Όμ‹  λͺ¨λ“  λΆ„λ“€κ»˜\n + μ§„μ‹¬μœΌλ‘œ κ°μ‚¬μ˜ 말씀을 λ“œλ¦½λ‹ˆλ‹€.\n + β€˜ν„°λ‹β€™μ€ 11μ›” 25μΌλΆ€λ‘œ μ„œλΉ„μŠ€κ°€ μ’…λ£Œλ  μ˜ˆμ •μ΄λ©°,\n + μžμ„Έν•œ 사항은 곡지 λ‚΄μš©μ„ ν™•μΈν•΄μ£Όμ„Έμš”. + μ„œλΉ„μŠ€ μ’…λ£Œ μ˜ˆμ •μΌ + 2025λ…„ 11μ›” 25일 + λ‹«κΈ° + μžμ„Ένžˆ 보기 + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ec1801e4..92e57073b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ compileSdk = "35" minSdk = "28" targetSdk = "35" -versionName = "1.3.8" -versionCode = "103080" +versionName = "1.4.0" +versionCode = "104000" jvmTarget = "1.8" ## Android gradle plugin