From 13ef566004a9c4e478d81b1f5b1421bcdcb6d368 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 23 Jul 2025 20:17:53 +0900 Subject: [PATCH 1/6] =?UTF-8?q?[REFACTOR/#235]=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/yapp/mission/MissionViewModel.kt | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index dc4b8d84..9fc9c340 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -31,7 +31,7 @@ class MissionViewModel @Inject constructor( private val fortuneRepository: FortuneRepository, private val userInfoRepository: UserInfoRepository, private val app: Application, - private val savedStateHandle: SavedStateHandle, + val savedStateHandle: SavedStateHandle, ) : ViewModel(), ContainerHost { override val container: Container = container( @@ -40,25 +40,28 @@ class MissionViewModel @Inject constructor( savedStateHandle.get("notificationId")?.toLong()?.let { sendAlarmDismissIntent(it) } - loadMissionInfo() + loadMissionInfo( + missionTypeRaw = savedStateHandle.get("missionType"), + missionCountRaw = savedStateHandle.get("missionCount"), + ) } fun processAction(action: MissionContract.Action) { when (action) { - is MissionContract.Action.ShakeCard -> handleShake() - is MissionContract.Action.ClickCard -> handleClick() + is MissionContract.Action.ShakeCard -> handleMissionProgress(MissionType.SHAKE) + is MissionContract.Action.ClickCard -> handleMissionProgress(MissionType.TAP) is MissionContract.Action.ShowExitDialog -> showExitDialog() is MissionContract.Action.HideExitDialog -> hideExitDialog() is MissionContract.Action.RetryPostFortune -> retryPostFortune() } } - private fun loadMissionInfo() = intent { - val missionTypeString = savedStateHandle.get("missionType") - val missionCountString = savedStateHandle.get("missionCount") - - val missionType = missionTypeString?.toIntOrNull() ?: MissionType.TAP.value - val missionCount = missionCountString?.toIntOrNull() ?: 10 + private fun loadMissionInfo( + missionTypeRaw: String?, + missionCountRaw: String?, + ) = intent { + val missionType = missionTypeRaw?.toIntOrNull() ?: MissionType.TAP.value + val missionCount = missionCountRaw?.toIntOrNull() ?: 10 reduce { state.copy( @@ -77,45 +80,35 @@ class MissionViewModel @Inject constructor( reduce { state.copy(showExitDialog = false) } } - private fun handleShake() = intent { - if (state.missionType != MissionType.SHAKE) return@intent + private fun handleMissionProgress(missionType: MissionType) = intent { + val isLast = state.currentCount >= state.missionCount - 1 + val nextCount = state.currentCount + 1 - val currentCount = state.currentCount - if (currentCount < state.missionCount - 1) { - performHapticSuccess() - reduce { state.copy(currentCount = currentCount + 1) } - } else if (!state.isFlipped) { - completeMission(type = "shake") + performHapticSuccess() + + if (isLast) { + completeMission(type = missionType.name.lowercase()) reduce { state.copy( isMissionCompleted = true, currentCount = state.missionCount, - isFlipped = true, + showFinalAnimation = true, ) } delay(500) - } - } + } else { + val transientState = if (missionType == MissionType.TAP) { + state.copy(currentCount = nextCount, playWhenClick = true) + } else { + state.copy(currentCount = nextCount) + } - private fun handleClick() = intent { - if (state.missionType != MissionType.TAP) return@intent + reduce { transientState } - val currentCount = state.currentCount - if (currentCount < state.missionCount - 1) { - performHapticSuccess() - reduce { state.copy(currentCount = currentCount + 1, playWhenClick = true) } - delay(500) - reduce { state.copy(playWhenClick = false) } - } else { - completeMission("click") - reduce { - state.copy( - isMissionCompleted = true, - currentCount = state.missionCount, - showFinalAnimation = true, - ) + if (missionType == MissionType.TAP) { + delay(500) + reduce { state.copy(playWhenClick = false) } } - delay(500) } } From 1acb13ace5b9a5a2ac8a3f396bcc6e51ddc7459e Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 23 Jul 2025 21:31:55 +0900 Subject: [PATCH 2/6] =?UTF-8?q?[FEAT/#235]=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/navigation/route/MissionRoute.kt | 10 +- .../java/com/yapp/mission/MissionContract.kt | 2 + .../java/com/yapp/mission/MissionNavGraph.kt | 9 +- .../java/com/yapp/mission/MissionScreen.kt | 183 +++++++++++------- .../java/com/yapp/mission/MissionViewModel.kt | 33 +++- 5 files changed, 162 insertions(+), 75 deletions(-) diff --git a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt index d5a04949..e087c4e0 100644 --- a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt +++ b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt @@ -3,4 +3,12 @@ package com.yapp.common.navigation.route import kotlinx.serialization.Serializable @Serializable -data object MissionRoute +data class MissionRoute( + val missionType: String, + val missionCount: String, + val missionMode: String = "REAL", // PREVIEW 지원 +) { + companion object { + const val route = "mission" + } +} diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt index b21e6909..1e6dd5a2 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt @@ -5,6 +5,7 @@ import com.yapp.domain.model.MissionType sealed class MissionContract { data class State( + val missionMode: MissionMode = MissionMode.REAL, val missionType: MissionType = MissionType.TAP, val isMissionTypeLoading: Boolean = true, val missionCount: Int = 10, @@ -20,6 +21,7 @@ sealed class MissionContract { ) : com.yapp.ui.base.UiState sealed class Action { + data object NavigateBack : Action() data object ShakeCard : Action() data object ClickCard : Action() data object ShowExitDialog : Action() diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt b/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt index 1e11f8f6..247a9e5c 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionNavGraph.kt @@ -25,7 +25,10 @@ fun NavGraphBuilder.missionScreen( handleSideEffect(it, navigator) } - MissionRoute(viewModel) + MissionRoute( + navigator = navigator, + viewModel = viewModel, + ) } } @@ -37,7 +40,7 @@ private fun handleSideEffect( MissionContract.SideEffect.NavigateToFortune -> { navigator.navigateToFortune( navOptions = navOptions { - popUpTo(MissionRoute) { + popUpTo(MissionRoute.route) { inclusive = true } }, @@ -47,7 +50,7 @@ private fun handleSideEffect( MissionContract.SideEffect.NavigateToHome -> { navigator.navigateToHome( navOptions = navOptions { - popUpTo(MissionRoute) { + popUpTo(MissionRoute.route) { inclusive = true } }, diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt index 4f8d85ef..11a1ab1a 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt @@ -10,14 +10,21 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -39,6 +46,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper import com.yapp.analytics.LocalAnalyticsHelper +import com.yapp.common.navigation.OrbitNavigator import com.yapp.designsystem.theme.OrbitTheme import com.yapp.domain.model.MissionType import com.yapp.mission.component.FlipCard @@ -50,7 +58,10 @@ import com.yapp.ui.utils.heightForScreenPercentage import com.yapp.ui.utils.paddingForScreenPercentage @Composable -fun MissionRoute(viewModel: MissionViewModel = hiltViewModel()) { +fun MissionRoute( + viewModel: MissionViewModel = hiltViewModel(), + navigator: OrbitNavigator, +) { val state by viewModel.container.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current @@ -60,6 +71,21 @@ fun MissionRoute(viewModel: MissionViewModel = hiltViewModel()) { } } + BackHandler { + if (state.missionMode == MissionMode.PREVIEW) { + navigator.navigateBack() + return@BackHandler + } + + viewModel.processAction( + if (state.showExitDialog) { + MissionContract.Action.HideExitDialog + } else { + MissionContract.Action.ShowExitDialog + }, + ) + } + LaunchedEffect(Unit) { shakeDetector.start() } @@ -77,10 +103,6 @@ fun MissionRoute(viewModel: MissionViewModel = hiltViewModel()) { ) } -/** - * Mission 상태에 따라 적절한 화면을 구성하는 메인 컨테이너. - * 로딩, 콘텐츠, 성공 오버레이, 다이얼로그 등 분기 처리 포함. - */ @Composable fun MissionScreen( stateProvider: () -> MissionContract.State, @@ -90,17 +112,9 @@ fun MissionScreen( val state = stateProvider() val analytics = LocalAnalyticsHelper.current - BackHandler { - eventDispatcher( - if (state.showExitDialog) { - MissionContract.Action.HideExitDialog - } else { - MissionContract.Action.ShowExitDialog - }, - ) - } - - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier.fillMaxSize(), + ) { if (state.isMissionTypeLoading || state.missionType == MissionType.NONE) { MissionLoadingScreen() return @@ -113,7 +127,10 @@ fun MissionScreen( modifier = Modifier.matchParentSize(), ) - MissionContent(state, eventDispatcher) + MissionContent( + state = state, + eventDispatcher = eventDispatcher, + ) if (state.showExitDialog) { ExitDialog(state, eventDispatcher, onFinish, analytics) @@ -128,12 +145,36 @@ fun MissionScreen( eventDispatcher(MissionContract.Action.RetryPostFortune) } } + + if (state.missionMode == MissionMode.PREVIEW) { + val insets = WindowInsets.navigationBars.asPaddingValues() + + Button( + onClick = { + eventDispatcher(MissionContract.Action.NavigateBack) + }, + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = OrbitTheme.colors.white, + contentColor = OrbitTheme.colors.gray_900, + ), + contentPadding = PaddingValues( + horizontal = 24.dp, + vertical = 12.dp, + ), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = insets.calculateBottomPadding() + 28.dp), + ) { + Text( + text = "미리보기 종료", + style = OrbitTheme.typography.body1SemiBold, + ) + } + } } } -/** - * 미션 콘텐츠 본문. TopBar, 진행 바, 상태별 게임 포함. - */ @Composable fun MissionContent( state: MissionContract.State, @@ -143,7 +184,10 @@ fun MissionContent( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - MissionTopAppBar(onExit = { eventDispatcher(MissionContract.Action.ShowExitDialog) }) + MissionTopAppBar( + mode = state.missionMode, + onExit = { eventDispatcher(MissionContract.Action.ShowExitDialog) }, + ) MissionProgressBarSection(state) MissionLabel(state) Spacer(modifier = Modifier.heightForScreenPercentage(0.0665f)) @@ -168,11 +212,11 @@ fun MissionContent( } } -/** - * '나가기' 버튼이 포함된 미션 상단 앱바 영역. - */ @Composable -fun MissionTopAppBar(onExit: () -> Unit) { +fun MissionTopAppBar( + mode: MissionMode, + onExit: () -> Unit, +) { Spacer(modifier = Modifier.heightForScreenPercentage(0.066f)) Box( modifier = Modifier @@ -181,34 +225,35 @@ fun MissionTopAppBar(onExit: () -> Unit) { contentAlignment = Alignment.TopEnd, ) { Row( - modifier = Modifier.customClickable( - rippleEnabled = false, - fadeOnPress = true, - pressedAlpha = 0.5f, - onClick = onExit, - ), + modifier = Modifier + .height(26.dp) + .customClickable( + rippleEnabled = false, + fadeOnPress = true, + pressedAlpha = 0.5f, + onClick = onExit, + ), ) { - Icon( - painter = painterResource(id = core.designsystem.R.drawable.ic_cancel), - contentDescription = null, - tint = OrbitTheme.colors.white, - modifier = Modifier.size(24.dp), - ) - Text( - text = "나가기", - color = OrbitTheme.colors.white, - style = OrbitTheme.typography.body1SemiBold, - modifier = Modifier - .padding(start = 4.dp) - .align(Alignment.CenterVertically), - ) + if (mode == MissionMode.REAL) { + Icon( + painter = painterResource(id = core.designsystem.R.drawable.ic_cancel), + contentDescription = null, + tint = OrbitTheme.colors.white, + modifier = Modifier.size(24.dp), + ) + Text( + text = "나가기", + color = OrbitTheme.colors.white, + style = OrbitTheme.typography.body1SemiBold, + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically), + ) + } } } } -/** - * 미션 진행도 ProgressBar 섹션. - */ @Composable fun MissionProgressBarSection(state: MissionContract.State) { Spacer(modifier = Modifier.heightForScreenPercentage(0.0246f)) @@ -223,9 +268,6 @@ fun MissionProgressBarSection(state: MissionContract.State) { Spacer(modifier = Modifier.heightForScreenPercentage(0.06f)) } -/** - * 미션 안내 문구 및 현재 카운트. - */ @Composable fun MissionLabel(state: MissionContract.State) { val instruction = @@ -245,9 +287,6 @@ fun MissionLabel(state: MissionContract.State) { ) } -/** - * 흔들기 미션 초기 이미지. - */ @Composable fun MissionShakeInitialImage() { Image( @@ -259,9 +298,6 @@ fun MissionShakeInitialImage() { ) } -/** - * 클릭 미션 카드. 클릭 시 애니메이션 및 상태 변화. - */ @Composable fun MissionClickCard( state: MissionContract.State, @@ -297,9 +333,6 @@ fun MissionClickCard( } } -/** - * 미션 종료 시 나가기 다이얼로그. - */ @Composable fun ExitDialog( state: MissionContract.State, @@ -331,9 +364,6 @@ fun ExitDialog( ) } -/** - * 미션 성공 시 오버레이 화면. - */ @Composable fun MissionSuccessOverlay() { Box( @@ -368,9 +398,6 @@ fun MissionSuccessOverlay() { } } -/** - * 오류 발생 시 다이얼로그. - */ @Composable fun ErrorDialog(message: String, onConfirm: () -> Unit) { OrbitDialog( @@ -381,9 +408,6 @@ fun ErrorDialog(message: String, onConfirm: () -> Unit) { ) } -/** - * 로딩 화면. 미션 타입 로딩 중에 표시. - */ @Composable fun MissionLoadingScreen() { Box( @@ -397,12 +421,33 @@ fun MissionLoadingScreen() { } } +@Composable +@Preview +private fun MissionRouteReal() { + MissionScreen( + stateProvider = { + MissionContract.State( + isMissionTypeLoading = false, + missionType = MissionType.TAP, + currentCount = 0, + showFinalAnimation = false, + playWhenClick = false, + showExitDialog = false, + isMissionCompleted = false, + ) + }, + eventDispatcher = {}, + onFinish = {}, + ) +} + @Composable @Preview private fun MissionRoutePreview() { MissionScreen( stateProvider = { MissionContract.State( + missionMode = MissionMode.PREVIEW, isMissionTypeLoading = false, missionType = MissionType.TAP, currentCount = 0, diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index 9fc9c340..2ee7fa09 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -24,6 +24,22 @@ import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject +enum class MissionMode { + REAL, + PREVIEW, + ; + + companion object { + fun fromRaw(raw: String?): MissionMode { + return try { + valueOf(raw ?: "REAL") + } catch (_: IllegalArgumentException) { + REAL + } + } + } +} + @HiltViewModel class MissionViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, @@ -43,11 +59,13 @@ class MissionViewModel @Inject constructor( loadMissionInfo( missionTypeRaw = savedStateHandle.get("missionType"), missionCountRaw = savedStateHandle.get("missionCount"), + missionModeRaw = savedStateHandle.get("missionMode"), ) } fun processAction(action: MissionContract.Action) { when (action) { + is MissionContract.Action.NavigateBack -> navigateBack() is MissionContract.Action.ShakeCard -> handleMissionProgress(MissionType.SHAKE) is MissionContract.Action.ClickCard -> handleMissionProgress(MissionType.TAP) is MissionContract.Action.ShowExitDialog -> showExitDialog() @@ -59,12 +77,15 @@ class MissionViewModel @Inject constructor( private fun loadMissionInfo( missionTypeRaw: String?, missionCountRaw: String?, + missionModeRaw: String?, ) = intent { val missionType = missionTypeRaw?.toIntOrNull() ?: MissionType.TAP.value val missionCount = missionCountRaw?.toIntOrNull() ?: 10 + val missionMode = MissionMode.fromRaw(missionModeRaw) reduce { state.copy( + missionMode = missionMode, missionType = MissionType.fromInt(missionType), missionCount = missionCount, isMissionTypeLoading = false, @@ -72,6 +93,10 @@ class MissionViewModel @Inject constructor( } } + private fun navigateBack() = intent { + postSideEffect(MissionContract.SideEffect.NavigateBack) + } + private fun showExitDialog() = intent { reduce { state.copy(showExitDialog = true) } } @@ -137,10 +162,14 @@ class MissionViewModel @Inject constructor( postFortune(isRetry = true) } - private fun completeMission(type: String) { + private fun completeMission(type: String) = intent { performHapticSuccess() logMissionSuccess(type) - postFortune() + if (state.missionMode == MissionMode.REAL) { + postFortune() + } else { + postSideEffect(MissionContract.SideEffect.NavigateBack) + } } private fun performHapticSuccess() { From 2d4d067726391f94c950e509b4805c0dab98185d Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 23 Jul 2025 21:32:50 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[FEAT/#235]=20=EC=95=8C=EB=9E=8C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=99=94=EB=A9=B4=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yapp/common/navigation/OrbitNavigator.kt | 16 ++++++++++++++++ .../home/alarm/addedit/AlarmAddEditContract.kt | 6 ++++++ .../home/alarm/addedit/AlarmAddEditScreen.kt | 14 +++++++++++++- .../home/alarm/addedit/AlarmAddEditViewModel.kt | 8 ++++++++ .../bottomsheet/AlarmMissionBottomSheet.kt | 12 +++++------- 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt b/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt index 34ceed6f..ec884f97 100644 --- a/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt +++ b/core/common/src/main/java/com/yapp/common/navigation/OrbitNavigator.kt @@ -11,6 +11,7 @@ import com.yapp.common.navigation.route.FortuneBaseRoute import com.yapp.common.navigation.route.FortuneDestination import com.yapp.common.navigation.route.HomeBaseRoute import com.yapp.common.navigation.route.HomeDestination +import com.yapp.common.navigation.route.MissionRoute import com.yapp.common.navigation.route.OnboardingBaseRoute import com.yapp.common.navigation.route.OnboardingDestination import com.yapp.common.navigation.route.SettingBaseRoute @@ -57,6 +58,21 @@ class OrbitNavigator( navController.navigate(AlarmInteractionDestination.AlarmSnoozeTimer(alarm), navOptions) } + fun navigateToMissionPreview( + missionType: Int, + missionCount: Int, + navOptions: NavOptions? = null, + ) { + navController.navigate( + MissionRoute( + missionType = "$missionType", + missionCount = "$missionCount", + missionMode = "PREVIEW", + ), + navOptions, + ) + } + fun navigateToFortune(navOptions: NavOptions? = null) { navController.navigate(FortuneBaseRoute, navOptions) } diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt index 3396114a..eb99cf01 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditContract.kt @@ -85,6 +85,7 @@ sealed class AlarmAddEditContract { data object ToggleHolidaySkipOption : Action() data object ToggleSnoozeOption : Action() data class SaveMission(val type: MissionType, val count: Int) : Action() + data class NavigateToMissionPreview(val missionType: MissionType, val missionCount: Int) : Action() data class SetSnoozeInterval(val index: Int) : Action() data class SetSnoozeRepeatCount(val index: Int) : Action() data object ToggleVibrationOption : Action() @@ -103,6 +104,11 @@ sealed class AlarmAddEditContract { sealed class SideEffect : com.yapp.ui.base.SideEffect { data object NavigateBack : SideEffect() + data class NavigateToMissionPreview( + val missionType: MissionType, + val missionCount: Int, + ) : SideEffect() + data class SaveAlarm(val id: Long) : SideEffect() data class UpdateAlarm(val id: Long) : SideEffect() diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt index 1eb21d47..8bc32ab5 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditScreen.kt @@ -104,6 +104,12 @@ private suspend fun handleSideEffect( is AlarmAddEditContract.SideEffect.NavigateBack -> { navigator.navigateBack() } + is AlarmAddEditContract.SideEffect.NavigateToMissionPreview -> { + navigator.navigateToMissionPreview( + missionType = sideEffect.missionType.value, + missionCount = sideEffect.missionCount, + ) + } is AlarmAddEditContract.SideEffect.SaveAlarm -> { navigator.navController.previousBackStackEntry ?.savedStateHandle @@ -241,7 +247,13 @@ fun AlarmAddEditContent( ), ) }, - onPreviewMission = { + onPreviewMission = { missionType, missionCount -> + eventDispatcher( + AlarmAddEditContract.Action.NavigateToMissionPreview( + missionType = missionType, + missionCount = missionCount, + ), + ) }, ) diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt index 5e51ba96..1402eaa9 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt @@ -181,6 +181,7 @@ class AlarmAddEditViewModel @Inject constructor( is AlarmAddEditContract.Action.ToggleSpecificDaySelection -> toggleSpecificDaySelection(action.day) is AlarmAddEditContract.Action.ToggleHolidaySkipOption -> toggleHolidaySkipOption() is AlarmAddEditContract.Action.SaveMission -> saveMission(action.type, action.count) + is AlarmAddEditContract.Action.NavigateToMissionPreview -> navigateToMissionPreview(action.missionType, action.missionCount) is AlarmAddEditContract.Action.ToggleSnoozeOption -> toggleSnoozeOption() is AlarmAddEditContract.Action.SetSnoozeInterval -> setSnoozeInterval(action.index) is AlarmAddEditContract.Action.SetSnoozeRepeatCount -> setSnoozeRepeatCount(action.index) @@ -211,6 +212,13 @@ class AlarmAddEditViewModel @Inject constructor( postSideEffect(AlarmAddEditContract.SideEffect.NavigateBack) } + private fun navigateToMissionPreview( + missionType: MissionType, + missionCount: Int, + ) = intent { + postSideEffect(AlarmAddEditContract.SideEffect.NavigateToMissionPreview(missionType, missionCount)) + } + private fun saveAlarm() = intent { val newAlarm = state.toAlarm() diff --git a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt index 5ea69a22..9cb8669e 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/component/bottomsheet/AlarmMissionBottomSheet.kt @@ -68,7 +68,7 @@ internal fun AlarmMissionBottomSheet( isSheetOpen: Boolean, onDismiss: () -> Unit, onSaveMission: (MissionType, Int) -> Unit, - onPreviewMission: (MissionType) -> Unit, + onPreviewMission: (MissionType, Int) -> Unit, ) { var currentStep by remember { mutableStateOf(AlarmMissionSelectBottomSheetType.MISSION_SETTING) } @@ -147,7 +147,7 @@ internal fun AlarmMissionBottomSheet( onDismiss() }, onPreview = { - onPreviewMission(selectedMissionType) + onPreviewMission(selectedMissionType, selectedMissionCount) }, ) } @@ -475,7 +475,7 @@ private fun MissionDetailContent( onBack: () -> Unit, onClose: () -> Unit, onSave: () -> Unit, - onPreview: (MissionType) -> Unit, + onPreview: () -> Unit, ) { val (title, lottieRes) = when (missionType) { MissionType.SHAKE -> @@ -569,9 +569,7 @@ private fun MissionDetailContent( ) { OrbitButton( label = stringResource(id = feature.home.R.string.mission_detail_content_btn_preview), - onClick = { - onPreview(missionType) - }, + onClick = onPreview, useFillMaxWidth = false, enabled = true, containerColor = OrbitTheme.colors.gray_600, @@ -657,7 +655,7 @@ private fun AlarmMissionSelectBottomSheetPreview() { isSheetOpen = true, onDismiss = {}, onSaveMission = { _, _ -> }, - onPreviewMission = {}, + onPreviewMission = { _, _ -> }, ) } } From 76eb33fdf2f55679c70e9a2f2d80608c413598e4 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Wed, 23 Jul 2025 21:52:36 +0900 Subject: [PATCH 4/6] =?UTF-8?q?[REFACTOR/#235]=20=EB=AF=B8=EC=85=98=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/yapp/mission/MissionScreen.kt | 34 +++++++++++-------- .../com/yapp/mission/component/FlipCard.kt | 2 -- .../mission/src/main/res/values/strings.xml | 14 ++++++++ 3 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 feature/mission/src/main/res/values/strings.xml diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt index 11a1ab1a..5a7dbbf9 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -56,6 +57,7 @@ import com.yapp.ui.component.lottie.LottieAnimation import com.yapp.ui.extensions.customClickable import com.yapp.ui.utils.heightForScreenPercentage import com.yapp.ui.utils.paddingForScreenPercentage +import feature.mission.R @Composable fun MissionRoute( @@ -167,7 +169,7 @@ fun MissionScreen( .padding(bottom = insets.calculateBottomPadding() + 28.dp), ) { Text( - text = "미리보기 종료", + text = stringResource(id = R.string.mission_preview_exit), style = OrbitTheme.typography.body1SemiBold, ) } @@ -197,7 +199,7 @@ fun MissionContent( if (state.currentCount == 0) { MissionShakeInitialImage() } else { - FlipCard(state = state, eventDispatcher = eventDispatcher) + FlipCard(state) } } @@ -242,7 +244,7 @@ fun MissionTopAppBar( modifier = Modifier.size(24.dp), ) Text( - text = "나가기", + text = stringResource(id = R.string.exit), color = OrbitTheme.colors.white, style = OrbitTheme.typography.body1SemiBold, modifier = Modifier @@ -270,9 +272,13 @@ fun MissionProgressBarSection(state: MissionContract.State) { @Composable fun MissionLabel(state: MissionContract.State) { - val instruction = - if (state.missionType == MissionType.SHAKE) "${state.missionCount}회를 흔들어 부적을 뒤집어줘" else "${state.missionCount}회를 눌러 편지를 열어줘" - val count = state.currentCount + val instruction = stringResource( + id = when (state.missionType) { + MissionType.SHAKE -> R.string.mission_instruction_shake + else -> R.string.mission_instruction_tap + }, + state.missionCount, + ) Text( text = instruction, @@ -281,7 +287,7 @@ fun MissionLabel(state: MissionContract.State) { ) Spacer(modifier = Modifier.heightForScreenPercentage(0.005f)) Text( - text = count.toString(), + text = state.currentCount.toString(), color = OrbitTheme.colors.white, style = OrbitTheme.typography.displaySemiBold, ) @@ -341,10 +347,10 @@ fun ExitDialog( analytics: AnalyticsHelper, ) { OrbitDialog( - title = "나가면 운세를 받을 수 없어요", - message = "미션을 수행하지 않고 나가시겠어요?", - confirmText = "나가기", - cancelText = "취소", + title = stringResource(id = R.string.mission_exit_dialog_title), + message = stringResource(id = R.string.mission_exit_dialog_message), + confirmText = stringResource(id = R.string.exit), + cancelText = stringResource(id = R.string.cancel), onConfirm = { analytics.logEvent( AnalyticsEvent( @@ -387,7 +393,7 @@ fun MissionSuccessOverlay() { play = true, ) Text( - text = "미션 성공!", + text = stringResource(id = R.string.mission_success), color = OrbitTheme.colors.white, style = OrbitTheme.typography.title1Bold, modifier = Modifier @@ -401,9 +407,9 @@ fun MissionSuccessOverlay() { @Composable fun ErrorDialog(message: String, onConfirm: () -> Unit) { OrbitDialog( - title = "오류", + title = stringResource(id = R.string.error), message = message, - confirmText = "확인", + confirmText = stringResource(id = R.string.confirm), onConfirm = onConfirm, ) } diff --git a/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt b/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt index 980cf295..14e41a39 100644 --- a/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt +++ b/feature/mission/src/main/java/com/yapp/mission/component/FlipCard.kt @@ -25,7 +25,6 @@ import com.yapp.mission.MissionContract @Composable fun FlipCard( state: MissionContract.State, - eventDispatcher: (MissionContract.Action) -> Unit, ) { val rotationZ = remember { Animatable(0f) } val rotationY = remember { Animatable(state.rotationY) } @@ -109,7 +108,6 @@ fun FlipCardPreview() { ) { FlipCard( state = state.copy(rotationY = rotationY, rotationZ = rotationZ), - eventDispatcher = {}, ) } } diff --git a/feature/mission/src/main/res/values/strings.xml b/feature/mission/src/main/res/values/strings.xml new file mode 100644 index 00000000..3fd93606 --- /dev/null +++ b/feature/mission/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + 나가기 + 취소 + 확인 + 오류 + + 미리보기 종료 + 나가면 운세를 받을 수 없어요 + 미션을 수행하지 않고 나가시겠어요? + 미션 성공! + %1$d회를 흔들어 부적을 뒤집어줘 + %1$d회를 눌러 편지를 열어줘 + From 9373848d069fcb917beb7f09b0f51b8efbf3a3ed Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Sun, 27 Jul 2025 11:25:09 +0900 Subject: [PATCH 5/6] =?UTF-8?q?[REFACTOR/#235]=20MissionMode=20enum=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20domain=20=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yapp/alarm/services/AlarmService.kt | 12 +++++++++--- .../common/navigation/route/MissionRoute.kt | 3 ++- .../main/java/com/yapp/domain/MissionMode.kt | 13 +++++++++++++ .../java/com/yapp/mission/MissionContract.kt | 1 + .../java/com/yapp/mission/MissionScreen.kt | 1 + .../java/com/yapp/mission/MissionViewModel.kt | 19 ++----------------- 6 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 domain/src/main/java/com/yapp/domain/MissionMode.kt diff --git a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt index 660d7c53..70cc193d 100644 --- a/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt +++ b/core/alarm/src/main/java/com/yapp/alarm/services/AlarmService.kt @@ -20,7 +20,6 @@ import androidx.core.net.toUri import com.yapp.alarm.AlarmConstants import com.yapp.alarm.AndroidAlarmScheduler import com.yapp.alarm.pendingIntent.interaction.createAlarmAlertPendingIntent -import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissPendingIntent import com.yapp.alarm.pendingIntent.interaction.createAlarmSnoozePendingIntent import com.yapp.alarm.pendingIntent.interaction.createNavigateToMissionPendingIntent import com.yapp.domain.model.Alarm @@ -135,7 +134,7 @@ class AlarmService : Service() { val alarmAlertPendingIntent = createAlarmAlertPendingIntent(applicationContext, alarm) - val alarmDismissPendingIntent = if (shouldNavigateToMission) { + /*val alarmDismissPendingIntent = if (shouldNavigateToMission) { createNavigateToMissionPendingIntent( applicationContext = applicationContext, notificationId = alarm.id, @@ -147,7 +146,14 @@ class AlarmService : Service() { applicationContext = applicationContext, pendingIntentId = alarm.id, ) - } + }*/ + + val alarmDismissPendingIntent = createNavigateToMissionPendingIntent( + applicationContext = applicationContext, + notificationId = alarm.id, + missionType = alarm.missionType.value, + missionCount = alarm.missionCount, + ) val snoozePendingIntent = if (alarm.isSnoozeEnabled && alarm.snoozeCount != 0) { createAlarmSnoozePendingIntent(applicationContext, alarm) diff --git a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt index e087c4e0..2ffd29d6 100644 --- a/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt +++ b/core/common/src/main/java/com/yapp/common/navigation/route/MissionRoute.kt @@ -1,12 +1,13 @@ package com.yapp.common.navigation.route +import com.yapp.domain.MissionMode import kotlinx.serialization.Serializable @Serializable data class MissionRoute( val missionType: String, val missionCount: String, - val missionMode: String = "REAL", // PREVIEW 지원 + val missionMode: String = MissionMode.REAL.name, ) { companion object { const val route = "mission" diff --git a/domain/src/main/java/com/yapp/domain/MissionMode.kt b/domain/src/main/java/com/yapp/domain/MissionMode.kt new file mode 100644 index 00000000..b047c7c8 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/MissionMode.kt @@ -0,0 +1,13 @@ +package com.yapp.domain + +enum class MissionMode { + REAL, + PREVIEW, + ; + + companion object { + fun fromRaw(raw: String?): MissionMode { + return raw?.let { entries.find { it.name == raw } } ?: REAL + } + } +} diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt index 1e6dd5a2..9aa81001 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionContract.kt @@ -1,5 +1,6 @@ package com.yapp.mission +import com.yapp.domain.MissionMode import com.yapp.domain.model.MissionType sealed class MissionContract { diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt index 5a7dbbf9..ad178833 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionScreen.kt @@ -49,6 +49,7 @@ import com.yapp.analytics.AnalyticsHelper import com.yapp.analytics.LocalAnalyticsHelper import com.yapp.common.navigation.OrbitNavigator import com.yapp.designsystem.theme.OrbitTheme +import com.yapp.domain.MissionMode import com.yapp.domain.model.MissionType import com.yapp.mission.component.FlipCard import com.yapp.mission.component.MissionProgressBar diff --git a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt index 2ee7fa09..33650e31 100644 --- a/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt +++ b/feature/mission/src/main/java/com/yapp/mission/MissionViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel import com.yapp.alarm.pendingIntent.interaction.createAlarmDismissIntent import com.yapp.analytics.AnalyticsEvent import com.yapp.analytics.AnalyticsHelper +import com.yapp.domain.MissionMode import com.yapp.domain.model.MissionType import com.yapp.domain.repository.FortuneRepository import com.yapp.domain.repository.UserInfoRepository @@ -24,22 +25,6 @@ import org.orbitmvi.orbit.syntax.simple.reduce import org.orbitmvi.orbit.viewmodel.container import javax.inject.Inject -enum class MissionMode { - REAL, - PREVIEW, - ; - - companion object { - fun fromRaw(raw: String?): MissionMode { - return try { - valueOf(raw ?: "REAL") - } catch (_: IllegalArgumentException) { - REAL - } - } - } -} - @HiltViewModel class MissionViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, @@ -47,7 +32,7 @@ class MissionViewModel @Inject constructor( private val fortuneRepository: FortuneRepository, private val userInfoRepository: UserInfoRepository, private val app: Application, - val savedStateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, ) : ViewModel(), ContainerHost { override val container: Container = container( From cc8cf3cecbff0f101a89be3b7c34268d62d28d92 Mon Sep 17 00:00:00 2001 From: dongchyeon Date: Sun, 27 Jul 2025 13:02:21 +0900 Subject: [PATCH 6/6] =?UTF-8?q?[FIX/#235]=20=EB=AF=B8=EC=85=98=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=EC=9D=B4=EB=8F=99=20=EC=8B=9C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt index 1402eaa9..fc68790e 100644 --- a/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt +++ b/feature/home/src/main/java/com/yapp/home/alarm/addedit/AlarmAddEditViewModel.kt @@ -216,6 +216,14 @@ class AlarmAddEditViewModel @Inject constructor( missionType: MissionType, missionCount: Int, ) = intent { + val newTimeState = state.timeState.copy( + initialTime = state.timeState.currentTime, + ) + reduce { + state.copy( + timeState = newTimeState, + ) + } postSideEffect(AlarmAddEditContract.SideEffect.NavigateToMissionPreview(missionType, missionCount)) }