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(