diff --git a/.gitignore b/.gitignore
index aa724b7..efb17ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,332 @@
+# Created by https://www.toptal.com/developers/gitignore/api/macos,android,windows,intellij,androidstudio
+# Edit at https://www.toptal.com/developers/gitignore?templates=macos,android,windows,intellij,androidstudio
+
+### Android ###
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Log/OS Files
+*.log
+
+# Android Studio generated files and folders
+captures/
+.externalNativeBuild/
+.cxx/
+*.apk
+output.json
+
+# IntelliJ
*.iml
-.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
+.idea/
+!.idea/icon.svg
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
+
+# Keystore files
+*.jks
+*.keystore
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Android Profiling
+*.hprof
+
+### Android Patch ###
+gen-external-apklibs
+
+# Replacement of .externalNativeBuild directories introduced
+# with Android Studio 3.5.
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+# Azure Toolkit for IntelliJ plugin
+# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
+.idea/**/azureSettings.xml
+
+### macOS ###
+# General
.DS_Store
-/build
-/captures
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### AndroidStudio ###
+# Covers files to be ignored for android development using Android Studio.
+
+# Built application files
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+
+# Gradle files
+.gradle
+
+# Signing files
+.signing/
+
+# Local configuration file (sdk path, etc)
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+
+# Android Studio
+/*/build/
+/*/local.properties
+/*/out
+/*/*/build
+/*/*/production
+.navigation/
+*.ipr
+*~
+*.swp
+
+# Keystore files
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Android Patch
+
+# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
-.cxx
-local.properties
+
+# NDK
+obj/
+
+# IntelliJ IDEA
+/out/
+
+# User-specific configurations
+.idea/caches/
+.idea/libraries/
+.idea/shelf/
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/.name
+.idea/compiler.xml
+.idea/copyright/profiles_settings.xml
+.idea/encodings.xml
+.idea/misc.xml
+.idea/modules.xml
+.idea/scopes/scope_settings.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+.idea/datasources.xml
+.idea/dataSources.ids
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+.idea/assetWizardSettings.xml
+.idea/gradle.xml
+.idea/jarRepositories.xml
+.idea/navEditor.xml
+
+# Legacy Eclipse project files
+.classpath
+.project
+.cproject
+.settings/
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.war
+*.ear
+
+# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
+hs_err_pid*
+
+## Plugin-specific files:
+
+# mpeltonen/sbt-idea plugin
+
+# JIRA plugin
+
+# Mongo Explorer plugin
+.idea/mongoSettings.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+
+### AndroidStudio Patch ###
+
+!/gradle/wrapper/gradle-wrapper.jar
+
+# End of https://www.toptal.com/developers/gitignore/api/macos,android,windows,intellij,androidstudio
\ No newline at end of file
diff --git a/.idea/icon.svg b/.idea/icon.svg
new file mode 100644
index 0000000..8f87d94
--- /dev/null
+++ b/.idea/icon.svg
@@ -0,0 +1,29 @@
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2d4e24e..de7646d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/kiero/core/common/base/BaseUiStateProvider.kt b/app/src/main/java/com/kiero/core/common/base/BaseUiStateProvider.kt
new file mode 100644
index 0000000..d1127e3
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/common/base/BaseUiStateProvider.kt
@@ -0,0 +1,14 @@
+package com.kiero.core.common.base
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.kiero.core.model.UiState
+
+open class BaseUiStateProvider(
+ successData: T
+) : PreviewParameterProvider> {
+ override val values: Sequence> = sequenceOf(
+ UiState.Loading,
+ UiState.Success(successData),
+ UiState.Failure("네트워크 연결을 확인해주세요. (테스트 에러)")
+ )
+}
diff --git a/app/src/main/java/com/kiero/core/common/extension/FlowExt.kt b/app/src/main/java/com/kiero/core/common/extension/FlowExt.kt
new file mode 100644
index 0000000..2a3eef0
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/common/extension/FlowExt.kt
@@ -0,0 +1,47 @@
+package com.kiero.core.common.extension
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.flowWithLifecycle
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+fun Flow.collectSideEffect(
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
+ action: suspend (T) -> Unit
+) {
+ val lifecycle = lifecycleOwner.lifecycle
+
+ LaunchedEffect(this, lifecycle) {
+ flowWithLifecycle(lifecycle, minActiveState)
+ .collect(action)
+ }
+}
+
+@Composable
+fun Flow.collectSingleEvent(
+ lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+ throttleTime: Long = 500L,
+ action: suspend (T) -> Unit
+) {
+ val lifecycle = lifecycleOwner.lifecycle
+ var lastEmitTime by remember { mutableLongStateOf(0L) }
+
+ LaunchedEffect(this, lifecycle) {
+ flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { value ->
+ val currentTime = System.currentTimeMillis()
+ if (currentTime - lastEmitTime > throttleTime) {
+ lastEmitTime = currentTime
+ action(value)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/kiero/core/common/extension/StateFlowExt.kt b/app/src/main/java/com/kiero/core/common/extension/StateFlowExt.kt
new file mode 100644
index 0000000..789611f
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/common/extension/StateFlowExt.kt
@@ -0,0 +1,17 @@
+package com.kiero.core.common.extension
+
+import com.kiero.core.model.UiState
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+
+inline fun MutableStateFlow>.updateSuccess(
+ crossinline onUpdate: (T) -> T
+) {
+ update { currentState ->
+ if (currentState is UiState.Success) {
+ currentState.copy(data = onUpdate(currentState.data))
+ } else {
+ currentState
+ }
+ }
+}
diff --git a/app/src/main/java/com/kiero/core/common/util/HandleError.kt b/app/src/main/java/com/kiero/core/common/extension/ThrowableExt.kt
similarity index 67%
rename from app/src/main/java/com/kiero/core/common/util/HandleError.kt
rename to app/src/main/java/com/kiero/core/common/extension/ThrowableExt.kt
index 8c00884..5c2d953 100644
--- a/app/src/main/java/com/kiero/core/common/util/HandleError.kt
+++ b/app/src/main/java/com/kiero/core/common/extension/ThrowableExt.kt
@@ -1,14 +1,16 @@
-package com.kiero.core.common.util
+package com.kiero.core.common.extension
import java.io.IOException
+import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.util.concurrent.TimeoutException
-fun handleError(throwable: Throwable): String {
- return when (throwable) {
+fun Throwable.toHandleErrorMessage(): String {
+ return when (this) {
is TimeoutException -> "네트워크 시간이 초과되었습니다. 다시 시도해주세요."
+ is SocketTimeoutException -> "네트워크 시간이 초과되었습니다. 다시 시도해주세요"
is UnknownHostException -> "서버에 연결할 수 없습니다. 인터넷 연결을 확인하세요."
is IOException -> "네트워크 연결에 문제가 발생했습니다. 다시 시도하세요."
else -> "알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도하세요."
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/kiero/core/model/trigger/DialogState.kt b/app/src/main/java/com/kiero/core/model/trigger/DialogState.kt
new file mode 100644
index 0000000..0a77c5e
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/model/trigger/DialogState.kt
@@ -0,0 +1,17 @@
+package com.kiero.core.model.trigger
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+
+@Immutable
+data class DialogState(
+ val isVisible: Boolean = false,
+ val onClickAction: () -> Unit = {}
+)
+
+@Stable
+class DialogTrigger(
+ val show: (() -> Unit) -> Unit,
+ val dismiss: () -> Unit
+)
+
diff --git a/app/src/main/java/com/kiero/core/model/trigger/GlobalUiEventHolder.kt b/app/src/main/java/com/kiero/core/model/trigger/GlobalUiEventHolder.kt
new file mode 100644
index 0000000..7eddbe8
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/model/trigger/GlobalUiEventHolder.kt
@@ -0,0 +1,10 @@
+package com.kiero.core.model.trigger
+
+import androidx.compose.runtime.Stable
+
+@Stable
+class GlobalUiEventHolder(
+ val dialogTrigger: DialogTrigger,
+ val showToast: (String) -> Unit,
+ val showSnackbar: (SnackbarState) -> Unit
+)
diff --git a/app/src/main/java/com/kiero/core/model/trigger/SnackbarState.kt b/app/src/main/java/com/kiero/core/model/trigger/SnackbarState.kt
new file mode 100644
index 0000000..cb0d256
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/model/trigger/SnackbarState.kt
@@ -0,0 +1,8 @@
+package com.kiero.core.model.trigger
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class SnackbarState(
+ val message: String = "",
+)
diff --git a/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitor.kt b/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitor.kt
new file mode 100644
index 0000000..d74f912
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitor.kt
@@ -0,0 +1,7 @@
+package com.kiero.core.network.monitor
+
+import kotlinx.coroutines.flow.Flow
+
+interface NetworkMonitor {
+ val isOnline: Flow
+}
diff --git a/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitorImpl.kt b/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitorImpl.kt
new file mode 100644
index 0000000..bba80f1
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitorImpl.kt
@@ -0,0 +1,61 @@
+package com.kiero.core.network.monitor
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import androidx.core.content.getSystemService
+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.conflate
+import javax.inject.Inject
+
+class NetworkMonitorImpl @Inject constructor(
+ @param:ApplicationContext private val context: Context,
+) : NetworkMonitor {
+ override val isOnline: Flow = callbackFlow {
+ val connectivityManager = context.getSystemService()
+ if (connectivityManager == null) {
+ channel.trySend(false)
+ channel.close()
+ return@callbackFlow
+ }
+
+ val callback = object : ConnectivityManager.NetworkCallback() {
+ private val networks = mutableSetOf()
+
+ override fun onAvailable(network: Network) {
+ networks += network
+ channel.trySend(true)
+ }
+
+ override fun onLost(network: Network) {
+ networks -= network
+ channel.trySend(networks.isNotEmpty())
+ }
+ }
+
+ val request = NetworkRequest.Builder()
+ .addCapability(
+ NetworkCapabilities.NET_CAPABILITY_INTERNET
+ )
+ .build()
+
+ connectivityManager.registerNetworkCallback(request, callback)
+
+ // 현재 활성 네트워크가 있고 + 그 네트워크가 인터넷이 되는지 확인용
+ val activeNetwork = connectivityManager.activeNetwork
+ val netCapability = connectivityManager.getNetworkCapabilities(activeNetwork)
+ val isConnected =
+ netCapability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
+
+ channel.trySend(isConnected)
+
+ awaitClose {
+ connectivityManager.unregisterNetworkCallback(callback)
+ }
+ }.conflate()
+}
diff --git a/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitorModule.kt b/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitorModule.kt
new file mode 100644
index 0000000..4d2e574
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/network/monitor/NetworkMonitorModule.kt
@@ -0,0 +1,19 @@
+package com.kiero.core.network.monitor
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class NetworkMonitorModule {
+
+ @Binds
+ @Singleton
+ abstract fun bindNetworkMonitor(
+ networkMonitor: NetworkMonitorImpl
+ ): NetworkMonitor
+
+}
diff --git a/app/src/main/java/com/kiero/core/trigger/LocalGlobalUiEventTrigger.kt b/app/src/main/java/com/kiero/core/trigger/LocalGlobalUiEventTrigger.kt
new file mode 100644
index 0000000..510dd1f
--- /dev/null
+++ b/app/src/main/java/com/kiero/core/trigger/LocalGlobalUiEventTrigger.kt
@@ -0,0 +1,8 @@
+package com.kiero.core.trigger
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import com.kiero.core.model.trigger.GlobalUiEventHolder
+
+val LocalGlobalUiEventTrigger = staticCompositionLocalOf {
+ error("No GlobalUiEvent Trigger provided")
+}
diff --git a/app/src/main/java/com/kiero/data/di/DataSourceModule.kt b/app/src/main/java/com/kiero/data/di/DataSourceModule.kt
index b3c506d..eac15f7 100644
--- a/app/src/main/java/com/kiero/data/di/DataSourceModule.kt
+++ b/app/src/main/java/com/kiero/data/di/DataSourceModule.kt
@@ -21,8 +21,7 @@ abstract class DummyDataSourceModule {
abstract fun bindDummyDataSource(
dummyDataSourceImpl: DummyDataSourceImpl,
): DummyDataSource
-
-
+
@Binds
@Singleton
abstract fun bindAuthDataSource(
@@ -34,4 +33,4 @@ abstract class DummyDataSourceModule {
abstract fun bindAuthLocalDataSource(
authLocalDataSourceImpl: AuthLocalDataSourceImpl,
): AuthLocalDataSource
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/kiero/presentation/auth/viewmodel/AuthViewModel.kt b/app/src/main/java/com/kiero/presentation/auth/viewmodel/AuthViewModel.kt
index 64f0302..5e17189 100644
--- a/app/src/main/java/com/kiero/presentation/auth/viewmodel/AuthViewModel.kt
+++ b/app/src/main/java/com/kiero/presentation/auth/viewmodel/AuthViewModel.kt
@@ -6,6 +6,11 @@ import com.kiero.core.common.util.handleError
import com.kiero.data.auth.repository.AuthRepository
import com.kiero.presentation.auth.model.AuthSideEffect
import com.kiero.presentation.auth.model.AuthState
+import com.kiero.core.common.extension.toHandleErrorMessage
+import com.kiero.core.model.UiState
+import com.kiero.data.auth.repository.DummyRepository
+import com.kiero.presentation.auth.model.DummySideEffect
+import com.kiero.presentation.auth.model.DummyState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
diff --git a/app/src/main/java/com/kiero/presentation/kid/journey/KidJourneyScreen.kt b/app/src/main/java/com/kiero/presentation/kid/journey/KidJourneyScreen.kt
index 5835052..f860c09 100644
--- a/app/src/main/java/com/kiero/presentation/kid/journey/KidJourneyScreen.kt
+++ b/app/src/main/java/com/kiero/presentation/kid/journey/KidJourneyScreen.kt
@@ -8,16 +8,38 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import com.kiero.core.common.extension.collectSideEffect
import com.kiero.core.designsystem.theme.KieroTheme
+import com.kiero.core.model.trigger.SnackbarState
+import com.kiero.core.trigger.LocalGlobalUiEventTrigger
+import com.kiero.presentation.kid.journey.state.KidJourneySideEffect
+import com.kiero.presentation.kid.journey.viewmodel.KidJourneyViewModel
@Composable
fun KidJourneyRoute(
paddingValues: PaddingValues,
navigateUp: () -> Unit,
+ viewModel: KidJourneyViewModel = hiltViewModel()
) {
+ val globalTrigger = LocalGlobalUiEventTrigger.current
+
+ viewModel.sideEffect.collectSideEffect { sideEffect ->
+ when (sideEffect) {
+ is KidJourneySideEffect.ShowToast -> globalTrigger.showToast(sideEffect.message)
+ is KidJourneySideEffect.ShowSnackbar -> globalTrigger.showSnackbar(
+ SnackbarState(
+ message = sideEffect.message
+ )
+ )
+
+ KidJourneySideEffect.ShowDialog -> globalTrigger.dialogTrigger.show {}
+ }
+ }
+
KidJourneyScreen(
paddingValues = paddingValues,
- navigateUp = navigateUp
+ navigateUp = navigateUp,
)
}
@@ -30,7 +52,7 @@ private fun KidJourneyScreen(
Column(
modifier = modifier
.fillMaxSize()
- .padding(paddingValues),
+ .padding(paddingValues)
) {
Text(
text = "오늘의 여정"
@@ -42,4 +64,4 @@ private fun KidJourneyScreen(
@Preview
private fun KidJourneyScreenPreview() {
KieroTheme {}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/kiero/presentation/kid/journey/state/KidJourneyContract.kt b/app/src/main/java/com/kiero/presentation/kid/journey/state/KidJourneyContract.kt
new file mode 100644
index 0000000..ea03dc3
--- /dev/null
+++ b/app/src/main/java/com/kiero/presentation/kid/journey/state/KidJourneyContract.kt
@@ -0,0 +1,13 @@
+package com.kiero.presentation.kid.journey.state
+
+sealed interface KidJourneySideEffect {
+ data object ShowDialog : KidJourneySideEffect
+
+ data class ShowToast(
+ val message: String
+ ) : KidJourneySideEffect
+
+ data class ShowSnackbar(
+ val message: String,
+ ) : KidJourneySideEffect
+}
diff --git a/app/src/main/java/com/kiero/presentation/kid/journey/viewmodel/KidJourneyViewModel.kt b/app/src/main/java/com/kiero/presentation/kid/journey/viewmodel/KidJourneyViewModel.kt
new file mode 100644
index 0000000..56f09b7
--- /dev/null
+++ b/app/src/main/java/com/kiero/presentation/kid/journey/viewmodel/KidJourneyViewModel.kt
@@ -0,0 +1,16 @@
+package com.kiero.presentation.kid.journey.viewmodel
+
+import androidx.lifecycle.ViewModel
+import com.kiero.presentation.kid.journey.state.KidJourneySideEffect
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import javax.inject.Inject
+
+@HiltViewModel
+class KidJourneyViewModel @Inject constructor(
+) : ViewModel() {
+ private val _sideEffect = MutableSharedFlow()
+ val sideEffect: SharedFlow = _sideEffect.asSharedFlow()
+}
diff --git a/app/src/main/java/com/kiero/presentation/main/activity/MainActivity.kt b/app/src/main/java/com/kiero/presentation/main/activity/MainActivity.kt
index 92e1ed3..a2a00ea 100644
--- a/app/src/main/java/com/kiero/presentation/main/activity/MainActivity.kt
+++ b/app/src/main/java/com/kiero/presentation/main/activity/MainActivity.kt
@@ -1,23 +1,46 @@
package com.kiero.presentation.main.activity
+import android.annotation.SuppressLint
+import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import com.kiero.R
import com.kiero.core.designsystem.theme.KieroTheme
+import com.kiero.core.network.monitor.NetworkMonitor
+import com.kiero.presentation.main.navigation.rememberMainAppState
import com.kiero.presentation.main.screen.MainRoute
import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+ @Inject
+ lateinit var networkMonitor: NetworkMonitor
+
+ @SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- enableEdgeToEdge()
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.dark(
+ getColor(R.color.bottombar_color)
+ ),
+ navigationBarStyle = SystemBarStyle.dark(
+ getColor(R.color.bottombar_color)
+ )
+ )
setContent {
KieroTheme {
- MainRoute()
+ val appState = rememberMainAppState(networkMonitor = networkMonitor)
+
+ MainRoute(
+ appState = appState
+ )
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/kiero/presentation/main/navigation/MainAppState.kt b/app/src/main/java/com/kiero/presentation/main/navigation/MainAppState.kt
index afc33b4..974f6e1 100644
--- a/app/src/main/java/com/kiero/presentation/main/navigation/MainAppState.kt
+++ b/app/src/main/java/com/kiero/presentation/main/navigation/MainAppState.kt
@@ -1,6 +1,5 @@
package com.kiero.presentation.main.navigation
-import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
@@ -10,6 +9,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptions
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
+import com.kiero.core.network.monitor.NetworkMonitor
import com.kiero.presentation.auth.navigation.AuthGraph
import com.kiero.presentation.kid.journey.navigation.navigateToJourney
import com.kiero.presentation.kid.mission.navigation.navigateToMission
@@ -34,7 +34,18 @@ import kotlinx.coroutines.flow.stateIn
class MainAppState(
val navController: NavHostController,
coroutineScope: CoroutineScope,
+ networkMonitor: NetworkMonitor
) {
+ val startDestination = AuthGraph
+
+ val isOffline: StateFlow = networkMonitor.isOnline
+ .map(Boolean::not)
+ .stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.WhileSubscribed(5_000),
+ initialValue = false
+ )
+
private val currentDestination = navController.currentBackStackEntryFlow
.map { it.destination }
.stateIn(
@@ -87,7 +98,6 @@ class MainAppState(
initialValue = false
)
-
fun navigateToParent() {
navController.navigate(ParentGraph) {
popUpTo(AuthGraph) { inclusive = true }
@@ -157,14 +167,15 @@ class MainAppState(
@Composable
fun rememberMainAppState(
+ networkMonitor: NetworkMonitor,
navController: NavHostController = rememberNavController(),
- snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
coroutineScope: CoroutineScope = rememberCoroutineScope(),
): MainAppState {
- return remember(navController, snackbarHostState, coroutineScope) {
+ return remember(networkMonitor, navController, coroutineScope) {
MainAppState(
+ networkMonitor = networkMonitor,
navController = navController,
coroutineScope = coroutineScope
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/kiero/presentation/main/screen/MainScreen.kt b/app/src/main/java/com/kiero/presentation/main/screen/MainScreen.kt
index 40ce788..3fcc831 100644
--- a/app/src/main/java/com/kiero/presentation/main/screen/MainScreen.kt
+++ b/app/src/main/java/com/kiero/presentation/main/screen/MainScreen.kt
@@ -1,29 +1,49 @@
package com.kiero.presentation.main.screen
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
+import androidx.activity.compose.LocalActivity
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kiero.core.designsystem.component.KieroSnackbar
+import com.kiero.core.model.trigger.DialogTrigger
+import com.kiero.core.model.trigger.GlobalUiEventHolder
+import com.kiero.core.model.trigger.SnackbarState
+import com.kiero.core.trigger.LocalGlobalUiEventTrigger
import com.kiero.presentation.main.navigation.KidMainTab
import com.kiero.presentation.main.navigation.KieroNavHost
import com.kiero.presentation.main.navigation.MainAppState
import com.kiero.presentation.main.navigation.ParentMainTab
import com.kiero.presentation.main.navigation.component.MainBottomBar
-import com.kiero.presentation.main.navigation.rememberMainAppState
+import com.kiero.presentation.main.state.rememberDialogStateHolder
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import timber.log.Timber
@Composable
fun MainRoute(
- appState: MainAppState = rememberMainAppState()
+ appState: MainAppState,
) {
val snackBarHostState = remember { SnackbarHostState() }
@@ -39,12 +59,20 @@ fun MainScreen(
snackBarHostState: SnackbarHostState,
modifier: Modifier = Modifier,
) {
+ val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+
+ val isOffline by appState.isOffline.collectAsStateWithLifecycle()
+
val showParentBottomBar by appState.showParentBottomBar.collectAsStateWithLifecycle()
val showKidBottomBar by appState.showKidBottomBar.collectAsStateWithLifecycle()
+
val currentParentTab by appState.currentParentTab.collectAsStateWithLifecycle()
val currentKidTab by appState.currentKidTab.collectAsStateWithLifecycle()
+ var currentSnackbarState by remember { mutableStateOf(null) }
val isVisible = showParentBottomBar || showKidBottomBar
+
val containerShape = if (showParentBottomBar) {
RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp)
} else {
@@ -57,35 +85,131 @@ fun MainScreen(
}
val currentTab = if (showParentBottomBar) currentParentTab else currentKidTab
- Scaffold(
- modifier = modifier.fillMaxSize(),
- snackbarHost = {
- SnackbarHost(hostState = snackBarHostState) { data ->
- KieroSnackbar(
- message = data.visuals.message,
- // TODO: 디자인 확정 후 스낵바 높이 및 패딩 수정 필요
- modifier = Modifier.padding(16.dp)
- )
- }
- },
- bottomBar = {
- MainBottomBar(
- isVisible = isVisible,
- containerShape = containerShape,
- tabs = tabs,
- currentTab = currentTab,
- onTabSelected = { tab ->
- when (tab) {
- is ParentMainTab -> appState.navigateParentTab(tab)
- is KidMainTab -> appState.navigateKidTab(tab)
+ val dialogState = rememberDialogStateHolder()
+
+ val onShowToast: (String) -> Unit = remember {
+ { message ->
+ Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ val onShowSnackbar: (SnackbarState) -> Unit = remember(scope, snackBarHostState) {
+ { state ->
+ currentSnackbarState = state
+ scope.launch {
+ snackBarHostState.currentSnackbarData?.dismiss()
+
+ val job = launch {
+ snackBarHostState.showSnackbar(
+ message = state.message,
+ )
+ }
+ job.invokeOnCompletion {
+ if (currentSnackbarState == state) {
+ currentSnackbarState = null
}
}
- )
+ delay(2000L)
+ job.cancel()
+ }
}
- ) { paddingValues ->
- KieroNavHost(
- appState = appState,
- paddingValues = paddingValues,
+ }
+
+ val eventHolder = remember(dialogState, onShowToast, onShowSnackbar) {
+ GlobalUiEventHolder(
+ dialogTrigger = DialogTrigger(
+ show = { onConfirm ->
+ dialogState.showDialog(onConfirm)
+ },
+ dismiss = {
+ dialogState.dismissDialog()
+ }
+ ),
+ showToast = onShowToast,
+ showSnackbar = onShowSnackbar
)
}
-}
\ No newline at end of file
+
+ LaunchedEffect(isOffline) {
+ Timber.e("네트워크 상태 변경 감지: isOffline = $isOffline")
+
+ if (isOffline && !dialogState.dialogState.isVisible) {
+ eventHolder.dialogTrigger.show {
+ eventHolder.dialogTrigger.dismiss()
+ }
+ }
+ }
+
+ HandleBackPressToExit(
+ onShowToast = {
+ onShowToast("버튼을 한번 더 누르면 앱이 종료됩니다.")
+ }
+ )
+
+ CompositionLocalProvider(
+ LocalGlobalUiEventTrigger provides eventHolder
+ ) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ .navigationBarsPadding()
+ ) {
+ Scaffold(
+ snackbarHost = {
+ SnackbarHost(hostState = snackBarHostState) { data ->
+ KieroSnackbar(
+ message = data.visuals.message,
+ // TODO: 디자인 확정 후 스낵바 높이 및 패딩 수정 필요
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ },
+ bottomBar = {
+ MainBottomBar(
+ isVisible = isVisible,
+ containerShape = containerShape,
+ tabs = tabs,
+ currentTab = currentTab,
+ onTabSelected = { tab ->
+ when (tab) {
+ is ParentMainTab -> appState.navigateParentTab(tab)
+ is KidMainTab -> appState.navigateKidTab(tab)
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ if (dialogState.dialogState.isVisible) {
+ // Todo : 공통 Dialog 띄우기
+ Timber.e("Dialog 띄워짐")
+ }
+
+ KieroNavHost(
+ appState = appState,
+ paddingValues = paddingValues,
+ startDestination = appState.startDestination
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun HandleBackPressToExit(
+ enabled: Boolean = true,
+ exitDuration: Long = 2000L,
+ onShowToast: () -> Unit = {}
+) {
+ val activity = LocalActivity.current
+ var backPressedTime by remember { mutableLongStateOf(0L) }
+
+ BackHandler(enabled = enabled) {
+ if (System.currentTimeMillis() - backPressedTime <= exitDuration) {
+ activity?.finish()
+ } else {
+ onShowToast()
+ }
+ backPressedTime = System.currentTimeMillis()
+ }
+}
diff --git a/app/src/main/java/com/kiero/presentation/main/state/DialogStateHolder.kt b/app/src/main/java/com/kiero/presentation/main/state/DialogStateHolder.kt
new file mode 100644
index 0000000..94acf8d
--- /dev/null
+++ b/app/src/main/java/com/kiero/presentation/main/state/DialogStateHolder.kt
@@ -0,0 +1,28 @@
+package com.kiero.presentation.main.state
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.kiero.core.model.trigger.DialogState
+
+@Stable
+class DialogStateHolder {
+ var dialogState by mutableStateOf(DialogState())
+ private set
+
+ fun showDialog(onClick: () -> Unit) {
+ dialogState = DialogState(isVisible = true, onClickAction = onClick)
+ }
+
+ fun dismissDialog() {
+ dialogState = dialogState.copy(isVisible = false)
+ }
+}
+
+@Composable
+fun rememberDialogStateHolder(): DialogStateHolder = remember {
+ DialogStateHolder()
+}
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f8c6127..7090aac 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,4 +7,6 @@
#FF018786
#FF000000
#FFFFFFFF
+
+ #FF232428
\ No newline at end of file