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