diff --git a/core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt index ab898f48..482eda29 100644 --- a/core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt +++ b/core/design-system/src/main/java/com/twix/designsystem/components/photolog/BackgroundCard.kt @@ -27,11 +27,11 @@ import com.twix.ui.extension.noRippleClickable @Composable fun BackgroundCard( - isCertificated: Boolean, uploadedAt: String, - buttonTitle: String, - onClick: () -> Unit, + actionLabel: String, rotation: Float, + onClickAction: () -> Unit, + showActionButton: Boolean, ) { Column { PhotologCard( @@ -39,7 +39,7 @@ fun BackgroundCard( borderColor = GrayColor.C500, rotation = rotation, ) - if (isCertificated) { + if (!showActionButton) { AppText( text = uploadedAt, style = AppTextStyle.B4, @@ -60,19 +60,19 @@ fun BackgroundCard( Modifier .width(150.dp) .height(74.dp) - .noRippleClickable { onClick() }, - text = buttonTitle, + .noRippleClickable { onClickAction() }, + text = actionLabel, textColor = GrayColor.C500, backgroundColor = CommonColor.White, ) } Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_keepi_sting), + imageVector = ImageVector.vectorResource(R.drawable.ic_photolog_action_poke), contentDescription = null, modifier = Modifier - .padding(end = 24.dp, top = 15.dp) + .padding(end = 16.dp, top = 15.dp) .align(Alignment.TopEnd), ) } @@ -85,10 +85,10 @@ fun BackgroundCard( fun PreviewBackgroundCard() { TwixTheme { BackgroundCard( - buttonTitle = stringResource(R.string.word_sting), + actionLabel = stringResource(R.string.word_sting), uploadedAt = "2023.10.31 23:59", - onClick = {}, - isCertificated = true, + onClickAction = {}, + showActionButton = true, rotation = -8f, ) } diff --git a/core/design-system/src/main/res/drawable/ic_doubt.xml b/core/design-system/src/main/res/drawable/ic_doubt.xml index bdf5ab45..ddbb2b7a 100644 --- a/core/design-system/src/main/res/drawable/ic_doubt.xml +++ b/core/design-system/src/main/res/drawable/ic_doubt.xml @@ -1,85 +1,67 @@ + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + - + - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_fuck.xml b/core/design-system/src/main/res/drawable/ic_fuck.xml index 97799f48..995c2f67 100644 --- a/core/design-system/src/main/res/drawable/ic_fuck.xml +++ b/core/design-system/src/main/res/drawable/ic_fuck.xml @@ -1,135 +1,135 @@ + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + android:pathData="M3.043,3.047h46v46h-46z"/> diff --git a/core/design-system/src/main/res/drawable/ic_happy.xml b/core/design-system/src/main/res/drawable/ic_happy.xml index a15aeaf2..a0ae82a2 100644 --- a/core/design-system/src/main/res/drawable/ic_happy.xml +++ b/core/design-system/src/main/res/drawable/ic_happy.xml @@ -1,87 +1,102 @@ + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + android:pathData="M1.93,4.719h49v44h-49z"/> + android:pathData="M16.897,35.942C18.989,34.838 19.789,32.248 18.685,30.157C17.582,28.065 14.991,27.265 12.9,28.368C10.809,29.472 10.008,32.063 11.112,34.154C12.216,36.245 14.806,37.046 16.897,35.942Z" + android:fillColor="#FDE0DF"/> + android:pathData="M38.389,28.387C40.481,27.284 41.281,24.693 40.178,22.602C39.074,20.511 36.484,19.71 34.392,20.814C32.301,21.918 31.5,24.508 32.604,26.599C33.708,28.691 36.298,29.491 38.389,28.387Z" + android:fillColor="#FDE0DF"/> - + android:pathData="M39.641,16.583C39.641,16.583 40.623,12.381 44.924,13.397C49.225,14.412 50.034,22.535 43.265,23.493" + android:fillColor="#ffffff"/> + android:pathData="M19.366,23.199C19.117,23.046 18.916,22.884 18.723,22.779C18.465,22.638 18.248,22.546 18.052,22.45C17.826,22.34 17.611,22.238 17.379,22.131C17.183,22.035 16.951,21.973 16.705,21.826C16.512,21.721 16.309,21.568 16.063,21.406" + android:strokeLineJoin="round" + android:strokeWidth="1.38" + android:fillColor="#00000000" + android:strokeColor="#181818" + android:strokeLineCap="round"/> + + diff --git a/core/design-system/src/main/res/drawable/ic_keepi_angry.xml b/core/design-system/src/main/res/drawable/ic_keepi_angry.xml deleted file mode 100644 index 1a396e8c..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_angry.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_keepi_doubt.xml b/core/design-system/src/main/res/drawable/ic_keepi_doubt.xml deleted file mode 100644 index 45601312..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_doubt.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_keepi_fuck.xml b/core/design-system/src/main/res/drawable/ic_keepi_fuck.xml deleted file mode 100644 index 22d0db5f..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_fuck.xml +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_keepi_happy.xml b/core/design-system/src/main/res/drawable/ic_keepi_happy.xml deleted file mode 100644 index ce8d791d..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_happy.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_keepi_love.xml b/core/design-system/src/main/res/drawable/ic_keepi_love.xml deleted file mode 100644 index ef233362..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_love.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_keepi_sting.xml b/core/design-system/src/main/res/drawable/ic_keepi_sting.xml deleted file mode 100644 index ca28a97f..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_sting.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_keepi_trouble.xml b/core/design-system/src/main/res/drawable/ic_keepi_trouble.xml deleted file mode 100644 index 16ca9adf..00000000 --- a/core/design-system/src/main/res/drawable/ic_keepi_trouble.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/core/design-system/src/main/res/drawable/ic_love.xml b/core/design-system/src/main/res/drawable/ic_love.xml index 476ecc69..45b98ba7 100644 --- a/core/design-system/src/main/res/drawable/ic_love.xml +++ b/core/design-system/src/main/res/drawable/ic_love.xml @@ -1,84 +1,101 @@ + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + android:pathData="M4.711,2.648h42v47h-42z"/> + android:pathData="M16.956,31.1C19.032,30.004 19.827,27.433 18.731,25.357C17.636,23.282 15.065,22.487 12.989,23.583C10.913,24.678 10.119,27.249 11.214,29.325C12.31,31.401 14.881,32.195 16.956,31.1Z" + android:fillColor="#FDE0DF"/> + android:pathData="M38.277,30.084C40.353,28.988 41.147,26.418 40.051,24.342C38.956,22.266 36.385,21.472 34.309,22.567C32.234,23.663 31.439,26.233 32.535,28.309C33.63,30.385 36.201,31.18 38.277,30.084Z" + android:fillColor="#FDE0DF"/> - + + android:pathData="M22.942,28.705L22.582,29.552L23.118,31.977L24.468,30.666L26.581,30.549L28.076,30.069L30.025,30.9L29.773,28.305L27.603,28.172L25.887,27.879L24.279,28.769L22.942,28.71V28.705Z" + android:fillColor="#BC6159"/> + + + diff --git a/core/design-system/src/main/res/drawable/ic_my_reaction_union.xml b/core/design-system/src/main/res/drawable/ic_my_reaction_union.xml new file mode 100644 index 00000000..e2133fff --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_my_reaction_union.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/design-system/src/main/res/drawable/ic_photolog_action_poke.xml b/core/design-system/src/main/res/drawable/ic_photolog_action_poke.xml new file mode 100644 index 00000000..2d58e737 --- /dev/null +++ b/core/design-system/src/main/res/drawable/ic_photolog_action_poke.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/drawable/ic_trouble.xml b/core/design-system/src/main/res/drawable/ic_trouble.xml index 35894ac5..37a013a9 100644 --- a/core/design-system/src/main/res/drawable/ic_trouble.xml +++ b/core/design-system/src/main/res/drawable/ic_trouble.xml @@ -1,125 +1,144 @@ + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + - + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index f796c47b..67fdf85c 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -45,8 +45,8 @@ 끝내기 이뤘어요 탈퇴하기 - 찌르기 - 찌르기! + 찌르기 + 찌르기! 오늘 우리 목표 diff --git a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt index 008c6412..b3139b1f 100644 --- a/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt +++ b/core/navigation/src/main/java/com/twix/navigation/NavRoutes.kt @@ -35,16 +35,18 @@ sealed class NavRoutes( object TaskCertificationGraph : NavRoutes("task_certification_graph") object TaskCertificationDetailRoute : - NavRoutes("task_certification_detail/{goalId}/{date}/{betweenUs}") { + NavRoutes("task_certification_detail/{goalId}/{date}/{betweenUs}?isCompleted={isCompleted}") { const val ARG_GOAL_ID = "goalId" const val ARG_DATE = "date" const val ARG_BETWEEN_US = "betweenUs" + const val ARG_IS_COMPLETED = "isCompleted" fun createRoute( goalId: Long, date: LocalDate, betweenUs: String, - ) = "task_certification_detail/$goalId/$date/$betweenUs" + isCompleted: Boolean = false, + ) = "task_certification_detail/$goalId/$date/$betweenUs?isCompleted=$isCompleted" } object TaskCertificationRoute : NavRoutes("task_certification/{data}") { diff --git a/feature/main/src/main/java/com/twix/home/component/GoalVerifications.kt b/feature/main/src/main/java/com/twix/home/component/GoalVerifications.kt index 8ff1f6d9..e1b25212 100644 --- a/feature/main/src/main/java/com/twix/home/component/GoalVerifications.kt +++ b/feature/main/src/main/java/com/twix/home/component/GoalVerifications.kt @@ -91,7 +91,7 @@ private fun EmptyContent( ) AppRoundButton( - text = stringResource(R.string.action_sting_emphasized), + text = stringResource(R.string.action_poke_emphasized), textColor = GrayColor.C500, textStyle = AppTextStyle.C2, backgroundColor = CommonColor.White, diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt index 8c1e4a39..35dd2e97 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt @@ -61,7 +61,7 @@ import java.time.LocalDate fun StatsDetailRoute( onBack: () -> Unit, navigateToGoalEditor: (Long) -> Unit, - navigateToTaskCertificationDetail: (Long, LocalDate, BetweenUs) -> Unit, + navigateToTaskCertificationDetail: (Long, LocalDate, BetweenUs, Boolean) -> Unit, toastManager: ToastManager = koinInject(), viewModel: StatsDetailViewModel = koinViewModel(), ) { @@ -79,6 +79,7 @@ fun StatsDetailRoute( sideEffect.goalId, sideEffect.date, sideEffect.betweenUs, + sideEffect.isCompleted, ) is StatsDetailSideEffect.ShowToast -> { toastManager.tryShow( diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt index b047036f..9e578d2e 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt @@ -211,6 +211,7 @@ class StatsDetailViewModel( goalId = argGoalId, date = selectedDate, betweenUs = determineDisplayBetweenUs(completedDate.date), + isCompleted = currentState.detail.isCompleted, ), ) } diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt index a8777465..d7c2d393 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/contract/StatsDetailSideEffect.kt @@ -21,5 +21,6 @@ sealed interface StatsDetailSideEffect : SideEffect { val goalId: Long, val date: LocalDate, val betweenUs: BetweenUs, + val isCompleted: Boolean, ) : StatsDetailSideEffect } diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt index b2052d2c..ef12f777 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/navigation/StatsDetailGraph.kt @@ -36,12 +36,13 @@ object StatsDetailGraph : NavGraphContributor { launchSingleTop = true } }, - navigateToTaskCertificationDetail = { goalId, date, betweenUs -> + navigateToTaskCertificationDetail = { goalId, date, betweenUs, isCompleted -> val destination = NavRoutes.TaskCertificationDetailRoute.createRoute( goalId = goalId, date = date, betweenUs = betweenUs.name, + isCompleted = isCompleted, ) navController.navigate(destination) { launchSingleTop = true diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt index 21a8e50b..07770dc7 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetail.kt @@ -4,6 +4,7 @@ import android.Manifest import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -14,6 +15,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -30,6 +32,8 @@ import com.twix.domain.model.enums.GoalReactionType import com.twix.task_certification.detail.component.TaskCertificationCardContent import com.twix.task_certification.detail.component.TaskCertificationDetailTopBar import com.twix.task_certification.detail.component.reaction.ReactionContent +import com.twix.task_certification.detail.component.reaction.ReactionEffect +import com.twix.task_certification.detail.component.reaction.ReactionEffectSpec import com.twix.task_certification.detail.contract.TaskCertificationDetailIntent import com.twix.task_certification.detail.contract.TaskCertificationDetailSideEffect import com.twix.task_certification.detail.contract.TaskCertificationDetailUiState @@ -62,6 +66,12 @@ fun TaskCertificationDetailRoute( ToastData(currentContext.getString(sideEffect.message), sideEffect.type), ) } + + is TaskCertificationDetailSideEffect.ShowPokeToast -> { + toastManager.tryShow( + ToastData(sideEffect.message, ToastType.SUCCESS), + ) + } } } @@ -96,26 +106,49 @@ fun TaskCertificationDetailRoute( } } - TaskCertificationDetailScreen( - uiState = uiState, - onBack = navigateToBack, - onClickModify = { - navigateToEditor( - uiState.goalId, - uiState.selectedDate, - ) - }, - onClickReaction = { viewModel.dispatch(TaskCertificationDetailIntent.Reaction(it)) }, - onClickUpload = { - if (currentContext.hasCameraPermission()) { - navigateToCertification(uiState.goalId, uiState.selectedDate) - } else { - permissionLauncher.launch(Manifest.permission.CAMERA) + BoxWithConstraints { + val density = LocalDensity.current + val screenHeightPx = with(density) { maxHeight.toPx() } + + TaskCertificationDetailScreen( + uiState = uiState, + onBack = navigateToBack, + onClickModify = { + navigateToEditor( + uiState.goalId, + uiState.selectedDate, + ) + }, + onClickReaction = { viewModel.dispatch(TaskCertificationDetailIntent.Reaction(it)) }, + onClickUpload = { + if (currentContext.hasCameraPermission()) { + navigateToCertification(uiState.goalId, uiState.selectedDate) + } else { + permissionLauncher.launch(Manifest.permission.CAMERA) + } + }, + onPoke = { viewModel.dispatch(TaskCertificationDetailIntent.Poke) }, + onSwipe = { viewModel.dispatch(TaskCertificationDetailIntent.SwipeCard) }, + ) + if (!uiState.hasShownMyReaction && uiState.isDisplayedMyPhotolog) { + val model = uiState.myReaction + if (model != null) { + ReactionEffect( + targetReaction = model, + spec = + ReactionEffectSpec( + particleCount = 10, + durationRange = 500..800, + // 전체 화면 높이까지 퍼짐 + travelDistanceRange = 500..screenHeightPx.toInt(), + ), + onFinished = { + viewModel.dispatch(TaskCertificationDetailIntent.MyReactionEffected) + }, + ) } - }, - onClickSting = { viewModel.dispatch(TaskCertificationDetailIntent.Sting) }, - onSwipe = { viewModel.dispatch(TaskCertificationDetailIntent.SwipeCard) }, - ) + } + } } @Composable @@ -125,7 +158,7 @@ fun TaskCertificationDetailScreen( onClickModify: () -> Unit, onClickReaction: (GoalReactionType) -> Unit, onClickUpload: () -> Unit, - onClickSting: () -> Unit, + onPoke: () -> Unit, onSwipe: () -> Unit, ) { Column( @@ -146,7 +179,7 @@ fun TaskCertificationDetailScreen( uiState = uiState, onSwipe = onSwipe, onClickUpload = onClickUpload, - onClickSting = onClickSting, + onPoke = onPoke, ) if (uiState.canReaction) { @@ -172,7 +205,7 @@ private fun TaskCertificationDetailScreenPreview( onClickModify = {}, onClickReaction = {}, onClickUpload = {}, - onClickSting = {}, + onPoke = {}, onSwipe = {}, ) } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt index f24edb60..d24a1e96 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/TaskCertificationDetailViewModel.kt @@ -7,6 +7,7 @@ import com.twix.designsystem.components.toast.model.ToastType import com.twix.domain.model.enums.BetweenUs import com.twix.domain.model.enums.GoalReactionType import com.twix.domain.repository.PhotoLogRepository +import com.twix.domain.repository.PokeRepository import com.twix.navigation.NavRoutes import com.twix.task_certification.detail.contract.TaskCertificationDetailIntent import com.twix.task_certification.detail.contract.TaskCertificationDetailSideEffect @@ -26,6 +27,7 @@ import java.time.LocalDate class TaskCertificationDetailViewModel( private val photologRepository: PhotoLogRepository, + private val pokeRepository: PokeRepository, private val detailRefreshBus: TaskCertificationRefreshBus, private val goalRefreshBus: GoalRefreshBus, savedStateHandle: SavedStateHandle, @@ -46,6 +48,9 @@ class TaskCertificationDetailViewModel( savedStateHandle[NavRoutes.TaskCertificationDetailRoute.ARG_BETWEEN_US] ?: error(BETWEEN_US_NOT_FOUND) + private val argIsCompleted: Boolean = + savedStateHandle[NavRoutes.TaskCertificationDetailRoute.ARG_IS_COMPLETED] ?: false + private var lastReaction: GoalReactionType? = null private val reactionFlow = @@ -63,7 +68,16 @@ class TaskCertificationDetailViewModel( private fun fetchPhotolog() { launchResult( block = { photologRepository.fetchPhotologs(argTargetDate, argGoalId) }, - onSuccess = { reduce { it.toUiState(argGoalId, argBetweenUs, argTargetDate) } }, + onSuccess = { + reduce { + it.toUiState( + argGoalId, + argBetweenUs, + argTargetDate, + argIsCompleted, + ) + } + }, onError = { showToast(R.string.task_certification_detail_fetch_photolog_fail, ToastType.ERROR) }, @@ -79,6 +93,7 @@ class TaskCertificationDetailViewModel( .debounce(DEBOUNCE_INTERVAL) .collectLatest { reaction -> reactToPhotolog(reaction) + goalRefreshBus.notifyGoalListChanged() } } } @@ -120,8 +135,9 @@ class TaskCertificationDetailViewModel( override suspend fun handleIntent(intent: TaskCertificationDetailIntent) { when (intent) { is TaskCertificationDetailIntent.Reaction -> reduceReaction(intent.type) - TaskCertificationDetailIntent.Sting -> TODO("찌르기 API 연동") + TaskCertificationDetailIntent.Poke -> pokeToPartner() TaskCertificationDetailIntent.SwipeCard -> reduceShownCard() + TaskCertificationDetailIntent.MyReactionEffected -> reduceMyReactionShown() } } @@ -131,6 +147,14 @@ class TaskCertificationDetailViewModel( reactionFlow.tryEmit(reaction) } + private fun pokeToPartner() { + launchResult( + block = { pokeRepository.pokeGoal(argGoalId) }, + onSuccess = { tryEmitSideEffect(TaskCertificationDetailSideEffect.ShowPokeToast(it.message)) }, + onError = { showToast(R.string.toast_poke_goal_failed, ToastType.ERROR) }, + ) + } + private fun reduceShownCard() { reduce { toggleBetweenUs() } } @@ -144,13 +168,15 @@ class TaskCertificationDetailViewModel( }, ) - private fun showToast( + private fun reduceMyReactionShown() { + reduce { copy(hasShownMyReaction = true) } + } + + private suspend fun showToast( message: Int, type: ToastType, ) { - viewModelScope.launch { - emitSideEffect(TaskCertificationDetailSideEffect.ShowToast(message, type)) - } + emitSideEffect(TaskCertificationDetailSideEffect.ShowToast(message, type)) } companion object { diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt index 836901c7..0f865749 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/TaskCertificationCardContent.kt @@ -1,41 +1,50 @@ package com.twix.task_certification.detail.component +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import com.twix.designsystem.R import com.twix.designsystem.components.photolog.BackgroundCard import com.twix.designsystem.components.photolog.ForegroundCard +import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.BetweenUs +import com.twix.task_certification.detail.component.reaction.ReactionUiModel import com.twix.task_certification.detail.component.swipe.SwipeableCard import com.twix.task_certification.detail.contract.TaskCertificationDetailUiState +import com.twix.task_certification.detail.preview.TaskCertificationDetailPreviewProvider @Composable internal fun TaskCertificationCardContent( uiState: TaskCertificationDetailUiState, onSwipe: () -> Unit, onClickUpload: () -> Unit, - onClickSting: () -> Unit, + onPoke: () -> Unit, ) { - Box(Modifier.fillMaxWidth()) { + Box { BackgroundCard( - isCertificated = uiState.isDisplayedGoalCertificated, uploadedAt = uiState.displayedGoalUpdateAt, - buttonTitle = + actionLabel = when (uiState.currentShow) { BetweenUs.ME -> stringResource(R.string.task_certification_take_picture) - BetweenUs.PARTNER -> stringResource(R.string.action_sting) + BetweenUs.PARTNER -> stringResource(R.string.action_poke) }, rotation = if (uiState.isDisplayedMyPhotolog) -8f else 0f, - onClick = if (uiState.isDisplayedMyPhotolog) onClickUpload else onClickSting, + onClickAction = if (uiState.isDisplayedMyPhotolog) onClickUpload else onPoke, + showActionButton = uiState.showActionButton, ) SwipeableCard( onSwipe = onSwipe, isDisplayingMyPhoto = uiState.isDisplayedMyPhotolog, - modifier = Modifier.fillMaxWidth(), ) { ForegroundCard( isCertificated = uiState.isDisplayedGoalCertificated, @@ -46,5 +55,57 @@ internal fun TaskCertificationCardContent( rotation = if (uiState.isDisplayedMyPhotolog) 0f else -8f, ) } + + MyReactionBadge( + visible = uiState.showMyPhotologReactionBadge, + reaction = uiState.myReaction, + modifier = + Modifier + .align(Alignment.TopEnd) + .offset(x = (-8).dp, y = (-13).dp), + ) + } +} + +@Composable +private fun MyReactionBadge( + visible: Boolean, + reaction: ReactionUiModel?, + modifier: Modifier = Modifier, +) { + if (!visible) return + + reaction?.let { + Box(modifier = modifier) { + Image( + painter = painterResource(R.drawable.ic_my_reaction_union), + contentDescription = null, + ) + + Image( + painter = painterResource(it.imageResources), + contentDescription = null, + modifier = + Modifier + .padding(bottom = 10.dp) + .align(Alignment.Center), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun TaskCertificationCardContentPreview( + @PreviewParameter(TaskCertificationDetailPreviewProvider::class) + uiState: TaskCertificationDetailUiState, +) { + TwixTheme { + TaskCertificationCardContent( + uiState = uiState.copy(isLoading = true), + onSwipe = {}, + onClickUpload = {}, + onPoke = {}, + ) } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt index 696eb339..95ecbf97 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionEffect.kt @@ -33,6 +33,7 @@ fun ReactionEffect( targetReaction: ReactionUiModel?, modifier: Modifier = Modifier, spec: ReactionEffectSpec = ReactionEffectSpec(), + onFinished: () -> Unit = {}, ) { if (targetReaction == null) return @@ -59,6 +60,8 @@ fun ReactionEffect( particles.addAll(newParticles) + var remaining = newParticles.size + // 2. 파티클 애니메이션 실행 newParticles.forEach { particle -> scope.launch { @@ -145,6 +148,9 @@ fun ReactionEffect( ) particles.remove(particle) + + remaining -= 1 + if (remaining == 0) onFinished() } } } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt index 6f68fd1a..2961c377 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/component/reaction/ReactionUiModel.kt @@ -7,11 +7,11 @@ enum class ReactionUiModel( val type: GoalReactionType, val imageResources: Int, ) { - HAPPY(GoalReactionType.HAPPY, R.drawable.ic_keepi_happy), - TROUBLE(GoalReactionType.TROUBLE, R.drawable.ic_keepi_trouble), - LOVE(GoalReactionType.LOVE, R.drawable.ic_keepi_love), - DOUBT(GoalReactionType.DOUBT, R.drawable.ic_keepi_doubt), - FUCK(GoalReactionType.FUCK, R.drawable.ic_keepi_fuck), + HAPPY(GoalReactionType.HAPPY, R.drawable.ic_happy), + TROUBLE(GoalReactionType.TROUBLE, R.drawable.ic_trouble), + LOVE(GoalReactionType.LOVE, R.drawable.ic_love), + DOUBT(GoalReactionType.DOUBT, R.drawable.ic_doubt), + FUCK(GoalReactionType.FUCK, R.drawable.ic_fuck), ; companion object { diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt index 650100dd..00e57ece 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailIntent.kt @@ -8,7 +8,9 @@ sealed interface TaskCertificationDetailIntent : Intent { val type: GoalReactionType, ) : TaskCertificationDetailIntent - data object Sting : TaskCertificationDetailIntent + data object Poke : TaskCertificationDetailIntent data object SwipeCard : TaskCertificationDetailIntent + + data object MyReactionEffected : TaskCertificationDetailIntent } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt index 202348ef..92532bfb 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailSideEffect.kt @@ -8,4 +8,8 @@ sealed interface TaskCertificationDetailSideEffect : SideEffect { val message: Int, val type: ToastType, ) : TaskCertificationDetailSideEffect + + data class ShowPokeToast( + val message: String, + ) : TaskCertificationDetailSideEffect } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt index a907b700..f5d70449 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/detail/contract/TaskCertificationDetailUiState.kt @@ -5,6 +5,7 @@ import com.twix.domain.model.enums.BetweenUs import com.twix.domain.model.enums.GoalIconType import com.twix.domain.model.photolog.PhotoLogs import com.twix.domain.model.photolog.PhotologDetail +import com.twix.task_certification.detail.component.reaction.ReactionUiModel import com.twix.ui.base.State import com.twix.util.RelativeTimeFormatter import java.time.LocalDate @@ -20,11 +21,22 @@ data class TaskCertificationDetailUiState( val icon: GoalIconType = GoalIconType.DEFAULT, val myPhotolog: PhotologDetail? = null, val partnerPhotolog: PhotologDetail? = null, + val isCompletedGoal: Boolean = false, + /** + * 내 인증샷에 상대방이 리액션을 남겼을 경우 최초 1회 인터렉션 렌더링을 위한 변수 + */ + val hasShownMyReaction: Boolean = false, /** * 초기값으로 인해 찌르기/업로드 버튼이 렌더링 되는 것을 막기 위한 변수 - * */ + */ val isLoading: Boolean = false, ) : State { + /** + * 현재 [currentShow]에 해당하는 사용자의 인증샷 인증 여부 + * + * - [BetweenUs.ME]: 내 인증샷이 존재하면 `true` + * - [BetweenUs.PARTNER]: 파트너 인증샷이 존재하면 `true` + */ val isDisplayedGoalCertificated: Boolean get() = when (currentShow) { @@ -32,6 +44,12 @@ data class TaskCertificationDetailUiState( BetweenUs.PARTNER -> partnerPhotolog != null } + /** + * 현재 [currentShow]에 해당하는 인증샷의 업로드 시간을 상대적 시간 문자열 + * + * - [BetweenUs.ME]: 내 인증샷 업로드 시간 + * - [BetweenUs.PARTNER]: 파트너 인증샷 업로드 시간 + */ val displayedGoalUpdateAt: String get() = when (currentShow) { @@ -42,12 +60,17 @@ data class TaskCertificationDetailUiState( BetweenUs.PARTNER -> partnerPhotolog?.uploadedAt?.let { - RelativeTimeFormatter.format( - it, - ) + RelativeTimeFormatter.format(it) } ?: "" } + /** + * 현재 [currentShow]에 해당하는 인증샷 URL + * + * - [BetweenUs.ME]: 내 인증샷 이미지 URL + * - [BetweenUs.PARTNER]: 파트너 인증샷 이미지 URL + * + */ val displayedGoalImageUrl: String? get() = when (currentShow) { @@ -55,6 +78,13 @@ data class TaskCertificationDetailUiState( BetweenUs.PARTNER -> partnerPhotolog?.imageUrl } + /** + * 현재 [currentShow]에 해당하는 인증샷의 코멘트 + * + * - [BetweenUs.ME]: 내 코멘트 + * - [BetweenUs.PARTNER]: 파트너 코멘트 + * + */ val displayedGoalComment: String? get() = when (currentShow) { @@ -62,6 +92,12 @@ data class TaskCertificationDetailUiState( BetweenUs.PARTNER -> partnerPhotolog?.comment } + /** + * 현재 [currentShow]에 해당하는 사용자의 닉네임 + * + * - [BetweenUs.ME]: 내 닉네임 + * - [BetweenUs.PARTNER]: 파트너 닉네임 + */ val displayedNickname: String get() = when (currentShow) { @@ -69,23 +105,64 @@ data class TaskCertificationDetailUiState( BetweenUs.PARTNER -> partnerNickname } + /** + * 현재 화면이 내 인증샷을 표시하는 상태인지 여부 + * + * 내 인증샷일 때 `true` + */ val isDisplayedMyPhotolog: Boolean - get() = - currentShow == BetweenUs.ME + get() = currentShow == BetweenUs.ME + /** + * 내 인증샷를 수정할 수 있는지 여부 반환 + * + * 현재 내 인증샷를 보고 있고([isDisplayedMyPhotolog]), + * 인증이 완료된 상태([isDisplayedGoalCertificated])일 때 `true` + */ val canModify: Boolean - get() = - currentShow == BetweenUs.ME && isDisplayedGoalCertificated + get() = currentShow == BetweenUs.ME && isDisplayedGoalCertificated + /** + * 파트너 인증샷에 리액션을 남길 수 있는지 여부 + * + * 현재 파트너 인증샷을 보고 있고([BetweenUs.PARTNER]), + * 파트너의 인증이 완료된 상태([isDisplayedGoalCertificated])일 때 `true` + */ val canReaction: Boolean + get() = currentShow == BetweenUs.PARTNER && isDisplayedGoalCertificated + + /** + * 인증샷 업로드 또는 찌르기 액션 버튼을 표시할지 여부를 반환 + * + * 목표가 완료되지 않았고([isCompletedGoal]이 `false`), + * 현재 대상의 인증이 없는 상태([isDisplayedGoalCertificated]가 `false`)일 때 `true` + */ + val showActionButton: Boolean + get() = !isCompletedGoal && !isDisplayedGoalCertificated + + /** + * 내 인증샷에 달린 리액션 뱃지를 표시할지 여부를 반환 + * + * 내 인증샷을 보고 있고([isDisplayedMyPhotolog]), + * 리액션이 존재하며([myPhotolog]의 reaction이 non-null) 일 때 `true` + */ + val showMyPhotologReactionBadge: Boolean get() = - currentShow == BetweenUs.PARTNER && isDisplayedGoalCertificated + isDisplayedMyPhotolog && + myPhotolog?.reaction != null + + /** + * 내 인증샷의 리액션을 [ReactionUiModel]로 변환하여 반환 + */ + val myReaction: ReactionUiModel? + get() = myPhotolog?.reaction?.let { ReactionUiModel.find(it) } } fun PhotoLogs.toUiState( goalId: Long, betweenUs: String, selectedDate: LocalDate, + isCompletedGoal: Boolean, ): TaskCertificationDetailUiState { val currentGoalPhotolog = goals.firstOrNull { @@ -102,5 +179,6 @@ fun PhotoLogs.toUiState( icon = currentGoalPhotolog.icon, myPhotolog = currentGoalPhotolog.myPhotolog, partnerPhotolog = currentGoalPhotolog.partnerPhotolog, + isCompletedGoal = isCompletedGoal, ) } diff --git a/feature/task-certification/src/main/java/com/twix/task_certification/navigation/TaskCertificationGraph.kt b/feature/task-certification/src/main/java/com/twix/task_certification/navigation/TaskCertificationGraph.kt index 8890a49f..30cb6ed1 100644 --- a/feature/task-certification/src/main/java/com/twix/task_certification/navigation/TaskCertificationGraph.kt +++ b/feature/task-certification/src/main/java/com/twix/task_certification/navigation/TaskCertificationGraph.kt @@ -37,6 +37,10 @@ object TaskCertificationGraph : NavGraphContributor { navArgument(NavRoutes.TaskCertificationDetailRoute.ARG_BETWEEN_US) { type = NavType.StringType }, + navArgument(NavRoutes.TaskCertificationDetailRoute.ARG_IS_COMPLETED) { + type = NavType.BoolType + defaultValue = false + }, ), ) { TaskCertificationDetailRoute(